mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-27 01:53:26 +00:00
feat(ffi): native event delivery + dual-ABI event symbol naming
Events now mirror the native/CBOR split already in place for requests, with the
same symbol-naming convention:
- `<lib>_add_event_listener` -> NATIVE listener (typed `<T>Pod` by pointer)
- `<lib>_add_event_listener_cbor` -> CBOR listener (EventEnvelope bytes)
Framework: `FFIEventListener` gains a `native` flag; `addEventListener` a
`native` param; a new `dispatchFFIEventDual` template builds the `<T>Pod` once
for native listeners (`nimToPod`/`freePod`) and the CBOR envelope once for the
rest, fanning each out — so a single `{.ffiEvent.}` dispatch serves both kinds.
`declareLibrary` exports both registration entry points.
Generators: the bare `<lib>_add_event_listener` is the native symbol; every
CBOR consumer (C/C++/Go/Rust) now targets `<lib>_add_event_listener_cbor`. The
rename and the generator updates ship together so the bare name is never briefly
broken. Bindings regenerated.
Validated: native-event unit test (typed POD to native + CBOR to cbor listener,
orc/refc/ASAN); full unit suite; C++ e2e 19/19; Go example; existing event
tests unchanged. The per-event *typed* native callback + wildcard router (the
ergonomic consumer surface) is a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c000a8467d
commit
918dd72390
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<void(const EchoEvent&)> handler) {
|
||||
auto owned = std::make_unique<TypedListener<EchoEvent>>(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<EchoEvent>, raw);
|
||||
if (id == 0) return ListenerHandle{0};
|
||||
listeners_.emplace(id, std::move(owned));
|
||||
@ -856,7 +856,7 @@ public:
|
||||
ListenerHandle addEventListener(std::function<void(int, const std::string&, std::span<const std::uint8_t>)> handler) {
|
||||
auto owned = std::make_unique<WildcardListener>(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));
|
||||
|
||||
@ -10,6 +10,7 @@ package my_timer
|
||||
#include <pthread.h>
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@ -219,7 +219,7 @@ impl MyTimerCtx {
|
||||
owned: Box<dyn std::any::Any + Send>,
|
||||
) -> 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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 <Event>*` (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);"
|
||||
|
||||
@ -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<WildcardListener>(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(
|
||||
|
||||
@ -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\"")
|
||||
|
||||
@ -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(" };")
|
||||
|
||||
@ -41,6 +41,10 @@ type
|
||||
id*: uint64
|
||||
callback*: FFICallBack
|
||||
userData*: pointer
|
||||
native*: bool
|
||||
## true -> deliver the payload as a typed `<T>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 `<T>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 `<T>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
|
||||
)
|
||||
|
||||
@ -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
|
||||
# `<lib>_add_event_listener` is the NATIVE (typed `<T>Pod` payload) entry
|
||||
# point; `<lib>_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,
|
||||
)
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
165
tests/unit/test_native_events.nim
Normal file
165
tests/unit/test_native_events.nim
Normal file
@ -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 `<T>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()
|
||||
Loading…
x
Reference in New Issue
Block a user