Ah, I misunderstood what you were trying to do. @Hlaaftana is correct, in order for type safety to remain, Nim needs to know what types you want to include in the event registry at compile time. I took a very brief stab at a rough version using macros (that can certainly be improved), and here's what I came up with: import tables, strutils import macros macro registerEvents(body: untyped): untyped = template createTriggerProc() = proc disconnect(reg: Registry, name: string) = for evObj in reg[].fields: evObj.connections.del name template createEventProcs(fieldName, tyName) = proc connect(reg: Registry, callback: proc (arg: tyName), name: string) = reg.fieldName.connections[name] = callback proc trigger(reg: Registry, val: tyName) = for connection in reg.fieldName.connections.values: connection(val) result = newNimNode(nnkStmtList) var regRecList = nnkRecList.newNimNode var registryTypeDef = nnkTypeDef.newTree( ident("Registry"), newEmptyNode(), nnkRefTy.newTree( nnkObjectTy.newTree( newEmptyNode(), newEmptyNode(), regRecList ) ) ) var typeSection = nnkTypeSection.newNimNode result.add typeSection for node in body: let signalName = ident($node.toStrLit & "Signal") let tyName = ident($node.toStrLit) let lowerTyName = ident(($node.toStrLit).toLowerAscii) result.add getAst(createEventProcs(lowerTyName, tyName)) regRecList.add nnkIdentDefs.newTree( lowerTyName, signalName, newEmptyNode() ) let procTy = nnkProcTy.newTree( nnkFormalParams.newTree( newEmptyNode(), nnkIdentDefs.newTree( ident("arg"), tyName, newEmptyNode() ) ), newEmptyNode() ) typeSection.add nnkTypeDef.newTree( signalName, newEmptyNode(), nnkObjectTy.newTree( newEmptyNode(), newEmptyNode(), nnkRecList.newTree( nnkIdentDefs.newTree( ident("connections"), nnkBracketExpr.newTree( ident("Table"), ident("string"), procTy ), newEmptyNode() ) ) ) ) typeSection.add registryTypeDef result.add getAst(createTriggerProc()) type MyEvent = object hihi: string hoho: int SomeOtherEvent = object ents: seq[int] # This is all you need to do for each type you want to be in the registry, but can only be called once # Can be improved to take a name for the Registry type registerEvents: MyEvent # MyEvent2 # These macros currently need to be defined where ever you call `registerEvents`, otherwise you # must specify the names of the events manually macro connect(reg: Registry, callback: untyped): untyped = let name = callback.toStrLit let regName = reg.toStrLit template doConnect(reg, regName, callback, name) = when compiles(connect(reg, callback, name)): connect(reg, callback, name) else: {.error: "'" & name & "' event not found in registry '" & regName & "'".} return getAst(doConnect(reg, regName, callback, name)) macro disconnect(reg: Registry, callback: untyped): untyped = let name = callback.toStrLit template doDisconnect(reg, name) = disconnect(reg, name) return getAst(doDisconnect(reg, name)) var reg = Registry() var obj = "From bound obj: asdads" # A bound obj proc cbMyEvent(ev: MyEvent) = echo "my event was triggered", ev.hihi, ev.hoho echo obj proc cbMyOtherEvent(ev: SomeOtherEvent) = echo "my event was triggered", ev.ents echo obj proc cbMyEvent2(ev: MyEvent) = echo "my event was triggered TWO", ev.hihi, ev.hoho echo obj reg.connect(cbMyEvent) # these are good reg.connect(cbMyEvent2) # these are good reg.disconnect(cbMyEvent2) # these are good reg.connect(cbMyOtherEvent) # this now fails at compile time with "Error: 'cbMyOtherEvent' event not found in registry 'reg'" var myev = MyEvent() myev.hihi = "hihi" myev.hoho = 1337 trigger(reg, myev) myev.hihi = "HAHAHAH" # change the event a little reg.trigger(myev) var sev = SomeOtherEvent() sev.ents = @[1,2,3] trigger(reg, sev) # Breaks at compile time Run
The `registerEvents` macro invocation above creates this equivalent code (which can be printed using `echo result.toStrLit`): type MyEventSignal = object connections: Table[string, proc (arg: MyEvent)] Registry = ref object myevent: MyEventSignal proc connect(reg: Registry; callback: proc (arg: MyEvent); name: string) = reg.myevent.connections[name] = callback proc trigger(reg: Registry; val: MyEvent) = for connection in reg.myevent.connections.values: connection(val) proc disconnect(reg: Registry; name: string) = for evObj in reg[].fields: evObj.connections.del name Run