diff --git a/examples/echo/cpp_bindings/echo.hpp b/examples/echo/cpp_bindings/echo.hpp index 3064fde..1c26987 100644 --- a/examples/echo/cpp_bindings/echo.hpp +++ b/examples/echo/cpp_bindings/echo.hpp @@ -407,7 +407,7 @@ void* echo_create_cbor(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback int echo_shout_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); int echo_version_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); int echo_destroy(void* ctx); -uint64_t echo_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data); +uint64_t echo_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); int echo_remove_event_listener(void* ctx, uint64_t listener_id); } // extern "C" diff --git a/examples/timer/c_bindings/my_timer_cbor.h b/examples/timer/c_bindings/my_timer_cbor.h index 04e8c93..dba62ed 100644 --- a/examples/timer/c_bindings/my_timer_cbor.h +++ b/examples/timer/c_bindings/my_timer_cbor.h @@ -168,7 +168,7 @@ int my_timer_schedule_cbor(void *ctx, FFICallBack callback, void *userData, cons int my_timer_destroy(void *ctx); -uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData); +uint64_t my_timer_add_event_listener_cbor(void *ctx, const char *eventName, FFICallBack callback, void *userData); int my_timer_remove_event_listener(void *ctx, uint64_t listenerId); #ifdef __cplusplus diff --git a/examples/timer/cpp_bindings/my_timer.hpp b/examples/timer/cpp_bindings/my_timer.hpp index f2aba67..8a0591d 100644 --- a/examples/timer/cpp_bindings/my_timer.hpp +++ b/examples/timer/cpp_bindings/my_timer.hpp @@ -708,7 +708,7 @@ int my_timer_version_cbor(void* ctx, FFICallback callback, void* user_data, cons int my_timer_complex_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); int my_timer_schedule_cbor(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len); int my_timer_destroy(void* ctx); -uint64_t my_timer_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data); +uint64_t my_timer_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data); int my_timer_remove_event_listener(void* ctx, uint64_t listener_id); } // extern "C" @@ -846,7 +846,7 @@ public: ListenerHandle addOnEchoFiredListener(std::function handler) { auto owned = std::make_unique>(std::move(handler)); auto* raw = owned.get(); - const auto id = my_timer_add_event_listener( + const auto id = my_timer_add_event_listener_cbor( ptr_, "on_echo_fired", &MyTimerCtx::typedTrampoline, raw); if (id == 0) return ListenerHandle{0}; listeners_.emplace(id, std::move(owned)); @@ -856,7 +856,7 @@ public: ListenerHandle addEventListener(std::function)> handler) { auto owned = std::make_unique(std::move(handler)); auto* raw = owned.get(); - const auto id = my_timer_add_event_listener( + const auto id = my_timer_add_event_listener_cbor( ptr_, "", &MyTimerCtx::wildcardTrampoline, raw); if (id == 0) return ListenerHandle{0}; listeners_.emplace(id, std::move(owned)); diff --git a/examples/timer/go_bindings/my_timer.go b/examples/timer/go_bindings/my_timer.go index 41ae0b9..25ba745 100644 --- a/examples/timer/go_bindings/my_timer.go +++ b/examples/timer/go_bindings/my_timer.go @@ -10,6 +10,7 @@ package my_timer #include extern void my_timerGoEvent(int ret, char* msg, size_t len, void* userData); +extern uint64_t my_timer_add_event_listener_cbor(void* ctx, const char* eventName, FFICallBack callback, void* userData); 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); @@ -60,7 +61,7 @@ static int my_timerCall_my_timer_version(void* ctx, My_timerResp* r) { return rc; } static int my_timerCall_my_timer_destroy(void* ctx) { return my_timer_destroy(ctx); } -static uint64_t my_timerRegisterEvents(void* ctx) { return my_timer_add_event_listener(ctx, "", (FFICallBack)my_timerGoEvent, ctx); } +static uint64_t my_timerRegisterEvents(void* ctx) { return my_timer_add_event_listener_cbor(ctx, "", (FFICallBack)my_timerGoEvent, ctx); } */ import "C" diff --git a/examples/timer/rust_bindings/src/api.rs b/examples/timer/rust_bindings/src/api.rs index 2afa582..6e025f6 100644 --- a/examples/timer/rust_bindings/src/api.rs +++ b/examples/timer/rust_bindings/src/api.rs @@ -219,7 +219,7 @@ impl MyTimerCtx { owned: Box, ) -> ListenerHandle { let id = unsafe { - ffi::my_timer_add_event_listener(self.ptr, event_name, callback, raw) + ffi::my_timer_add_event_listener_cbor(self.ptr, event_name, callback, raw) }; if id != 0 { self.listeners.lock().unwrap().insert(id, owned); diff --git a/examples/timer/rust_bindings/src/ffi.rs b/examples/timer/rust_bindings/src/ffi.rs index ae893fc..fa889fb 100644 --- a/examples/timer/rust_bindings/src/ffi.rs +++ b/examples/timer/rust_bindings/src/ffi.rs @@ -15,6 +15,6 @@ extern "C" { pub fn my_timer_complex_cbor(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; pub fn my_timer_schedule_cbor(ctx: *mut c_void, callback: FFICallback, user_data: *mut c_void, req_cbor: *const u8, req_cbor_len: usize) -> c_int; pub fn my_timer_destroy(ctx: *mut c_void) -> c_int; - pub fn my_timer_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64; + pub fn my_timer_add_event_listener_cbor(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64; pub fn my_timer_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int; } diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index f4010c8..369b5d1 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -166,7 +166,9 @@ proc generateCHeader*( ) lines.add("") - # `declareLibrary` always exports the listener-registration ABI. + # `declareLibrary` always exports the listener-registration ABI. The native + # header advertises the native listener: the callback's msg is a typed + # `const *` (cast it), not CBOR. lines.add( "uint64_t " & libName & "_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);" @@ -378,7 +380,7 @@ proc generateCborCHeader*( lines.add( "uint64_t " & libName & - "_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);" + "_add_event_listener_cbor(void *ctx, const char *eventName, FFICallBack callback, void *userData);" ) lines.add( "int " & libName & "_remove_event_listener(void *ctx, uint64_t listenerId);" diff --git a/ffi/codegen/cpp.nim b/ffi/codegen/cpp.nim index f045e61..a2daa78 100644 --- a/ffi/codegen/cpp.nim +++ b/ffi/codegen/cpp.nim @@ -178,7 +178,7 @@ proc emitEventDispatcher( ) lines.add(" auto* raw = owned.get();") lines.add( - " const auto id = $1_add_event_listener(" % [libName] + " const auto id = $1_add_event_listener_cbor(" % [libName] ) lines.add( " ptr_, \"$1\", &$2::typedTrampoline<$3>, raw);" % @@ -204,7 +204,7 @@ proc emitEventDispatcher( " auto owned = std::make_unique(std::move(handler));" ) lines.add(" auto* raw = owned.get();") - lines.add(" const auto id = $1_add_event_listener(" % [libName]) + lines.add(" const auto id = $1_add_event_listener_cbor(" % [libName]) lines.add( " ptr_, \"\", &$1::wildcardTrampoline, raw);" % [ctxTypeName] ) @@ -429,7 +429,7 @@ proc generateCppHeader*( # `declareLibrary` always exports the listener-registration ABI. Declare # it here so the typed event-handler wiring below can call into it. lines.add( - "uint64_t $1_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);" % + "uint64_t $1_add_event_listener_cbor(void* ctx, const char* event_name, FFICallback callback, void* user_data);" % [libName] ) lines.add( diff --git a/ffi/codegen/go.nim b/ffi/codegen/go.nim index 2234329..789da35 100644 --- a/ffi/codegen/go.nim +++ b/ffi/codegen/go.nim @@ -370,6 +370,12 @@ proc generateGoFile*( L.add( "extern void " & libName & "GoEvent(int ret, char* msg, size_t len, void* userData);" ) + # The Go wrapper consumes events over CBOR; the native header only declares the + # native listener, so forward-declare the CBOR registration we call below. + L.add( + "extern uint64_t " & libName & + "_add_event_listener_cbor(void* ctx, const char* eventName, FFICallBack callback, void* userData);" + ) # One exported Go result callback per struct-returning proc (it reads the typed # return POD in-callback). Forward-declared here so cgo's `char*` shape matches. for p in procs: @@ -491,7 +497,7 @@ proc generateGoFile*( ) L.add( "static uint64_t " & libName & "RegisterEvents(void* ctx) { return " & libName & - "_add_event_listener(ctx, \"\", (FFICallBack)" & libName & "GoEvent, ctx); }" + "_add_event_listener_cbor(ctx, \"\", (FFICallBack)" & libName & "GoEvent, ctx); }" ) L.add("*/") L.add("import \"C\"") diff --git a/ffi/codegen/rust.nim b/ffi/codegen/rust.nim index b08d0be..b5de184 100644 --- a/ffi/codegen/rust.nim +++ b/ffi/codegen/rust.nim @@ -213,7 +213,7 @@ proc generateFFIRs*(procs: seq[FFIProcMeta]): string = # Listener-registration ABI — emitted on the Nim side by `declareLibrary`, # always present in the dylib. lines.add( - " pub fn $1_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;" % + " pub fn $1_add_event_listener_cbor(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;" % [linkLibName] ) lines.add( @@ -709,7 +709,7 @@ proc generateApiRs*( lines.add(" ) -> ListenerHandle {") lines.add(" let id = unsafe {") lines.add( - " ffi::$1_add_event_listener(self.ptr, event_name, callback, raw)" % + " ffi::$1_add_event_listener_cbor(self.ptr, event_name, callback, raw)" % [libName] ) lines.add(" };") diff --git a/ffi/ffi_events.nim b/ffi/ffi_events.nim index 09cfa0f..d6c65d2 100644 --- a/ffi/ffi_events.nim +++ b/ffi/ffi_events.nim @@ -41,6 +41,10 @@ type id*: uint64 callback*: FFICallBack userData*: pointer + native*: bool + ## true -> deliver the payload as a typed `Pod` by pointer (zero + ## serialization, same-process). false -> deliver the CBOR + ## `EventEnvelope` bytes (inter-process). FFIEventRegistry* = object ## Per-context multi-listener registry. `lock` guards every mutation; @@ -88,11 +92,13 @@ proc addEventListener*( eventName: string, callback: FFICallBack, userData: pointer, + native = false, ): uint64 {.raises: [].} = ## Registers `callback` for `eventName` and returns the listener's stable ## id (always non-zero on success). `eventName == ""` registers a wildcard - ## listener that receives every dispatched event. Returns 0 if `callback` - ## is nil — the only documented failure mode. + ## listener that receives every dispatched event. `native` selects the + ## payload form delivered on dispatch (typed `Pod` pointer vs CBOR bytes). + ## Returns 0 if `callback` is nil — the only documented failure mode. if callback.isNil(): return 0 @@ -101,8 +107,9 @@ proc addEventListener*( withLock reg.lock: reg.nextId.inc() assigned = reg.nextId - let listener = - FFIEventListener(id: assigned, callback: callback, userData: userData) + let listener = FFIEventListener( + id: assigned, callback: callback, userData: userData, native: native + ) if eventName.len == 0: reg.wildcard.add(listener) else: @@ -251,3 +258,45 @@ template dispatchFFIEventCbor*(eventName: string, eventPayload: typed) = listener.callback( RET_OK, cast[ptr cchar](data), cast[csize_t](dataLen), listener.userData ) + +template dispatchFFIEventDual*(eventName: string, eventPayload: typed) = + ## Dual-ABI event dispatch used by `{.ffiEvent.}` procs — the event-side + ## mirror of the native/CBOR request split. Native listeners get the payload + ## as a typed `Pod` by pointer (zero serialization, valid only for the + ## callback's lifetime); CBOR listeners get the `EventEnvelope` bytes. Each + ## form is built at most once per dispatch and fanned out to its listeners. + ## + ## `nimToPod`/`freePod` are the per-type POD machinery generated next to each + ## `{.ffi.}` event type; `mixin` resolves them in the dispatching module. + mixin nimToPod, freePod + withFFIEventDispatch(eventName, listeners): + var nativeBuilt = false + var nativePod: typeof(nimToPod(eventPayload)) + var cborBuilt = false + var cborData: ptr UncheckedArray[byte] + var cborLen: int + defer: + if nativeBuilt: + freePod(nativePod) + if cborBuilt: + cborFreeShared(cborData) + for listener in listeners: + if listener.native: + if not nativeBuilt: + nativePod = nimToPod(eventPayload) + nativeBuilt = true + listener.callback( + RET_OK, cast[ptr cchar](addr nativePod), sizeof(nativePod).csize_t, + listener.userData, + ) + else: + if not cborBuilt: + (cborData, cborLen) = cborEncodeShared( + EventEnvelope[typeof(eventPayload)]( + eventType: eventName, payload: eventPayload + ) + ) + cborBuilt = true + listener.callback( + RET_OK, cast[ptr cchar](cborData), cborLen.csize_t, listener.userData + ) diff --git a/ffi/internal/ffi_library.nim b/ffi/internal/ffi_library.nim index 0fcd26c..c80f4d9 100644 --- a/ffi/internal/ffi_library.nim +++ b/ffi/internal/ffi_library.nim @@ -134,7 +134,21 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped = newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), ) - # {libraryName}_add_event_listener + # Event registration mirrors the request naming convention: the bare + # `_add_event_listener` is the NATIVE (typed `Pod` payload) entry + # point; `_add_event_listener_cbor` is the CBOR (EventEnvelope bytes) one. + # Both are exported — native for same-process consumers, CBOR for inter-process. + # Fresh param nodes per proc — AST nodes must not be shared across two procs. + proc evtParams(): seq[NimNode] = + @[ + ident("uint64"), + newIdentDefs(ident("ctx"), ctxType.copyNimTree()), + newIdentDefs(ident("eventName"), ident("cstring")), + newIdentDefs(ident("callback"), ident("FFICallBack")), + newIdentDefs(ident("userData"), ident("pointer")), + ] + + # {libraryName}_add_event_listener (native) let addName = libraryName & "_add_event_listener" let addErr = "error: invalid context in " & addName let addBody = quote: @@ -143,20 +157,33 @@ macro declareLibrary*(libraryName: static[string], libType: untyped): untyped = echo `addErr` return ret let evtName = if eventName.isNil(): "" else: $eventName + ret = addEventListener( + ctx[].eventRegistry, evtName, callback, userData, native = true + ) + return ret + + stmts.add( + newProc( + name = ident(addName), params = evtParams(), body = addBody, + pragmas = cdeclExportPragma, + ) + ) + + # {libraryName}_add_event_listener_cbor (CBOR / inter-process) + let addCborName = libraryName & "_add_event_listener_cbor" + let addCborErr = "error: invalid context in " & addCborName + let addCborBody = quote: + var ret: uint64 = 0 + if isNil(ctx): + echo `addCborErr` + return ret + let evtName = if eventName.isNil(): "" else: $eventName ret = addEventListener(ctx[].eventRegistry, evtName, callback, userData) return ret stmts.add( newProc( - name = ident(addName), - params = @[ - ident("uint64"), - newIdentDefs(ident("ctx"), ctxType), - newIdentDefs(ident("eventName"), ident("cstring")), - newIdentDefs(ident("callback"), ident("FFICallBack")), - newIdentDefs(ident("userData"), ident("pointer")), - ], - body = addBody, + name = ident(addCborName), params = evtParams(), body = addCborBody, pragmas = cdeclExportPragma, ) ) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 5179bf7..576a35a 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -1887,10 +1887,11 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped = if procName.kind == nnkPostfix: userProcName = procName[1] - # The generated body: dispatchFFIEventCbor("wire_name", payload). + # The generated body: dispatchFFIEventDual("wire_name", payload) — delivers a + # typed POD to native listeners and CBOR bytes to CBOR listeners. let wireNameLit = newStrLitNode(wireName) let dispatchBody = - newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, payloadParamName)) + newStmtList(newCall(ident("dispatchFFIEventDual"), wireNameLit, payloadParamName)) var newParams = newSeq[NimNode]() newParams.add(formalParams[0]) # return type (typically empty/void) diff --git a/tests/unit/test_native_events.nim b/tests/unit/test_native_events.nim new file mode 100644 index 0000000..ff34923 --- /dev/null +++ b/tests/unit/test_native_events.nim @@ -0,0 +1,165 @@ +## Native (zero-serialization) event delivery: a `{.ffiEvent.}` now fans out to +## both kinds of listener at once — native listeners receive the payload as a +## typed `Pod` by pointer, CBOR listeners receive the `EventEnvelope` bytes. +## Mirrors the native/CBOR split already in place for requests. +## +## The CBOR side is covered more broadly in test_event_dispatch; here we pin the +## *native* path and that both fire from one dispatch. Runs under orc + refc. + +import std/[locks] +import unittest2 +import results +import ffi + +type EvtLib = object + +type NativeEvt {.ffi.} = object + message: string + count: int + tags: seq[string] + ok: bool + +proc onNativeEvt(evt: NativeEvt) {.ffiEvent: "native_evt".} + +proc sampleEvt(): NativeEvt = + NativeEvt(message: "hello", count: 7, tags: @["a", "", "ccc"], ok: true) + +# A request that fires the event from the FFI thread, then returns. +registerReqFFI(EmitEvtRequest, lib: ptr EvtLib): + proc(): Future[Result[string, string]] {.async.} = + onNativeEvt(sampleEvt()) + return ok("emitted") + +# --- native listener capture (clone the typed POD off the FFI thread) ------- +type NativeCap = object + lock: Lock + cond: Cond + done: bool + ret: cint + pod: NativeEvtPod + hasPod: bool + +proc initNativeCap(c: var NativeCap) = + c.lock.initLock() + c.cond.initCond() +proc deinitNativeCap(c: var NativeCap) = + c.cond.deinitCond() + c.lock.deinitLock() +proc waitNativeCap(c: var NativeCap) = + acquire(c.lock) + while not c.done: + wait(c.cond, c.lock) + release(c.lock) + +proc nativeEvtCb( + ret: cint, msg: ptr cchar, len: csize_t, ud: pointer +) {.cdecl, gcsafe, raises: [].} = + let c = cast[ptr NativeCap](ud) + acquire(c[].lock) + c[].ret = ret + if ret == RET_OK and not msg.isNil: + # Native ABI: msg is a `ptr NativeEvtPod`; deep-copy it off the FFI thread. + c[].pod = clonePod(cast[ptr NativeEvtPod](msg)[]) + c[].hasPod = true + c[].done = true + signal(c[].cond) + release(c[].lock) + +# --- CBOR listener capture -------------------------------------------------- +type CborCap = object + lock: Lock + cond: Cond + done: bool + ret: cint + msg: array[2048, byte] + msgLen: int + +proc initCborCap(c: var CborCap) = + c.lock.initLock() + c.cond.initCond() +proc deinitCborCap(c: var CborCap) = + c.cond.deinitCond() + c.lock.deinitLock() +proc waitCborCap(c: var CborCap) = + acquire(c.lock) + while not c.done: + wait(c.cond, c.lock) + release(c.lock) +proc bytes(c: var CborCap): seq[byte] = + var b = newSeq[byte](c.msgLen) + if c.msgLen > 0: + copyMem(addr b[0], addr c.msg[0], c.msgLen) + return b + +proc cborEvtCb( + ret: cint, msg: ptr cchar, len: csize_t, ud: pointer +) {.cdecl, gcsafe, raises: [].} = + let c = cast[ptr CborCap](ud) + acquire(c[].lock) + c[].ret = ret + let n = min(int(len), c[].msg.len) + if n > 0 and not msg.isNil: + copyMem(addr c[].msg[0], msg, n) + c[].msgLen = n + c[].done = true + signal(c[].cond) + release(c[].lock) + +# A throwaway response callback to know the request (and thus the dispatch) ran. +proc rspCb( + ret: cint, msg: ptr cchar, len: csize_t, ud: pointer +) {.cdecl, gcsafe, raises: [].} = + let c = cast[ptr CborCap](ud) + acquire(c[].lock) + c[].done = true + signal(c[].cond) + release(c[].lock) + +suite "native event delivery": + test "one dispatch delivers a typed POD to native + CBOR to cbor listeners": + var pool: FFIContextPool[EvtLib] + let ctx = pool.createFFIContext().valueOr: + check false + return + defer: + discard pool.destroyFFIContext(ctx) + + var ncap: NativeCap + initNativeCap(ncap) + defer: + deinitNativeCap(ncap) + var ccap: CborCap + initCborCap(ccap) + defer: + deinitCborCap(ccap) + + # Same event, two listeners of different formats. + check addEventListener( + ctx[].eventRegistry, "native_evt", nativeEvtCb, addr ncap, native = true + ) != 0'u64 + check addEventListener( + ctx[].eventRegistry, "native_evt", cborEvtCb, addr ccap, native = false + ) != 0'u64 + + var rsp: CborCap + initCborCap(rsp) + defer: + deinitCborCap(rsp) + check sendRequestToFFIThread(ctx, EmitEvtRequest.ffiNewReq(rspCb, addr rsp)).isOk() + waitCborCap(rsp) + waitNativeCap(ncap) + waitCborCap(ccap) + + # Native listener: the typed POD round-trips to the original value. + check ncap.ret == RET_OK + check ncap.hasPod + let got = podToNim(ncap.pod) + freePod(ncap.pod) + check got == sampleEvt() + + # CBOR listener: the EventEnvelope decodes to the same value. + check ccap.ret == RET_OK + let decoded = cborDecode(bytes(ccap), EventEnvelope[NativeEvt]) + check decoded.isOk() + check decoded.value.eventType == "native_evt" + check decoded.value.payload == sampleEvt()