BlitzMax supports function pointers, but they can be a bit tricky at times.
Function my_callback_function:String(name:String) Return name + " was called!" End Function Local function_pointer:String(name:String) = my_callback_function Print function_pointer("callback")
The example above will print “callback was called” when run.
This approach works well enough, but what if you’d like to call a method on an object? You need to work around it a little bit:
Type MyType Function MyCallback:String(obj:MyType, name:String) return MyType(obj).myActualCallback(name) End Function Method myActualCallback:String(name:String) return name + " was called!" End Method End Type Local callback:String(obj:MyType, name:String) = MyType.MyCallback Local instance:MyType = new MyType Print callback(instance, "callback")
This isn’t ideal, as it means all methods need wrapping by another function as well as have the object passed in.
Using BlitzMax reflection
BlitzMax has a nice reflection system, which makes it possible to get information about a variety of things at runtime. This can be used to query a type and its variables, methods and functions.
Type method information is returned in a
TMethod object which contains the method name, parameters and return type. It also has a method called
invoke, which calls the method on an object instance.
Using the example
MyType above, we could get information on
myActualCallback using the following:
Local objectInfo:TTypeId = TTypeId.ForName("MyType") Local methodInfo:TMethod = objectInfo.FindMethod("myActualCallback")
When combined with
invoke, a method on
instance can be executed without explicitly calling it.
' Executes the 'myActualCallback' method on our instance. Print String(methodInfo.invoke(instance, ["invoked callback"]))
invoke method takes two arguments: an object instance, and an array of objects that are passed as parameters to the method.
Wrapping it all up
With a little wrapper class we can call a method on any object instance.
SuperStrict Import brl.reflection Type CallbackWrapper Field _caller:Object Field _method:TMethod Method execute:Object(args:Object) Return Self._method.Invoke(Self._caller, args) End Method Function Create:CallbackWrapper(caller:Object, methodName:String) Local this:CallbackWrapper = New CallbackWrapper this._caller = caller this._method = TTypeId.ForObject(caller).FindMethod(methodName) ' Must be a valid method If this._method = Null Then Throw "Cannot create a callback for missing method: " + methodName EndIf Return this End Function End Type
This allows us to do the following using the
MyType object from earlier:
Local instance:MyType = new MyType Local wrapper:CallbackWrapper = CallbackWrapper.create(instance, "myActualCallback") Print String(callbackInstance.execute("wrapped callback"))
One nice thing about this approach is it can be used on any object, so you don’t have to worry about extending a base class. Also, because the callbacks are BlitzMax objects they can be stored in data structures like
TList. Function pointers cannot.
This approach is not without its drawbacks. Because
Object for its arguments and return values, there usually needs to be a little data massaging to get it working.
To get around this I’ll usually write an
execute method for specific method signatures I want:
' Wrapping the `execute` method with something nicer. Method execute_callback:String(name:String) Return String(self._method.invoke(self._caller, [name])) End Method
There is a performance hit to using this approach. Testing each approach 10,000,000 times gave the following results on a Linux machine:
|Method Used||Total Time (Milliseconds)|
|Type Function Pointer||570|
|Reflection-based Callback (wrapped)||3878|
Wrapping the callback with casting slows things down considerably, so there is a trade-off between readability and speed.
I’ve used this approach in a couple of places in my own projects:
- The pangolin.events module uses it for event handlers. Using this system allows events to have handler objects or methods added and removed dynamically at runtime.
- The pangolin entity factory uses it to check if an object has an
initializeFromTemplatemethod. If it does, the method is called. Otherwise a warning is signalled and the object is created using reflection.
- A couple of tools use it to provide some very basic scripting. For example, something like
addCommand("command_name", MyCallback.create(object, "exec_command_name"))can be used to dynamically add commands.