Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/echo/cpp_bindings/echo.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 2 additions & 0 deletions examples/timer/c_bindings/my_timer.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec j

int my_timer_destroy(void *ctx);

// Native event payloads — cast the callback's msg accordingly:
// "on_echo_fired" -> const EchoEvent *
uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);
int my_timer_remove_event_listener(void *ctx, uint64_t listenerId);

Expand Down
2 changes: 1 addition & 1 deletion examples/timer/c_bindings/my_timer_cbor.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions examples/timer/cpp_bindings/my_timer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -846,7 +846,7 @@ class MyTimerCtx {
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));
Expand All @@ -856,7 +856,7 @@ class MyTimerCtx {
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));
Expand Down
11 changes: 11 additions & 0 deletions examples/timer/go_bindings/example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 25 additions & 1 deletion examples/timer/go_bindings/my_timer.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/timer/rust_bindings/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion examples/timer/rust_bindings/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
29 changes: 16 additions & 13 deletions ffi.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,22 @@ task genbindings_rust, "Generate Rust bindings for the timer example":
" -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"

task genbindings_c, "Generate C bindings for the timer example":
exec "nim c " & nimFlagsOrc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" &
" -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
exec "nim c " & nimFlagsRefc &
" --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c" &
" -d:ffiOutputDir=examples/timer/c_bindings" &
" -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"
# `mode` selects the ABI to emit: "native", "cbor", or "both" (-d:ffiMode).
proc genC(mode: string) =
for flags in [nimFlagsOrc, nimFlagsRefc]:
exec "nim c " & flags & " --app:lib --noMain --nimMainPrefix:libmy_timer" &
" -d:ffiGenBindings -d:targetLang=c -d:ffiMode=" & mode &
" -d:ffiOutputDir=examples/timer/c_bindings -d:ffiSrcPath=../timer.nim" &
" -o:/dev/null examples/timer/timer.nim"

task genbindings_c, "Generate C bindings (native + CBOR) for the timer example":
genC("both")

task genbindings_c_native, "Generate only the native C bindings (<lib>.h)":
genC("native")

task genbindings_c_cbor, "Generate only the CBOR C bindings (<lib>_cbor.h)":
genC("cbor")

task genbindings_go, "Generate Go (cgo) bindings for the timer example":
exec "nim c " & nimFlagsOrc &
Expand Down
31 changes: 20 additions & 11 deletions ffi/codegen/c.nim
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,14 @@ proc generateCHeader*(
)
lines.add("")

# `declareLibrary` always exports the listener-registration ABI.
# `declareLibrary` always exports the listener-registration ABI. The native
# listener delivers the payload as a typed struct: on RET_OK the callback's
# `msg` is a `const <Event>*` (cast it; valid only for the callback), keyed by
# the registered event name below.
if events.len > 0:
lines.add("// Native event payloads — cast the callback's msg accordingly:")
for e in events:
lines.add("// \"" & e.wireName & "\" -> const " & e.payloadTypeName & " *")
lines.add(
"uint64_t " & libName &
"_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);"
Expand Down Expand Up @@ -378,7 +385,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);"
Expand All @@ -399,12 +406,14 @@ proc generateCBindings*(
nimSrcRelPath: string,
events: seq[FFIEventMeta] = @[],
) =
# Emit both ABIs so consumers can choose per call site: the native (zero-copy,
# same-process) one and the CBOR (boundary-crossing / generic) one.
writeFile(
outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events)
)
writeFile(
outputDir / (libName & "_cbor.h"),
generateCborCHeader(procs, types, libName, events),
)
# Emit the ABI(s) selected by -d:ffiMode (default both): the native (zero-copy,
# same-process) header and/or the CBOR (boundary-crossing / generic) one.
if ffiEmitNative():
writeFile(
outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events)
)
if ffiEmitCbor():
writeFile(
outputDir / (libName & "_cbor.h"),
generateCborCHeader(procs, types, libName, events),
)
6 changes: 3 additions & 3 deletions ffi/codegen/cpp.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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);" %
Expand All @@ -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]
)
Expand Down Expand Up @@ -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(
Expand Down
57 changes: 56 additions & 1 deletion ffi/codegen/go.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -379,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;")
Expand Down Expand Up @@ -491,7 +505,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\"")
Expand Down Expand Up @@ -559,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)
Expand Down
10 changes: 10 additions & 0 deletions ffi/codegen/meta.nim
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ var currentLibName* {.compileTime.}: string
# Target language for binding generation; override with -d:targetLang=cpp
const targetLang* {.strdefine.} = "rust"

# Which ABI(s) to emit: "native" (zero-serialization C structs), "cbor"
# (inter-process), or "both" (default). Override with -d:ffiMode=native.
const ffiMode* {.strdefine.} = "both"

func ffiEmitNative*(): bool =
ffiMode == "native" or ffiMode == "both"

func ffiEmitCbor*(): bool =
ffiMode == "cbor" or ffiMode == "both"

# Output directory for generated bindings; set with -d:ffiOutputDir=path/to/dir
const ffiOutputDir* {.strdefine.} = ""

Expand Down
4 changes: 2 additions & 2 deletions ffi/codegen/rust.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(" };")
Expand Down
Loading