mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
- 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>
420 lines
14 KiB
Nim
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),
|
|
)
|