Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics
 DbiregisterCallback for Delphi 1.x
See Also: Delphi Papers and Columns

Question: We would like to be aware when another user changes a record in one of the tables that I'm using; i.e. we don't want to use exclusive table access, but share the tables among multiple users. How can we get our Delphi applications to detect such changes?

Answer: For Paradox tables, the DbiRegisterCallback API allows you to register a callback function that gets called whenever your table changes. Be aware that when the callback function gets called, your data segment DS will be the one of the calling application (the one that caused the callback), so you need to re-set your DS. This is because smart callbacks are needed for the VCL, and they cause the 'wrong' DS to be loaded. One solution may be to hack into the prologue code and (try to) undo the smart callback code, while the solution that we present in this article is simply to get the right data segment DS that belongs to this code segment CS).


DbiRegisterCallback
The unit DBIPROCS (see DBIPROCS.INT in the DELPHI\DOC subdirectory) lists the routine DbiRegisterCallback. The BDE on-line helps tells us that this routine can be used to register a callback function for a BDE-client application that gets triggered when your table changes:

 function DbiRegisterCallBack( { Register a BDE callback function }
   hCursor     : hDBICur;      { Cursor (Optional) }
   ecbType     : CBType;       { Type of callback }
   iClientData : Longint;      { Pass-thru client data }
   iCbBufLen   : Word;         { Callback buffer len }
   CbBuf       : Pointer;      { Pointer to callback buffer }
   pfCb        : pfDBICallBack { Callback function being registered }
   ): DBIResult;
DbiRegisterCallBack allows us to install a callback function that is called by the Borland Database Engine upon the occurrence of a certain event. The DbiRegisterCallback function has 6 parameters: All callbacks are applicable to the current session only. The callback is valid only while the cursor is open; when the cursor is closed, any cursor-specific callbacks are automatically unregistered (so it doesn't even matter if we accidently 'forget' to de-install a callback function). If hCursor is 0, then the callback applies to all cursors in the current session that do not have an explicit callback of their own.

cbTableChanged
The cbTableChanged type of callback is the one we need to inform us of changes in our table. For this callback type we only need to pass the handle of the table and the callback routine itself. Hence, the call to the cbTableChanged type of DbiRegisterCallback looks like this:

 var cbResult: DBIResult;
 begin
   cbResult := DBIRegisterCallback(Table1.Handle,
                                   cbTableChanged,
                                   0,0,nil, RefreshTable);
 end;
Where Table1 is our table and RefreshTable is our callback function of type pfDBICallBack. The file DBITYPES.INT in DELPHI\DOC contains this type definition:
 type
   ppfDBICallBack = ^pfDBICallBack;
   pfDBICallBack  = function ( { Call-back funtion pntr type }
       ecbType      : CBType;  { Callback type }
       iClientData  : Longint; { Client callback data }
   var CbInfo       : Pointer  { Call back info/Client Input }
    ): CBRType;
The engine calls the client-registered function when the pertinent event occurs, and the client responds to the callback by telling the engine what to do with the appropriate return code of type CBRType. Again from DBITYPES.INT:
 type
   pCBRType = ^CBRType;
   CBRType  = (         { Call-back return type }
     cbrUSEDEF,         { Take default action }
     cbrCONTINUE,       { Continue }
     cbrABORT,          { Abort the operation }
     cbrCHKINPUT,       { Input given }
     cbrYES,            { Take requested action }
     cbrNO,             { Do not take requested action }
     cbrPARTIALASSIST   { Assist in completing the job }
   );
With this information we can construct our RefreshTable callback routine, which looks like this:
 function RefreshTable(ecbType: CBType; iClientData: LongInt;
                   var CbInfo: Pointer): CBRType; export;
 begin
   ...
 end {RefreshTable};
For our cbTableChanged callback type we don't need to bother with the iClientData or CbInfo parameters. And the ecbType will be equal to cbTableChanged. And at the end of our callback routine, we should return the value cbrCONTINUE to indicate that we've done our job. So far, so good.

Who is General Protection? And why is it his Fault?
But the problems show up when you want to 'do' something in this callback. You get an instant GPF whenever you want to call Form1.Table1.Refresh, or even Application.ProcessMessages.

 {$F+}
 function RefreshAddress(ecbType: CBType; iClientData: LongInt;
                     var CbInfo: Pointer): CBRType; export;
 { this function will be installed as our callback routine }
 begin
   {$IFDEF CRASH}
     if Assigned(Form1) then
       Form1.Table1.Refresh;
   {$ENDIF}
   Result:= cbrContinue
 end {RefreshAddress};
In fact, any read or write to one of the global variables in our application seems to cause a GPF somehow. And strange as it may sound, a breakpoint in the callback routine never gets activated! It's as if Windows' is telling the debugger that the callback routine really isn't being executed in this task, but in the BDE itself (the BDE that is causing the callback to be called in the first place).
Once we're inside this callback function, we seem to be severely limited in what we can do!

I've set good-old MessageBox statements (that did not GPF) with the values of the stack, data and code segments (SS, DS and CS), and the result showed me that the callback routine was in fact called with the SS and DS (SS == DS) of the BDE, while the CS is the correct one of our application. The mapping CS-DS didn't seem to matter anymore. And this explained why we get a General Protection Fault when we try to access anything in 'our' data segment: we're no longer in our data segment, but in the BDE's data segment. And if we call Form.Table1.Refresh, for example, we're using the offset of Form1 to point to some place deep within the BDE's data segment; causing a Protection Fault since we don't belong there!
The fact that we end up with the wrong data segment is caused by the {$K+} smart callbacks compiler directive that is needed for the Visual Class Library. In short: with {$K-} your application won't work at all if you've used VCL components. And with {$K+} the smart callbacks enable special prologue/epilogue code that results in the wrong Data Segment in our DbiRegisterCallback callback routine. A no-win situation? Fortunately not!

The only solution is to restore the old CS/DS mapping (set the old DS that was the same when this CS started) and go on 'touching' the global vars of our application. Of course, the smart exit code will make sure the DS is 'restored' when the callback would return to the BDE again.

We just have to find a way to save and restore the contents of the original Data Segment DS register. At first, I used a very dirty hack and saved it in the Inter-Application Communication Area (at $0040:$00F0). However, this area is for all applications to use, so this solution will probably not work everywhere!

RESTORED.DLL
The final working solution is based on the concept of a Dynamic Link Library. DLLs have one data segment of their own, and no stack segment (they use the stack of the calling application). If we could somehow call a function from another DLL, then that DLL could somehow give us the value of our Data Segment DS register back. Of course, we need to tell it on beforehand what the value of our Data Segment DS register is. And we need a way to make sure the DLL knows which value to return (after all, this DLL can be in use by several applications at the same time, which all have a different data segment). Fortunately, one thing that will be right at all times is our Code Segment CS. All we need to do is use the value of the CS register as our 'key' to get the value of our 'DS' register. All the DLL needs to do is keep a table of CS/DS register combinations in memory. That's not hard to do, and should work like a charm.

So, I wrote RESTORED.DLL to 'store' and 'retrieve' our DS. Three interface functions are all that are needed to service several applications at the same time:

 procedure SetSegments(CodeSegment, DataSegment: Word); far;
           external 'RESTORED' index 1;
 { sets the value of the CodeSegment and corresponding DataSegment }

 function GetDataSegment(CodeSegment: Word): Word; far;
          external 'RESTORED' index 2;
 { returns the DataSegment that belongs to this CodeSegment }

 procedure ClearSegments(CodeSegment: Word); far;
           external 'RESTORED' index 3;
 { clears the Segments values belonging to this CodeSegment }
Any application that includes the above three function definition and has the RESTORED.DLL available (for example in the WINDOWS\SYSTEM directory) can make use of these features.

Usage
The first thing we need to do in our application is make sure the RESTORED.DLL knows our CS and DS registers:

 procedure MyExitProc; far;
 var CSSeg: Word;
 begin
   asm
       mov   AX, CS
       mov   CSSeg, AX
   end;
   ClearSegments(CSSeg)
 end {MyExitProc};

 var CSSeg, DSSeg: Word;
 initialization
   asm
       push  DS
       pop   AX
       mov   DSSeg, AX
       mov   AX, CS
       mov   CSSeg, AX
   end;
   SetSegments(CSSeg, DSSeg);
   AddExitProc(MyExitProc)
 end.
Now that we've done that, we can start writing our callback routine. Note that we need to do our business as fast as possible. And the best way to do that is to use a Boolean "NeedRefresh" flag to indicate that the table needs a refresh and return immediately. Of course, we need to re-set our correct DS first, using the RESTORED.DLL:
 {$F+}
 function RefreshAddress(ecbType: CBType; iClientData: LongInt;
                     var CbInfo: Pointer): CBRType; export;
 { this function will be installed as our callback routine }
 var CSSeg, DSSeg: Word;
 begin
   asm
      mov   AX, CS
      mov   CSSeg, AX
   end;
   DSSeg := GetDataSegment(CSSeg);
   asm
      mov   AX, DSSeg
      push  AX
      pop   DS
   end {restore DS};
   if Assigned(Form1) then Form1.NeedRefresh := True;
   Result:= cbrContinue
 end {RefreshAddress};
Now that we've set a flag to indicate that the Table1 needs a refresh, it's up to our application to detect the change in that flag. Fortunately, we can use the Application's OnIde event for this task as follows:
 procedure TForm1.AppIdle(Sender: TObject; var Done: Boolean);
 begin
   Done := True;
   DbiCheckRefresh; { needed for remote tables }
   if NeedRefresh and Table1.Active then
   begin
     NeedRefresh := False;
     Table1.Refresh
   end
 end {AppIdle};
Now all we need to do is to install this OnIdle event in the FormCreate:
 procedure TForm1.FormCreate(Sender: TObject);
 begin
   NeedRefresh := False;
   Table1.Open;
   Application.OnIdle := AppIdle;
 end {FormCreate};
And that's really all there is to it. For a more detailed source code example, click here.

Summary
Actually, the situation is as follows: there are two applications that are concurrently updating a database. Each one want to know, of course, when the other one has actually made a change, so it (the one who didn't made the change itself) can do a 'refresh' and get synchronised again. This is not a very 'special' situation, and will be a real issue on network databases!

Anyway, the BDE offers the option to install a callback function (with DbiRegisterCallBack, see example in DB.PAS) that gets triggered whenever you like (well, not exactly, but you can make it trigger whenever a change is made to a certain table, for example). So, if say three apps are all working on a single table, and App1 makes change, then the BDE is responsible for detecting this change and calling the callback (for whichever App has it installed). Now, it seems that the BDE is able to call the callback whenever it's active, and that is indeed whether or not the app (for which the callback is called) is active or not. So, we just restore the DS that belongs to that CS and go on doing our business (fast and save, of course, you never know who's active this time - we don't want to crash its stack).


Acknowledgements
Phil Goulson of the UK-BUG (UK Delphi Developers' Group) was actually the first one to make me aware of this callback problem about a year ago. He used the solution that I present in this article to write a very useful DDGDBI.PAS unit that can be a powerful addition to your Delphi routines.


This webpage © 2000-2017 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.