Callbacks |
Top Previous Next |
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.
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. 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:
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.
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:
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:
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. 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. 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. 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.
|