feat(codegen): Go per-event typed native event handlers

Adds the ergonomic native event surface for Go: `node.On<Event>(func(<Payload>))`
registers a native listener for that event and the library delivers the typed
`<Payload>` POD, which an exported Go callback reads into a Go struct (reusing
the generated `fromC`) and hands to the user's handler — no CBOR parsing. Sits
beside the existing CBOR `SetEventHandler` (wildcard / inter-process).

The example registers `OnEchoFired` and receives a typed `EchoEvent` when Echo
fires it. Verified end-to-end with `go run -race`.

C/C++/Rust get the same per-event typed handler + router next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 17:51:23 +02:00
parent 918dd72390
commit ceccc7bef3
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
3 changed files with 83 additions and 0 deletions

View File

@ -31,12 +31,23 @@ func main() {
fmt.Printf("version: %s\n", v)
}
// Native typed event: Echo fires onEchoFired(EchoEvent) inside the library.
// Register a typed handler — the payload arrives as a Go struct, no CBOR.
events := make(chan timer.EchoEvent, 4)
node.OnEchoFired(func(e timer.EchoEvent) { events <- e })
// Struct param + typed struct return: EchoResponse { Echoed; TimerName }.
if resp, err := node.Echo(timer.EchoRequest{Message: "hello from Go", DelayMs: 5}); err != nil {
log.Printf("echo: %v", err)
} else {
fmt.Printf("echo: echoed=%q timerName=%q\n", resp.Echoed, resp.TimerName)
}
select {
case e := <-events:
fmt.Printf("event OnEchoFired: message=%q echoCount=%d\n", e.Message, e.EchoCount)
default:
fmt.Println("event OnEchoFired: (none received)")
}
// Deeply nested param + typed return: slice of structs, slice of strings,
// two optionals in; ComplexResponse { Summary; ItemCount; HasNote } out.

View File

@ -14,6 +14,7 @@ extern uint64_t my_timer_add_event_listener_cbor(void* ctx, const char* eventNam
extern void my_timerResultEcho(int ret, char* msg, size_t len, void* ud);
extern void my_timerResultComplex(int ret, char* msg, size_t len, void* ud);
extern void my_timerResultSchedule(int ret, char* msg, size_t len, void* ud);
extern void my_timerEvtOnEchoFired(int ret, char* msg, size_t len, void* ud);
typedef struct {
int ret; char* msg; size_t len; int done;
@ -474,6 +475,28 @@ func my_timerGoEvent(ret C.int, msg *C.char, length C.size_t, userData unsafe.Po
}
}
var evtOnEchoFiredHandler func(EchoEvent)
// OnEchoFired installs the native typed handler for the "on_echo_fired" event.
func (n *My_timerNode) OnEchoFired(h func(EchoEvent)) {
eventMu.Lock()
evtOnEchoFiredHandler = h
eventMu.Unlock()
cn := C.CString("on_echo_fired")
defer C.free(unsafe.Pointer(cn))
C.my_timer_add_event_listener(n.ctx, cn, C.FFICallBack(C.my_timerEvtOnEchoFired), n.ctx)
}
//export my_timerEvtOnEchoFired
func my_timerEvtOnEchoFired(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) {
eventMu.Lock()
h := evtOnEchoFiredHandler
eventMu.Unlock()
if h != nil && ret == C.RET_OK {
h(EchoEventFromC((*C.EchoEvent)(unsafe.Pointer(msg))))
}
}
func NewMy_timer(config TimerConfig) (*My_timerNode, error) {
c_config, free_config := config.toC()
defer func() {

View File

@ -385,6 +385,14 @@ proc generateGoFile*(
"extern void " & libName & "Result" & methodName(p.procName, libName) &
"(int ret, char* msg, size_t len, void* ud);"
)
# One exported Go callback per event (native typed delivery): msg is a typed
# `const <Payload>*` that the callback reads into a Go value.
for e in events:
if isFFIStruct(e.payloadTypeName, types):
L.add(
"extern void " & libName & "Evt" & snakeToPascalCase(e.wireName) &
"(int ret, char* msg, size_t len, void* ud);"
)
L.add("")
L.add("typedef struct {")
L.add(" int ret; char* msg; size_t len; int done;")
@ -565,6 +573,47 @@ proc generateGoFile*(
L.add("}")
L.add("")
# ---- per-event NATIVE typed handlers -------------------------------------
# `On<Event>(h)` registers a native listener; the library delivers the typed
# `<Payload>` POD, which the exported callback reads into a Go value and hands
# to `h`. No CBOR — this is the same-process path (cf. SetEventHandler).
for e in events:
if not isFFIStruct(e.payloadTypeName, types):
continue
let pascal = snakeToPascalCase(e.wireName)
let goType = e.payloadTypeName
let handlerVar = "evt" & pascal & "Handler"
let cbName = libName & "Evt" & pascal
L.add("var " & handlerVar & " func(" & goType & ")")
L.add("")
L.add("// " & pascal & " installs the native typed handler for the \"" &
e.wireName & "\" event.")
L.add("func (n *" & nodeType & ") " & pascal & "(h func(" & goType & ")) {")
L.add("\teventMu.Lock()")
L.add("\t" & handlerVar & " = h")
L.add("\teventMu.Unlock()")
L.add("\tcn := C.CString(\"" & e.wireName & "\")")
L.add("\tdefer C.free(unsafe.Pointer(cn))")
L.add(
"\tC." & libName & "_add_event_listener(n.ctx, cn, C.FFICallBack(C." & cbName &
"), n.ctx)"
)
L.add("}")
L.add("")
L.add("//export " & cbName)
L.add(
"func " & cbName &
"(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) {"
)
L.add("\teventMu.Lock()")
L.add("\th := " & handlerVar)
L.add("\teventMu.Unlock()")
L.add("\tif h != nil && ret == C.RET_OK {")
L.add("\t\th(" & goType & "FromC((*C." & goType & ")(unsafe.Pointer(msg))))")
L.add("\t}")
L.add("}")
L.add("")
# ---- constructor ---------------------------------------------------------
if haveCtor:
let (goParams, conv, callArgs) = goParamConv(ctor.extraParams, types)