I don't know what you want to call it: function pointer, manual dynamic dispatch, etc. I'm making a framework with the following object whose handle procedure I want to be changed during runtime by the caller of the framework. (I've removed nearly everything that isn't related to this question)
# framework:
type
Awsm* = ref object of RootObj
handle: proc(self: var Awsm)
The user is to define a subclass of Awsm and one or more handler procedures. The subclass is necessary so that the user has easy access to app-specific variables. The handler procedures will be called by the framework. The user's application determines which handler is active (it's a state machine).
# application:
type
App = ref object of Awsm
foo: int
proc handling2(self: var App)
proc handling1(self: var App) =
self.foo = 1
self.handle = handling2
proc handling2(self: var App) =
self.foo = 2
self.handle = handling1
when isMainModule:
var a = App()
a.handle = handling1
a.handle()
echo a.foo
a.handle()
echo a.foo
The error with this approach is that the compiler doesn't like the sub-class being used for the handler's argument's type. Error: type mismatch: got 'proc (self: var App){.noinline, noSideEffect, gcsafe.}' for 'handling1' but expected 'proc (self: var Awsm){.noinline.}'
I'd prefer the solution to this be type safe, not C-like function pointers. Do I need to study up on concepts or interfaces?
You might consider trying my Sigils library: https://github.com/elcritch/sigils
I made it for this sort of runtime dynamic dispatch scenario. It can also handle multiple handlers (slots) for an event.
In theory it could look something like this:
import sigils
# framework:
type
Awsm* = ref object of Agent
# application:
type
App = ref object of Awsm
foo: int
proc appEvent(self: App) {.signal.}
proc handling1(self: var App) {.slot.} =
self.foo = 1
disconnect(self, appEvent)
connect(self.handling2)
proc handling2(self: var App) {.slot.} =
self.foo = 2
disconnect(self, appEvent)
connect(self.handling1)
when isMainModule:
var a = App()
a.connect(handling1)
emit a.appEvent()
emit a.appEvent()
echo a.foo
emit a.appEvent()
echo a.foo
Hmmm, dang I forgot Sigils doesn't support pre-declaratiins yet. Maybe I should add that today.
IMO closures look like the most sane way to do this. You're probably not having many (thousands+) instances of your app, so creating context for each handle closure won't cause issues.
Another way I see would be with generics
# framework:
type
Awsm* = ref object of RootObj
# no handle here
# application:
type
App = ref object of Awsm
foo: int
handle: proc(self: var App) # move handle here, user has to define it
proc frameworkMainLoop[AppT](app: var AppT) =
...
app.handle()
...
This won't compile unless the user defines all the handles the framework assumes.
Thanks, guys, for bouncing ideas and giving me avenues that weren't obvious to me.
For more context, this is for resource-constrained embedded systems; replicating the QP framework. I want the handler procs (the Awsm-subclasses) to allow >1 instances of the state machine. For example, a single state machine for an MCU's I2C driver, but multiple instances (one for each running I2C peripheral).
@araq's reply has left me with areas to explore. I don't know exactly where to put the proc that holds the context yet. If I want to support multiple instances, I will have to parameterize the instance as an argument to the proc that becomes the context for the closures. (I hope I am grasping that and explaining that correctly)
@elcritch, I'm trying to avoid dependencies, but may reference Sigils for examples. Can you tell me: does Sigils compile away (zero cost)? Or is state information of signal/slot connections accumulated somewhere?
@darkestpidgeon, the framework (library) can't know anything about the shape of the users' App. However your frameworkMainLoop() example is giving me extra ideas to explore.
But it does know some. Here
# framework:
type
Awsm* = ref object of RootObj
handle: proc(self: var Awsm)
you're enforcing the users' App to have the handle field (by making it a part of the parent Awsm type), and you use this knowledge in your framework's code. The proc frameworkMainLoop[AppT](app: var AppT) enforces the same constraint during instantiation, but in a different way.
If you're willing to consider alternative ways to arrange things, how about
# framework
type Awsm*[UserData] = object
...
userData: UserData
proc frameworkMainLoop*[UserData](app: Awsm[UserData]) =
...
handle(app)
...
# application
type AppData = object
foo: int
proc handle(app: var Awsm[AppData]) =
template dt(): untyped = app.userData
echo dt.foo
if dt.foo == 1:
dt.foo = 2
elif dt.foo == 2:
dt.foo = 1
proc main() =
var app = createApp(..., userData=AppData(foo: 1))
frameworkMainLoop(app)
This would be most efficient and (by my taste) most flexible and easiest to use (if going with framework approach at all).
But it does know some.
Yes, you're right. I consider the framework's Handler to be the price of entry for the app to use the framework. So I was skipping right past that. What I meant was that the framework should not know anything about the number of handler functions or how they transition; nor should the framework know anything about Awsm subclasses. In fact, it is my goal that the framework only ever dispatches events to the Awsm's handle proc.
Here's a little progress. Still not technically sound. But headed in a working direction.
# framework:
type
Awsm* = ref object of RootObj
handle: proc() {.closure.}
# application:
type
App = ref object of Awsm
foo: int
var instances: seq[App]
proc spawnApp(): Natural =
result = instances.len
instances.add App()
proc dispatch(instance: Natural, evnt: Natural) =
var self = instances[instance]
proc handling2()
proc handling1() =
self.foo = 1
self.handle = handling2
proc handling2() =
self.foo = 2
self.handle = handling1
proc init() =
self.foo = 0
self.handle = handling1
case evnt:
of 0:
init()
else:
self.handle()
echo self.foo
proc main =
let n = spawnApp()
dispatch(n, 0)
dispatch(n, 9)
dispatch(n, 9)
dispatch(n, 9)
when isMainModule:
main()
I have a way to create >1 instance. It's not fully developed. I may have to get sugar.capture involved. Can you tell me: does Sigils compile away (zero cost)? Or is state information of signal/slot connections accumulated somewhere?
Nope, Sigil is built for runtime OOP so it has to have some state. Depending on your context it might be a lot, probably 3-4x a that of a simple pointer. It's essentially just a table of names to function pointers.
If you want to avoid closure overhead you could do this, which compiles and runs:
# framework:
type
Awsm* = ref object of RootObj
handler: proc(self: Awsm) {.nimcall.}
proc handle(self: Awsm) =
## helper
self.handler(self)
template adapter[T](handler: proc(self: T) {.nimcall.} ): proc(self: Awsm) {.nimcall.} =
## this converts the subtype for you
proc(self: Awsm) {.nimcall.} =
var app = T(self)
handler(app)
# application:
type
App = ref object of Awsm
foo: int
proc handling2(self: App)
proc handling1(self: App) =
self.foo = 1
self.handler = adapter(handling2)
proc handling2(self: App) =
self.foo = 2
self.handler = adapter(handling1)
when isMainModule:
var a = App()
a.handler = adapter(handling1)
a.handle()
echo a.foo
a.handle()
echo a.foo
Any idea why adding var to the arg in handle's signature introduces a type mismatch when without the var there is no mismatch?
proc handle(self: var Awsm)