Ivan FB f08cb7971d
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>
2026-05-31 18:37:27 +02:00

420 lines
14 KiB
Nim

## C binding generator for the nim-ffi framework.
##
## Each `{.ffi.}` / `{.ffiCtor.}` proc is exported twice — a native typed-arg
## entry point under `<name>` (no CBOR, result delivered raw) and a `<name>_cbor`
## variant (CBOR request/response) — so this generator emits BOTH headers, side
## by side, and the consumer chooses per call site:
##
## - `<lib>.h` — native ABI: `int <name>(ctx, cb, ud, <typed args...>)`,
## ctor `void *<name>(<typed args...>, cb, ud)`, dtor `int <name>(ctx)`. The
## callback gets the raw result (RET_OK; not NUL-terminated, use len) or raw
## error text (RET_ERR). Best for SAME-process callers (zero serialization).
## - `<lib>_cbor.h` — CBOR ABI: `int <name>_cbor(ctx, cb, ud, reqCbor, len)`,
## ctor `void *<name>_cbor(reqCbor, len, cb, ud)`, plus a small CBOR request
## encoder (FfiCbor) and response decoder (ffi_decode_text). The request is a
## CBOR map keyed by the Nim param names (noted per proc). Best for callers
## that cross a process / machine boundary, where serialization is required
## anyway, or any generic / cross-language caller.
##
## Both headers guard their shared definitions (RET codes, FFICallBack) so a
## translation unit may include both. Library-initiated events (registered via
## `<lib>_add_event_listener`) arrive as raw JSON in either case.
import std/[os, strutils]
import ./meta, ./string_helpers
## Fixed 64-bit C type for any Nim `ptr T` / `pointer`, matching the other
## generators so the layout is architecture-stable.
const CPtrType = "uint64_t"
proc nimTypeToC(typeName: string): string =
## Maps a Nim parameter type to the C type used in the declaration.
let t = typeName.strip()
if t.startsWith("ptr ") or t == "pointer":
return "void*"
case t
of "string", "cstring": "const char*"
of "int", "int64", "clong": "int64_t"
of "int32", "cint": "int32_t"
of "int16": "int16_t"
of "int8": "int8_t"
of "uint", "uint64", "csize_t": "uint64_t"
of "uint32", "cuint": "uint32_t"
of "uint16": "uint16_t"
of "uint8", "byte": "uint8_t"
of "bool": "int"
of "float", "float32": "float"
of "float64": "double"
else: t
proc cParam(ep: FFIParamMeta): string =
(if ep.isPtr: CPtrType else: nimTypeToC(ep.typeName)) & " " & ep.name
proc typedArgs(p: FFIProcMeta): string =
var parts: seq[string] = @[]
for ep in p.extraParams:
parts.add(cParam(ep))
return parts.join(", ")
proc innerOf(t, prefix: string): string =
## Strips a `Prefix[...]` wrapper, returning the inner type name.
t[prefix.len .. ^2]
proc emitCStructs(types: seq[FFITypeMeta]): seq[string] =
## Emits a `typedef struct { ... } <Name>;` for every `{.ffi.}` type so the
## native header is self-contained (no undefined `TimerConfig` / `EchoRequest`
## references). Field layout mirrors the Nim object so the struct can be passed
## by value to the native entry points:
## - scalar / bool / float / nested `{.ffi.}` struct -> the matching C type
## - `cstring` (use this, not `string`, for native ABI) -> `const char*`
## - `seq[T]` -> `{ const T* <f>; size_t <f>_len; }`
## - `Option[T]`/`Maybe[T]` -> `{ int <f>_present; T <f>; }`
var lines: seq[string] = @[]
if types.len > 0:
lines.add("// --- {.ffi.}-annotated types, exposed as C structs ----------")
for t in types:
lines.add("typedef struct {")
for f in t.fields:
let ft = f.typeName.strip()
if ft.startsWith("seq[") and ft.endsWith("]"):
lines.add(" " & nimTypeToC(innerOf(ft, "seq[")) & " *" & f.name & ";")
lines.add(" size_t " & f.name & "_len;")
elif ft.startsWith("Option[") and ft.endsWith("]"):
lines.add(" int " & f.name & "_present;")
lines.add(" " & nimTypeToC(innerOf(ft, "Option[")) & " " & f.name & ";")
elif ft.startsWith("Maybe[") and ft.endsWith("]"):
lines.add(" int " & f.name & "_present;")
lines.add(" " & nimTypeToC(innerOf(ft, "Maybe[")) & " " & f.name & ";")
else:
lines.add(" " & nimTypeToC(ft) & " " & f.name & ";")
lines.add("} " & t.name & ";")
lines.add("")
return lines
const HeaderPrelude = """
// Generated by nim-ffi C codegen. Do not edit by hand.
//
// Native (zero-serialization) C ABI. Each call delivers its result to the
// callback. On RET_OK:
// - string-returning procs: (msg, len) is the raw string bytes (not
// NUL-terminated; use len).
// - struct-returning procs: msg is a pointer to the returned C struct — cast
// it to `const <Type>*` (len is sizeof). It is valid ONLY for the duration
// of the callback; copy out anything you need before returning. The library
// deep-frees it right after the callback (you free nothing).
// On RET_ERR, (msg, len) is the raw error text. A `<name>_cbor` variant of each
// proc also exists for generic/cross-language callers that prefer CBOR.
#ifndef NIM_FFI_GEN_<GUARD>_H
#define NIM_FFI_GEN_<GUARD>_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef NIM_FFI_RET_CODES
#define NIM_FFI_RET_CODES
#define RET_OK 0
#define RET_ERR 1
#define RET_MISSING_CALLBACK 2
#endif
#ifndef NIM_FFI_CALLBACK_T
#define NIM_FFI_CALLBACK_T
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
#endif
"""
proc generateCHeader*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
events: seq[FFIEventMeta] = @[],
): string =
let guard = libName.toUpper()
var lines: seq[string] = @[]
lines.add(HeaderPrelude.replace("<GUARD>", guard))
lines.add("")
lines.add(emitCStructs(types))
lines.add("")
for p in procs:
let args = typedArgs(p)
case p.kind
of FFIKind.DTOR:
lines.add("int " & p.procName & "(void *ctx);")
of FFIKind.CTOR:
let lead =
if args.len > 0:
args & ", "
else:
""
lines.add(
"void *" & p.procName & "(" & lead & "FFICallBack callback, void *userData);"
)
of FFIKind.FFI:
let tail =
if args.len > 0:
", " & args
else:
""
lines.add(
"int " & p.procName & "(void *ctx, FFICallBack callback, void *userData" & tail &
");"
)
lines.add("")
# `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);"
)
lines.add(
"int " & libName & "_remove_event_listener(void *ctx, uint64_t listenerId);"
)
lines.add("")
lines.add("#ifdef __cplusplus")
lines.add("} // extern \"C\"")
lines.add("#endif")
lines.add("")
lines.add("#endif /* NIM_FFI_GEN_" & guard & "_H */")
return lines.join("\n")
const CborHeaderPrelude = """
// Generated by nim-ffi C codegen. Do not edit by hand.
//
// CBOR ABI (`<name>_cbor`). Use this for callers that cross a process or machine
// boundary (the request has to be serialized anyway) or any generic / cross-
// language caller. Build the request with the FfiCbor helpers below — a CBOR map
// whose keys are the Nim parameter names (listed per proc) — call the matching
// `<name>_cbor`, and decode the RET_OK response (a CBOR-encoded value; for
// string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR
// delivers raw error text. For same-process callers, prefer the native `<name>`
// ABI in the companion <lib>.h header.
#ifndef NIM_FFI_GEN_<GUARD>_CBOR_H
#define NIM_FFI_GEN_<GUARD>_CBOR_H
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef __cplusplus
extern "C" {
#endif
#ifndef NIM_FFI_RET_CODES
#define NIM_FFI_RET_CODES
#define RET_OK 0
#define RET_ERR 1
#define RET_MISSING_CALLBACK 2
#endif
#ifndef NIM_FFI_CALLBACK_T
#define NIM_FFI_CALLBACK_T
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
#endif
#ifndef NIM_FFI_CBOR_HELPERS
#define NIM_FFI_CBOR_HELPERS
// --- minimal growable CBOR request encoder --------------------------------
typedef struct {
uint8_t *buf;
size_t cap;
size_t len;
} FfiCbor;
static inline FfiCbor ffi_cbor_new(void) {
FfiCbor c;
c.cap = 256;
c.len = 0;
c.buf = (uint8_t *)malloc(c.cap);
return c;
}
static inline void ffi_cbor_free(FfiCbor *c) {
free(c->buf);
c->buf = NULL;
}
static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) {
if (c->len >= c->cap) {
c->cap *= 2;
c->buf = (uint8_t *)realloc(c->buf, c->cap);
}
c->buf[c->len++] = b;
}
static inline void ffi_cbor_head(FfiCbor *c, uint8_t major, uint64_t arg) {
uint8_t mt = (uint8_t)(major << 5);
if (arg < 24) {
ffi_cbor_put(c, mt | (uint8_t)arg);
} else if (arg <= 0xff) {
ffi_cbor_put(c, mt | 24);
ffi_cbor_put(c, (uint8_t)arg);
} else if (arg <= 0xffff) {
ffi_cbor_put(c, mt | 25);
ffi_cbor_put(c, (uint8_t)(arg >> 8));
ffi_cbor_put(c, (uint8_t)arg);
} else if (arg <= 0xffffffffULL) {
ffi_cbor_put(c, mt | 26);
ffi_cbor_put(c, (uint8_t)(arg >> 24));
ffi_cbor_put(c, (uint8_t)(arg >> 16));
ffi_cbor_put(c, (uint8_t)(arg >> 8));
ffi_cbor_put(c, (uint8_t)arg);
} else {
ffi_cbor_put(c, mt | 27);
for (int s = 56; s >= 0; s -= 8) ffi_cbor_put(c, (uint8_t)(arg >> s));
}
}
static inline void ffi_cbor_map(FfiCbor *c, size_t n) { ffi_cbor_head(c, 5, n); }
static inline void ffi_cbor_text(FfiCbor *c, const char *s) {
size_t n = s ? strlen(s) : 0;
ffi_cbor_head(c, 3, n);
for (size_t i = 0; i < n; i++) ffi_cbor_put(c, (uint8_t)s[i]);
}
static inline void ffi_cbor_kv_text(FfiCbor *c, const char *k, const char *v) {
ffi_cbor_text(c, k);
ffi_cbor_text(c, v);
}
static inline void ffi_cbor_kv_uint(FfiCbor *c, const char *k, uint64_t v) {
ffi_cbor_text(c, k);
ffi_cbor_head(c, 0, v);
}
static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_t v) {
ffi_cbor_text(c, k);
if (v >= 0)
ffi_cbor_head(c, 0, (uint64_t)v);
else
ffi_cbor_head(c, 1, (uint64_t)(-(v + 1)));
}
// --- response decoding -----------------------------------------------------
// Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets
// *out/*outLen to point INTO `data` (no allocation; valid only while `data` is)
// and returns 1; returns 0 for a non-text-string payload.
static inline int ffi_text_view(const uint8_t *data, size_t len,
const uint8_t **out, size_t *outLen) {
if (len < 1 || (data[0] >> 5) != 3) return 0;
uint8_t info = data[0] & 0x1f;
size_t p = 1;
uint64_t slen = 0;
if (info < 24) {
slen = info;
} else if (info == 24) {
if (len < p + 1) return 0;
slen = data[p++];
} else if (info == 25) {
if (len < p + 2) return 0;
slen = ((uint64_t)data[p] << 8) | data[p + 1];
p += 2;
} else if (info == 26) {
if (len < p + 4) return 0;
slen = ((uint64_t)data[p] << 24) | ((uint64_t)data[p + 1] << 16) |
((uint64_t)data[p + 2] << 8) | data[p + 3];
p += 4;
} else {
return 0;
}
if (len < p + slen) return 0;
*out = data + p;
*outLen = (size_t)slen;
return 1;
}
// Owning variant: malloc a NUL-terminated copy. NULL for a non-text payload.
// Caller frees.
static inline char *ffi_decode_text(const uint8_t *data, size_t len) {
const uint8_t *view;
size_t slen;
if (!ffi_text_view(data, len, &view, &slen)) return NULL;
char *out = (char *)malloc(slen + 1);
if (!out) return NULL;
memcpy(out, view, slen);
out[slen] = '\0';
return out;
}
#endif // NIM_FFI_CBOR_HELPERS
"""
proc requestFieldsComment(p: FFIProcMeta): string =
## A one-line note of the CBOR request-map keys (the Nim param names) so a
## caller knows what to encode.
if p.extraParams.len == 0:
return "// request: empty CBOR map (0xA0)"
var parts: seq[string] = @[]
for ep in p.extraParams:
parts.add("\"" & ep.name & "\": " & ep.typeName)
return "// request map keys: {" & parts.join(", ") & "}"
proc generateCborCHeader*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
events: seq[FFIEventMeta] = @[],
): string =
let guard = libName.toUpper()
var lines: seq[string] = @[]
lines.add(CborHeaderPrelude.replace("<GUARD>", guard))
lines.add("")
for p in procs:
case p.kind
of FFIKind.DTOR:
# The destructor takes no request, so it has no CBOR variant.
lines.add("int " & p.procName & "(void *ctx);")
of FFIKind.CTOR:
lines.add(requestFieldsComment(p))
lines.add(
"void *" & p.procName &
"_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);"
)
of FFIKind.FFI:
lines.add(requestFieldsComment(p))
lines.add(
"int " & p.procName &
"_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);"
)
lines.add("")
lines.add(
"uint64_t " & libName &
"_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);"
)
lines.add("")
lines.add("#ifdef __cplusplus")
lines.add("} // extern \"C\"")
lines.add("#endif")
lines.add("")
lines.add("#endif /* NIM_FFI_GEN_" & guard & "_CBOR_H */")
return lines.join("\n")
proc generateCBindings*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
outputDir: string,
nimSrcRelPath: string,
events: seq[FFIEventMeta] = @[],
) =
# 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),
)