Pattern: External Callback
Context
Interfacing with external software systems involves not only
calling externally implemented functions, but also implementing
functions to be used by external systems (callbacks). We need a
generic mechanism for implementing callbacks from other languages
in Smalltalk.
Solution
Implement an ExternalCallback block which external
systems can be passed like a function pointer to external systems
to enable them to call back into Dolphin.
- Define any new external
structures for any structure or pointer parameters
you are going to use in the callback block which are not
already defined.
- Define an argument type string describing the callbacks
parameter types mapped to the appropriate external
parameter types. The type string does not include the
return type (the result answered by the block will be
sent the #asLRESULT message for conversion to a
32-bit return value, the only possible return type at
present).
- Define a block with the correct number of arguments for
the callback procedure, inside which you implement your
callback functionality. The block can contain whatever
code is necessary to implement the callback. For many
callbacks, the return value is important, e.g. to
continue or terminate enumerations. The return value is
the result of the last expression in the block, and
should be an object with a conversion to 32-bit integer
when sent #asLRESULT.
- You may need to add an external
method to the appropriate external library function
to enable you to register your callback function with the
relevant external library.
Use an lpvoid parameter type for the callback
argument (the function pointer), and send the external
callback the #asParameter message to persuade it
to answer its machine code thunk. If the callback
is for the use of a control (or some other type of
window) which has a SendMessage() interface,
then you may need to define an appropriate external
structure for a parameter block, or you may have to
pass the external callback directly as the lParam.
- Create an ExternalCallback instance using the #block:argumentTypes:
message, passing (respectively) the callback block and
the argument type description string. You must retain a
reference to this external callback object to prevent it
being garbage collected, as although the ExternalCallback
class maintains a register of its instances, the register
is a weakling.
- If a particular callback is frequently created, then
consider using a precreated ExternalDescriptor (which
holds a "compiled" representation of the
argument type string) in conjunction with the ExternalCallback>>block:descriptor:
instantiator. This technique is illustrated in the
example.
- Pass your external callback object to the library method
you defined, and wait for the callbacks to come pouring
in!
- When you've finished with the callback, you can
explicitly #free it. If you're not sure when
you'll have finished with it, then simply leave it to be
finalized when Dolphin garbage collects it (see Weak
References and Finalization).
An important callback example in the development system is
that implemented for streaming text out of a rich edit control in
RichTextEdit. (the browsers would not work without it). RichTextEdit
actually defines two callbacks (one for streaming in, and one for
streaming out), but that for streaming out is as follows:
streamIn: aStream format: streamFormat
"Private - Read text from the stream aStream.
The receiver holds on the to the stream it is reading, because
the RichEdit control appears to read from it asynchronously.
Answer the number of characters read from aStream.
Implementation Note: Extend the life of the old 'stream' to the end of the method
in case control still using it - BUT we must be very careful not to cause a reference
to the old callback to be kept in this method context, otherwise we'll build a huge
linked list of callbacks, blocks, and method contexts, etc."
| answer callback text size |
callback :=
ExternalCallback
block: [ :dwCookie :pbBuff :cb :pcb |
text := aStream nextAvailable: cb.
size := text size.
pbBuff replaceFrom: 1 to: size with: text startingAt: 1.
pcb value: size.
0 "The help is confusing/wrong. We must return 0 to continue streaming."]
descriptor: ##(ExternalDescriptor argumentTypes: 'dword lpvoid sdword DWORD*').
winStruct
pfnCallback: callback asParameter yourAddress.
answer := self sendMessage: EM_STREAMIN wParam: streamFormat lpParam: winStruct.
self setModify: false.
"It seems we have to increase the limit again after streaming in."
self setMaxTextLimit.
streamIn isNil ifFalse: [streamIn free].
streamIn := callback.
^answer
Notice how the arguments to the block are already objects of
suitable types, and can be used directly, even the DWORD*
parameter.
Like any other block, the callback block captures the
environment in which it was created, so we can directly reference
all the closure information we need (e.g. the argument to the
method, aStream)
The interface to the rich edit control is SendMessage() based,
and so uses a parameter block to hold the function pointer and
cookie ("extra data" or closure information). We don't
need the cookie, so we just pass 0, and ignore the argument in
the callback block.
Known Uses
- RichTextEdit control RTF streaming.
- Enumeration of Locale specific time and date
formats.
- Enumeration of Font characteristics.
- Enumerating top-level Views