Callback function within a type

BlitzMax Forums/BlitzMax Programming/Callback function within a type

I often want an instance of a type to fire a message to an instance of another type at a certain moment.
I thought a Callback function could be used for that but I can't address a callback function within a type.
The example below is a completely useless piece of code but it illustrates what I want.

I have an instance of a TControl type.
This instance initialize an instance of a TProcess type. After a while the Process instance needs to tell the Control instance "I'm ready".

Ofcourse, I can make a reference to the Control instance within TProcess but then I need to know it's type when calling it. Like I show in the second example. But that requires me to make one generic type and extend all my types from that type. That's not what I prefer to do.

What's best?

SuperStrict

Global o:TControl = TControl.Create()
o.Start(500)

While Not KeyHit(KEY_ESCAPE)
	o.Update()
Wend

End

Function StopIt()
	Print "Stop"
	o.Stop()
End Function


Type TProcess
	Field piStart%
	Field piDuration%
	Field poCallback()
	
	Function Create:TProcess(iDuration%, oCallback())
		Local o:TProcess = New TProcess
		o.piDuration = iDuration
		o.poCallback = oCallback
		o.piStart = MilliSecs()
		Return o
	End Function

	Method Update()
		Print "Busy"
		If MilliSecs()-piStart>=piDuration Then poCallback()
	End Method

End Type


Type TControl
	Field poProcess:TProcess

	Function Create:TControl()
		Local o:TControl = New TControl
		Return o
	End Function

	Method Start(iDuration%)
		Print "Start process..."
'This does not work, but it is what I actually want...
		poProcess = TProcess.Create(iDuration, Self.Stop)
'however this works, but that is Not what I want.
'		poProcess = TProcess.Create(iDuration, StopIt)		'uncomment this line to see what happens if you run this code.
	End Method

	Method Update()
		If poProcess Then poProcess.Update()
	End Method
	
	Method Stop()
		Print "Process done."
		poProcess = Null
	End Method
End Type


Second example (use a generic type TElement):
SuperStrict

Global o:TControl = TControl.Create()
o.Start(500)

While Not KeyHit(KEY_ESCAPE)
	o.Update()
Wend

End

Type TProcess
	Field piStart%
	Field piDuration%
	Field poControl:TElement
	
	Function Create:TProcess(iDuration%, oControl:TElement)
		Local o:TProcess = New TProcess
		o.piDuration = iDuration
		o.poControl = oControl
		o.piStart = MilliSecs()
		Return o
	End Function

	Method Update()
		Print "Busy"
		If MilliSecs()-piStart>=piDuration Then poControl.Callback()
	End Method

End Type


Type TElement
	Method Callback() Abstract
End Type

Type TControl Extends TElement
	Field poProcess:TProcess

	Function Create:TControl()
		Local o:TControl = New TControl
		Return o
	End Function

	Method Start(iDuration%)
		Print "Start process..."
		poProcess = TProcess.Create(iDuration, Self)
	End Method

	Method Update()
		If poProcess Then poProcess.Update()
	End Method
	
	Method Callback()
		Print "Process done."
		poProcess = Null
	End Method
End Type


This is what I came up with. Please tell me if it's clever or stupid?

Rem
This example shows a way to make an object to call a method within another object without knowing the type of that object.

In this example I have a Process type doing something for a defined period of time.
There is another type called TControl that controls the Process type. This Controller can be of any type.
TProcess knows nothing about the control type identity but is able to tell the object when it's finished.

End Rem
SuperStrict

Local c:TControl = TControl.Create()
c.StartProcess(500) 'run the process for 500 milliseconds

While Not KeyHit(KEY_ESCAPE)
	TElement.UpdateAll()
Wend

End


Type TElement
	Global golElements:TList
	
	Method New() 
		If Not golElements Then golElements = CreateList()
		golElements.AddLast(Self)
	End Method
	
	Method Kill() 
		golElements.Remove(Self)
	End Method

	Method Update() Abstract

   Function UpdateAll() 
		For Local e:TElement = EachIn golElements
         e.Update()
      Next
   End Function
	
End Type


Type TProcess Extends TElement
	Field piStart%
	Field piDuration%
	Field poControl:Object
	Field psCallback$
	
	Function Create:TProcess(iDuration%, oControl:Object, sCallback$)
		Local o:TProcess = New TProcess
		o.piDuration = iDuration
		o.poControl = oControl
		o.psCallback = sCallback
		o.piStart = MilliSecs()
		Return o
	End Function

	Method Update()
		Print "Delta t: " + String(MilliSecs() - piStart)
		If MilliSecs() - piStart >= piDuration Then 'time's up!
			Callback()
		End If
	End Method
	
	'here you find the clever Reflection and Invoke stuff...
	Method Callback()
		Local id:TTypeId = TTypeId.ForObject(poControl)
		Local callback:TMethod=id.FindMethod(psCallback)
		'Tell the Control object to run the method described in psCallback (in this case: poControl.ProcessDone()
		callback.Invoke poControl, Null
		Self.Kill()
	End Method		

End Type


Type TControl Extends TElement
	
	Function Create:TControl()
		Local o:TControl = New TControl
		Return o
	End Function
	
	'this method creates the process instance, make it refer to itself and allocates the ProcessDone() method to it.
	Method StartProcess(iDuration%)
		TProcess.Create(iDuration, Self, "ProcessDone")
	End Method
	
	Method Update()
		'do the updating stuff
	End Method
	
	Method ProcessDone()
		Print "Process done."
	End Method
	
End Type


How about TProcess creating an Event when it is ready and TControl checking for Events? You can either use the Bmax events system or your own system.

Ahhh, events, of course. How could I forget?
I'm going to have a look at that.
Thanks

Add a "Method" to reflection and fake it to direct to the type.function??

A pretty standard way to have a callback function is to supply "userdata" with the function. This userdata is passed back when the function is called. Your userdata could be the object you want to invoke a method of.

I don't get it- callback functions in a type don't work? We use them in Flow.

maybe what you need to do is set the callback *inside* the type too?

@Cygnus: can you show me a working example of how you implemented that in Flow?

I don't see how this could work given BM's current implementation of function pointers. A method just cannot be called without an object to call it on. Having a pointer to a method is meaningless without also having a pointer to a corresponding object that it should use as it's "self". That said, I think there should be a way to invoke a method pointer on a given object directly without using reflection, maybe an ._Invoke(method) method on the root class that handles the stack work, then invokes the method pointer you give it.

Oh, and a slightly cleaner, in my opinion, and probably faster way to implement this would be something like this, using SendMessage():

SuperStrict

Global o:TControl = TControl.Create()
o.Start(500)

While Not KeyHit(KEY_ESCAPE)
	o.Update()
Wend

End


' Generic class for all messages, individual instances are the actual messages. It would
' be more convenient to use integer constants but SendMessage requires two
' objects so it's a bit easier to just create "dummy" objects to serve as the messages.
' Note that you only need to create each instance ONCE, or this system doesn't work -
' the only reason a message means "STOP" is because it is stored in the MSG_STOP
' variable.
Type TMsg
EndType

' Create a new instance for the stop message
Global MSG_STOP:Object = New TMsg

Type TProcess
	Field piStart%
	Field piDuration%
	Field callbackObj:Object
	
	Function Create:TProcess(iDuration%, callbackObj:Object)
		Local o:TProcess = New TProcess
		o.piDuration = iDuration
		o.callbackObj = callbackObj
		o.piStart = MilliSecs()
		Return o
	End Function

	Method Update()
		Print "Busy"
		' Send the stop message to the callback object
		If MilliSecs()-piStart>=piDuration Then callbackObj.SendMessage(MSG_STOP, Null)
	End Method

End Type

Type TControl
	Field poProcess:TProcess

	Function Create:TControl()
		Local o:TControl = New TControl
		Return o
	End Function

	Method Start(iDuration%)
		Print "Start process..."
		poProcess = TProcess.Create(iDuration, Self)
	End Method

	Method Update()
		If poProcess Then poProcess.Update()
	End Method
	
	Method SendMessage:Object(Message:Object, Param:Object)
		' See which message was sent
		Select Message
			Case MSG_STOP
				Print "Process done."
				poProcess = Null
		EndSelect
	End Method
End Type


Note that this works because the root class defines SendMessage() for us (by default it does nothing and returns Null), therefore all classes in BlitzMax have it as a method. You can override it's behavior (as I have done in the example) to handle your own custom messages.

Hey Blueapples, that's a nice solution you've shown there. And a clear example as well. Thanks!

It uses the approach of my second example in my first post, but I was not (yet) aware of the SendMessage() method all objects inherit.

I think I go for this solution, although the Event version Tonyg suggested is nice and clean also. Mmmm, must... make... decision...

Events could work if you do not use an event loop with a select statement to process events - it gets really harry when you need all your objects to get a chance to process events in a loop. Possible (I've done it), but I don't recommend it. You could maybe instead use event hooks (see AddHook, EmitEventHook, and EmitEvent in the documentation).

Here's the thing: event hooks can't be methods either. They're just function pointers. You can use the data field of the TEvent object to store your context object, so it's close, but it still isn't a method. Your "methods" would look like this:

Type ...
Function Callback(id, data:Object, context:Object)
...
EndFunction
Endtype


So that Callback is actually a regular function, and it gets the event data sent to it.

Honestly though, try SendMessage. It's a lot cleaner and does pretty much the same thing as a hook will if you have only one object waiting for the callback/message/event (SendMessage obviously doesn't work if you want several objects to respond to the "callback"). It's there for the sort of thing you're doing.