feat(codegen): C native event payloads + -d:ffiMode (native/cbor/both) + tasks

- Add the `-d:ffiMode=native|cbor|both` strdefine (default both) with
  `ffiEmitNative`/`ffiEmitCbor` helpers; the C generator now emits only the
  selected header(s) (`<lib>.h` and/or `<lib>_cbor.h`).
- Native C events: the native header documents each event's payload type
  (`"on_echo_fired" -> const EchoEvent *`) so consumers cast the callback's msg
  to the typed struct — the bare native listener already delivers it.
- nimble tasks: `genbindings_c` (both), `genbindings_c_native`,
  `genbindings_c_cbor`.

Verified: native mode emits only my_timer.h, cbor only my_timer_cbor.h, both
emits both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 18:00:24 +02:00
parent ceccc7bef3
commit f08cb7971d
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
4 changed files with 46 additions and 24 deletions

View File

@ -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);

View File

@ -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 &

View File

@ -167,8 +167,13 @@ proc generateCHeader*(
lines.add("")
# `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.
# 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);"
@ -401,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),
)

View File

@ -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.} = ""