Callbacks

Top  Previous  Next

What are callbacks? - Don't call us, we'll call you.

Think of a callback as a phone number that you pass on to someone else. He's supposed to call that phone number if he has a certain problem or wants to pass on some information. If you want the whole process to work you must provide someone to answer the phone with this number who can answer the specific questions that might be asked or who can react to the information delivered to him. Of course things are a little different in a program. In most cases the person you pass your phone number will be the operating system. The phone number is the address of a function the O/S will call if it has a question or some information for your program. The type of questions asked or the information type delivered is known beforehand by the definition of parameters. All you have to do is set your program up to answer the questions that will be asked or handle the information passed to you.

 

The problem with Xbase++ is that it does not understand the questions or the information passed because it does not speak C, the language Windows was written in. Xbase++ has a completely different way of passing and receiving function parameters and return values. Hence It cannot receive the parameters passed or return a value the caller would understand.

 

The native Xbase++ solution is BAP, the Binary Access Package. Unfortunately it only allows you to set up one callback per thread and it comes with some other major limitations.

 

With Cockpit you can create an unlimited number of callbacks, they are thread-safe and can handle up to 30 parameters. Cockpit's callbacks support different calling conventions. This can be useful if you are using a dll that assumes the __cdecl calling convention in a callback function. Cockpit callbacks provide basic parameter and return value translation. All parameters passed by the O/S will arrive as LONG values in your Xbase++ callback. The values you can return can be LONG values or logical values which are automatically translated to TRUE and FALSE. All other types will return a NULL value to the caller.

Cockpit Callbacks can be used

 

to provide a Window Procedure
to provide a Window Class Procedure
to subclass Windows
to superclass Window classes
to provide callbacks for enumerations
to provide special event handlers, e.g. for timer events
to provide callbacks for COM events

The Cockpit functions involved

To install a callback you can use the functions Callback and RequestCallback. Callback will call SysError if the callback cannot be installed, RequestCallback will return a NIL value to indicate the failure. Your program is responsible to handle the problem in this case. If a callback isn't required anymore, it should be released with ReleaseCallback to free up the memory consumed by it. To replace the function called by a callback you can use ReplaceCallback. This can be helpful if some initialization must be done during the first call that can be skipped in subsequent calls.

 

The constants and some command extensions for callbacks can be found in the file Callback.ch.

How do Cockpit callbacks work?

I'll try to illustrate the process with a simple example. The API function EnumWindows is used to enumerate all top level Windows on your computer. It will call a callback function you must provide for every top-level window. When it calls your callback function, it will pass a window handle you can use for further action, for example to retrieve the text in its title bar. Here is the code you could use to do exactly that:

 

 

* Example - A

 

#INCLUDE "dll.ch"

#INCLUDE "CockpitCoreLibs.ch"

 

**************** Declare the Dll functions necessary *****************

DLLFUNCTION EnumWindows( callBack, xp )         USING STDCALL FROM "user32.dll"

DLLFUNCTION GetWindowTextA( handle, @txt, size ) USING STDCALL FROM "user32.dll"

 

 

**************** This function is called for every window *****************

FUNCTION EnumWindowsProc(handle,xp)

 

* Prepare a buffer to receive the text

LOCAL tx := SPACE(100)

 

* Get the title bar's text

GetWindowTextA(handle,@tx,100)

 

* Display it, but just up to the first Chr(0) character

? LeftChr0(tx)

 

* If a Window with "Palm Desktop" in the title is found, enumeration is cancelled

IF LeftChr0(tx) == "Palm Desktop"

  RETURN .F.   && You could also return FALSE

ENDIF

 

* Continue enumeration

RETURN .T.  && You could also return TRUE

 

 

*************** Here our program starts **************

FUNCTION Main

 

LOCAL callback

 

* Create a callback that will pass on to EnumWindowsProc when called

callback := Callback({|hdl,lp|EnumWindowsProc(hdl,lp)})

 

* Start enumerating all windows

 

EnumWindows(callback,0)

 

RETURN NIL

 

 

 

 

Let's take a look at the Main function. First a callback is created:

 

callback := Callback({|hdl,lp|EnumWindowsProc(hdl,lp)})

 

This line tells Cockpit to provide a callback that will receive 2 parameters and pass them on to EnumWindowsProc. The callback is returned as a numeric value which actually is a memory address. This value can be passed as "callback address".

 

The line

 

EnumWindows(callback,0)

 

passes the callback address to EnumWindows. The extra parameter is not of any importance in this example. Inside EnumWindows the operating system checks which top-level windows are waiting for us and calls the callback function passing one handle (and the extra parameter) every time.

This results in a call to EnumWindowsProc with the two parameters coming in as defined in the codeblock. All that's left to do is retrieve the window text and display it. If you read the MSDN documentation for EnumWindows you'll find that you can terminate the enumeration by returning a value of FALSE from the callback. That's what happens in the sample when our callback function hits a window title of "Palm Desktop".

 

Now there are few more things to spend some thoughts on.

 

1. What happens to the callback after return from EnumWindows? - Well, if you intend to use this function again later on, you can just sit back and do nothing. Great. If you are never going to need this callback again, it's a good idea to release the callback with ReleaseCallback. This will free the memory which was used to provide the callback.

 

2. What would happen if Windows saved the callback address and call it later? In this case we're lucky, it won't. But for example window procedures are not only called as a consequence of a call to a function after which we are free to get rid of the installed callback. In this case we may not release the callback unless we know it will not be called anymore.

 

Obviously there are two types of callbacks: Temporary ones (used only from a function we call first) and static ones (called from anywhere during the further course of our program). The temporary ones should be removed right after the framing function was left and the static ones should be released when they have been used for the last time. This can become tricky and for windows procedures that happens inside the callback function. Yes, Cockpit allows you to remove callbacks from inside themselves. The last message a window will receive before being dumped forever is WM_NCDESTROY. Cockpit windows release their window procedure callbacks when this messages arrives. As a general rule you should always release a callback in the thread in which it was created.

 

But now let's take a look at how we could improve the above program a little. First let's get some help from the preprocessor. By adding

#INCLUDE "Callback.ch" we can get rid of all the codeblock business necessary in example B.

 

 

* Example B

 

#INCLUDE "dll.ch"

#INCLUDE "CockpitCoreLibs.ch"

#INCLUDE "Callback.ch"

 

**************** Declare the Dll functions necessary *****************

DLLFUNCTION EnumWindows( callBack, xp )         USING STDCALL FROM "user32.dll"

DLLFUNCTION GetWindowTextA( handle, @txt, size ) USING STDCALL FROM "user32.dll"

 

 

**************** This function is called for every window *****************

CALLBACK FUNCTION EnumWindowsProc(handle,xp)

 

* Prepare a buffer to receive the text

LOCAL tx := SPACE(100)

 

* Get the title bar's text

GetWindowTextA(handle,@tx,100)

 

* Display it, but just up to the first Chr(0) character

? LeftChr0(tx)

 

* If a Window with "Palm Desktop" in the title is found, enumeration is cancelled

IF LeftChr0(tx) == "Palm Desktop"

  RETURN .F.   && You could also return FALSE

ENDIF

 

* Continue enumeration

RETURN .T.  && You could also return TRUE

 

 

*************** Here our program starts **************

FUNCTION Main

 

* Start enumerating all windows

EnumWindows(EnumWindowsProc(),0)

 

RETURN NIL

 

 

 

Now we declare the function EnumWindowsProc as a CALLBACK FUNCTION. This will advise the PP to create a function EnumWindowsProc which will return the address of the callback that will be automatically created during the first call. If you want to see the details, compile the program with the /p option and take a look at the result:

 

 

FUNCTION EnumWindowsProc

STATIC cbaddress

IF cbaddress == NIL

  _ThreadSafeCreateCallback_(@cbaddress,{|handle,xp|EnumWindowsProc_(handle,xp)},0,;

                             "EnumWindowsProc");

  IF cbaddress == 0

     SysError("Couldn't register callback.")

  ENDIF

ENDIF

RETURN cbaddress

 

STATIC FUNCTION EnumWindowsProc_ ( handle,xp)

 

* here the actual code of your function will appear

.

.

.

 

 

The call to _ThreadSafeCreateCallback_ is required because EnumWindowsProc could be called from different threads simultaneously. If that happened and with a lot of bad luck the callback could be created twice. _ThreadSafeCallback_ handles this problem, the callback will be created only once.

 

After this change we cannot use the function directly any more because it takes no parameters and returns the address of a callback. But we can simply use it's return value when we call EnumWindowsProc. Unfortunately the callback will remain in memory forever. This is not a good choice in this case, but it might be under different circumstances.

 

Cockpit makes things even simpler for you if you choose to link with the platform library. Since the first parameter of EnumWindows is declared as a CALLSUB, we can either pass a codeblock or the address of a callback function. And, of course, retrieval of the window text becomes easier as well. Here goes our final one-liner:

 

 

* Example C

 

#INCLUDE "CockpitPlatformLibs.ch"

#INCLUDE "Callback.ch"

 

FUNCTION Main

 

LOCAL tx

 

* Enumerate all windows until you hit "Palm Desktop"

EnumWindows({|hdl,xp|tx:=GetWindowText(hdl),QOut(tx),IIF(tx=="Palm Desktop",.F.,.T.)})

 

RETURN NIL

 

 

 

I think that can't be done any simpler. Now Cockpit takes care of creating the callback and releasing it after the call. We could also have declared the CALLBACK FUNCTION as in example B and passed the address by calling it, but it would have remained in memory forever. So I'd say we're done with this solution.

Some more important details

1. Parameter count

 

Cockpit derives the number of parameters by analyzing the codeblock you pass to RequestCallback. The number of parameters must exactly match the number of parameters passed by the caller. Otherwise your program is doomed because the processor stack will get corrupted.

 

2. Parameter types

 

No matter which parameter types are passed by the calling function, you will always receive LONG values as parameters. Your callback function is responsible to reinterpret them if necessary, for example by reading the string a parameter points to or by converting to an unsigned type. You can use Cockpit's Type Conversion and Pointers and Memory functions to accomplish this task.

 

3. Calling convention

 

Every callback adheres to a calling convention. This calling convention is passed to the Callback function as second parameter. The calling conventions available are CALLBACK_CALLCONVENTION_STDCALL and CALLBACK_CALLCONVENTION_CDECL which can be found in Callback.ch. The default calling convention is STDCALL. Again, if you don't use the correct calling convention the processor stack will get corrupted and your program will stall.

The calling convention to be used can also be specified for CALLBACK FUNCTIONs. Just add the word "CDECL" or "STDCALL" between "CALLBACK" and "FUNCTION". Again, STDCALL is the default.

 

4. Return values

 

The return value of your callback function can either be a numeric value in the LONG range (which will be the return value of your callback to the caller) or a logical value. A logical value will internally be converted to TRUE or FALSE, any other type will return a NULL value to the caller.

 

5. Reference and pointer parameters

 

Reference parameters and complex parameters are passed from C through a pointer. If you want to access these values or return a value you must read or change the data at the address pointed to by the parameter. You can use the Pointers and Memory functions to accomplish this task.

 

6. Releasing a callback

 

A callback should be released by the thread it was created in. It can be released inside your callback function during the last call to it. Releasing a callback that is currently active in a different thread can stall your program. A call to a callback that was already released will also stall your program.

Language Extensions available

With Callback.ch Cockpit supplies some language extensions for the declaration of callback functions:

 

[STATIC] CALLBACK [<convention>] FUNCTION <funcname> [(<parameterlist,...>)]

 

This will create either a STATIC or non-STATIC function named <funcname> returning a callback address. The installed callback will adhere to the calling convention specified with <convention> which may be CDECL or STDCALL. The default convention is STDCALL. The installed callback will execute the code following after this statement. The parameters given in <parameterlist> will be available to your code just like in a normal function.

The "Big Circle"

Now you know what callbacks are and how to implement them. But Cockpit does not only provide you with the means to create callbacks, it does also provide you with the tools to directly call C functions from your Xbase++ programs. As you learned in the previous paragraphs, creating callbacks is like faking C functions to the O/S or any other caller. So, shouldn't it be possible to call Cockpit callbacks with the straight C call functions? Indeed, it is. Let's have a look at a sample that will show how to pass parameters to C functions and how to retrieve parameters passed from a C function in Cockpit callbacks.

 

 

#INCLUDE "CockpitCoreLibs.ch"

#INCLUDE "Callback.ch"

#INCLUDE "ExtendedStructures.ch"

#INCLUDE "StringTypes.ch"

 

FUNCTION Main

 

LOCAL l1,l2,f1,f2,s1,s2,stru,rc,struAddress

 

* LONGs are passed directly

l1 := 111

 

* POINTERs to LONGs require us to allocate memmory and pass a pointer

l2 := LOCKSTRING(L2BIN(0))

 

* FLOATs are too big to be passed directly, so again we have to allocate

* some memory. Then we write the data to this memory ADDRESS and pass

* the pointer to this memory ADDRESS

f1 := LOCKSTRING(F2Bin(47.11))

 

* There's no difference in the way you pass complex data by pointer and

* passing data by pointer because you expect a return value.

f2 := LockString(F2Bin(0))

 

* The same applies to strings, they are always passed through a pointer,

* no matter if we expect a return value or not.

s1 := LockString("Extended necks.",STRINGTYPE_TEXT)

s2 := LockNullString(100,STRINGTYPE_TEXT)

 

* Structures require some extra care...

 

* This creates the structure object

stru := SAMPLESTRUCTURE():New()

 

* Then we assign some values

stru:longvalue := 747

stru:string := "Just some buffer to be filled downstairs."

 

* Now we allocate memory and write the structure data to it

* :Lock() allocates extra memory for the pointer member :string

struAddress := LockString(stru:Lock():buffer)

 

* And we save the memory address in the structure for later use

stru:memoryAddress := struAddress

 

* Now we pass the data and the pointers

rc := CallAddress(MyCallback(),l1,l2,f1,f2,s1,s2,struAddress,1,2,3)

 

* Let's see what was returned...

 

* The doubled LONG must be retrieved through the pointer

? "The doubled LONG:" , BIN2L(UNLOCKSTRING(l2))

 

* The same happens WITH the FLOAT

? "The doubled FLOAT:", Bin2F(UNLOCKSTRING(f2))

 

* And we have to read back our modified STRING

? "The modified string:", LEFTCHR0(UNLOCKSTRING(s2))

 

* Let's read back the structure data from memory (:memoryAddress was

* assigned beforehand to allow this.

stru:ReRead()

 

* We should not forget to release the extra memory allocated for :string

stru:Unlock()

 

* Let's see what we've got in our structure now.

? "The longvalue of the structure: ",stru:longvalue

? "The string in the structure:",stru:string

 

RETURN NIL

 

 

**************** This function is called for every window *****************

 

CALLBACK FUNCTION MyCallback(longByValue,longByReference,;

                            float,floatByReference,;

                            string,stringByReference,;

                            structureByReference)

 

LOCAL temp

 

* Double the LONG and return it in through the second parameter

temp := longByValue * 2

PokeLong(longByReference,temp)

 

* Retrieve the Float value and return it through the 4th parameter

temp := Bin2F(CopyMemoryToString(float,,Len(F2Bin(0))))

temp := temp * 2

CopyStringToMemory(floatByReference,F2Bin(temp))

 

* Retrieve the string, replace every e with a space and return it through the 6th

* parameter

temp := CopyMemoryToString(string,STRINGTYPE_TEXT)

temp := StrTran(temp,"e"," ")

CopyStringToMemory(stringByReference,temp,STRINGTYPE_TEXT)

 

* retrieve the structure and put some values in

temp := SAMPLESTRUCTURE():NewFrom(structureByReference)

temp:longvalue := temp:longvalue * 2

temp:string := "Beam me up, Scotty."

 

* Commit the changes to memory

temp:Commit()

 

RETURN 0

 

 

 

***************** We'll need this STRUCTURE to pass some data *************

MEMBERLIST SAMPLESTRUCTURE

  MEMBER longvalue

  PTR MEMBER string

ENDMEMBERLIST

 

STRUCTURE SAMPLESTRUCTURE

  LONG longvalue

  PTR STRING string

ENDSTRUCTURE