927 lines
34 KiB
Nim

## C99 binding generator for the nim-ffi framework.
## Emits a header-only C binding plus a CMakeLists.txt. The binding is split
## into three headers so the example reads cleanly: `nim_ffi_prelude.h` (owned
## string/byte types + libc includes), `nim_ffi_cbor.h` (leaf CBOR codecs and
## buffer drivers, includes the prelude), and `<lib>.h` (the library-specific
## structs, codecs and async API, includes the cbor header). Requests/responses
## travel as CBOR (encoded with the same vendored TinyCBOR the C++ backend
## uses, matching the Nim-side cbor_serial codec — both ends speak RFC 8949).
##
## C has neither generics nor overloading, so the codecs the C++ backend gets
## from templates are monomorphised here: every distinct `seq[T]` / `Option[T]`
## becomes its own struct + encode/decode/free triple, and each leaf type has a
## distinctly-named codec emitted by the cbor_helpers template.
import std/[os, strutils, tables, sets]
import ./meta, ./string_helpers, ./c_cpp_common
## Wire-format C type for any Nim `ptr T` / `pointer`. Fixed 64-bit so the CBOR
## payload size is stable regardless of host architecture (mirrors CppPtrType).
const CPtrType* = "uint64_t"
const
HeaderPreludeTpl = staticRead("templates/c/header_prelude.h.tpl")
CborHelpersTpl = staticRead("templates/c/cbor_helpers.h.tpl")
CMakeListsTpl = staticRead("templates/c/CMakeLists.txt.tpl")
# Shared headers written alongside the library header. Their names match the
# include guards baked into the templates and the `#include` the cbor header
# emits for the prelude.
PreludeHeaderName* = "nim_ffi_prelude.h"
CborHeaderName* = "nim_ffi_cbor.h"
type LeafInfo = tuple[ok: bool, cType: string, suffix: string, owns: bool]
func leafCType(t: string): LeafInfo =
## Maps a Nim leaf type to its C type, codec suffix and whether a decoded
## value owns heap memory. `ok` is false for composite types (seq/Option/
## user structs), which are monomorphised separately.
case t
of "int", "int64":
(true, "int64_t", "i64", false)
of "int32":
(true, "int32_t", "i32", false)
of "int16":
(true, "int16_t", "i16", false)
of "int8":
(true, "int8_t", "i8", false)
of "uint", "uint64":
(true, "uint64_t", "u64", false)
of "uint32":
(true, "uint32_t", "u32", false)
of "uint16":
(true, "uint16_t", "u16", false)
of "uint8", "byte":
(true, "uint8_t", "u8", false)
of "bool":
(true, "bool", "bool", false)
of "float", "float64":
(true, "double", "f64", false)
of "float32":
(true, "float", "f32", false)
of "pointer":
(true, CPtrType, "u64", false)
of "string", "cstring":
(true, "NimFfiStr", "str", true)
else:
(false, "", "", false)
func cToken(cType: string): string =
## Short PascalCase token used to build monomorphised container names and
## codec-adapter symbols. Composite C type names are already unique C
## identifiers, so they pass through verbatim.
case cType
of "int64_t": "I64"
of "int32_t": "I32"
of "int16_t": "I16"
of "int8_t": "I8"
of "uint64_t": "U64"
of "uint32_t": "U32"
of "uint16_t": "U16"
of "uint8_t": "U8"
of "bool": "Bool"
of "double": "F64"
of "float": "F32"
of "NimFfiStr": "Str"
of "NimFfiBytes": "Bytes"
else: cType
func leafSuffix(cType: string): string =
## Inverse of leafCType's cType→suffix for the leaf codecs the template
## provides; empty string for composite types.
case cType
of "int64_t": "i64"
of "int32_t": "i32"
of "int16_t": "i16"
of "int8_t": "i8"
of "uint64_t": "u64"
of "uint32_t": "u32"
of "uint16_t": "u16"
of "uint8_t": "u8"
of "bool": "bool"
of "double": "f64"
of "float": "f32"
of "NimFfiStr": "str"
of "NimFfiBytes": "bytes"
else: ""
type CTypeReg = object
libName: string ## snake_case symbol prefix, e.g. "my_timer"
libType: string ## PascalCase container-name prefix, e.g. "MyTimer"
typeTable: Table[string, FFITypeMeta] ## user structs + synthetic Req structs
emitted: HashSet[string] ## composite C type names already emitted
owns: Table[string, bool] ## C type name → owns-heap-memory
decls: seq[string] ## struct typedefs, dependency order
codecs: seq[string] ## enc/dec/free defs, dependency order
func encFn(reg: CTypeReg, cType: string): string =
let suffix = leafSuffix(cType)
if suffix.len > 0:
return "nimffi_enc_" & suffix
reg.libName & "_enc_" & cType
func decFn(reg: CTypeReg, cType: string): string =
let suffix = leafSuffix(cType)
if suffix.len > 0:
return "nimffi_dec_" & suffix
reg.libName & "_dec_" & cType
func freeFn(reg: CTypeReg, cType: string): string =
## Free-function name for `cType`, or "" when the type owns no heap memory.
case cType
of "NimFfiStr":
"nimffi_free_str"
of "NimFfiBytes":
"nimffi_free_bytes"
else:
if leafSuffix(cType).len > 0:
""
elif reg.owns.getOrDefault(cType, false):
reg.libName & "_free_" & cType
else:
""
proc emitSeqType(reg: var CTypeReg, name, elemC: string) =
let eEnc = encFn(reg, elemC)
let eDec = decFn(reg, elemC)
let eFree = freeFn(reg, elemC)
reg.decls.add(
"typedef struct {\n " & elemC & "* data;\n size_t len;\n} " & name & ";"
)
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
body.add(" CborEncoder* e, const " & name & "* v) {")
body.add(" CborEncoder arr;")
body.add(" CborError err = cbor_encoder_create_array(e, &arr, v->len);")
body.add(" if (err) return err;")
body.add(" for (size_t i = 0; i < v->len; i++) {")
body.add(" err = " & eEnc & "(&arr, &v->data[i]);")
body.add(" if (err) return err;")
body.add(" }")
body.add(" return cbor_encoder_close_container(e, &arr);")
body.add("}")
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
body.add(" CborValue* it, " & name & "* out) {")
body.add(" if (!cbor_value_is_array(it)) return CborErrorImproperValue;")
body.add(" size_t len = 0;")
body.add(" CborError err = cbor_value_get_array_length(it, &len);")
body.add(" if (err) return err;")
body.add(
" out->data = (" & elemC & "*)calloc(len ? len : 1, sizeof(" & elemC & "));"
)
body.add(" if (!out->data) return CborErrorOutOfMemory;")
body.add(" out->len = len;")
body.add(" CborValue inner;")
body.add(" err = cbor_value_enter_container(it, &inner);")
body.add(" if (err) return err;")
body.add(" for (size_t i = 0; i < len; i++) {")
body.add(" err = " & eDec & "(&inner, &out->data[i]);")
body.add(" if (err) return err;")
body.add(" }")
body.add(" return cbor_value_leave_container(it, &inner);")
body.add("}")
body.add(
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
)
body.add(" if (!v || !v->data) return;")
if eFree.len > 0:
body.add(" for (size_t i = 0; i < v->len; i++) " & eFree & "(&v->data[i]);")
body.add(" free(v->data);")
body.add(" v->data = NULL;")
body.add(" v->len = 0;")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[name] = true
proc emitOptType(reg: var CTypeReg, name, elemC: string, elemOwns: bool) =
let eEnc = encFn(reg, elemC)
let eDec = decFn(reg, elemC)
let eFree = freeFn(reg, elemC)
reg.decls.add(
"typedef struct {\n bool has_value;\n " & elemC & " value;\n} " & name & ";"
)
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & name & "(")
body.add(" CborEncoder* e, const " & name & "* v) {")
body.add(" if (!v->has_value) return cbor_encode_null(e);")
body.add(" return " & eEnc & "(e, &v->value);")
body.add("}")
body.add("static inline CborError " & reg.libName & "_dec_" & name & "(")
body.add(" CborValue* it, " & name & "* out) {")
body.add(" if (cbor_value_is_null(it)) {")
body.add(" out->has_value = false;")
body.add(" memset(&out->value, 0, sizeof(out->value));")
body.add(" return cbor_value_advance(it);")
body.add(" }")
body.add(" out->has_value = true;")
body.add(" return " & eDec & "(it, &out->value);")
body.add("}")
if elemOwns and eFree.len > 0:
body.add(
"static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {"
)
body.add(" if (!v || !v->has_value) return;")
body.add(" " & eFree & "(&v->value);")
body.add(" v->has_value = false;")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[name] = elemOwns
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool]
proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) =
var fieldDecls: seq[string] = @[]
var members: seq[tuple[name, cType: string, owns: bool]] = @[]
for f in t.fields:
let (cType, owns) = ensureCType(reg, f.typeName)
fieldDecls.add(" " & cType & " " & f.name & ";")
members.add((f.name, cType, owns))
if members.len == 0:
fieldDecls.add(" char _nimffi_empty; /* C forbids empty structs */")
reg.decls.add("typedef struct {\n" & fieldDecls.join("\n") & "\n} " & t.name & ";")
var body: seq[string] = @[]
body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(")
body.add(" CborEncoder* e, const " & t.name & "* v) {")
if members.len == 0:
body.add(" (void)v;")
body.add(" CborEncoder m;")
body.add(" CborError err = cbor_encoder_create_map(e, &m, " & $members.len & ");")
body.add(" if (err) return err;")
for mem in members:
body.add(" err = cbor_encode_text_stringz(&m, \"" & mem.name & "\");")
body.add(" if (err) return err;")
body.add(" err = " & encFn(reg, mem.cType) & "(&m, &v->" & mem.name & ");")
body.add(" if (err) return err;")
body.add(" return cbor_encoder_close_container(e, &m);")
body.add("}")
body.add("static inline CborError " & reg.libName & "_dec_" & t.name & "(")
body.add(" CborValue* it, " & t.name & "* out) {")
body.add(" if (!cbor_value_is_map(it)) return CborErrorImproperValue;")
if members.len == 0:
body.add(" (void)out;")
body.add(" return cbor_value_advance(it);")
else:
body.add(" CborValue field;")
body.add(" CborError err;")
for mem in members:
body.add(" err = cbor_value_map_find_value(it, \"" & mem.name & "\", &field);")
body.add(" if (err) return err;")
body.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;")
body.add(
" err = " & decFn(reg, mem.cType) & "(&field, &out->" & mem.name & ");"
)
body.add(" if (err) return err;")
body.add(" return cbor_value_advance(it);")
body.add("}")
var owns = false
for mem in members:
if mem.owns:
owns = true
if owns:
body.add(
"static inline void " & reg.libName & "_free_" & t.name & "(" & t.name & "* v) {"
)
body.add(" if (!v) return;")
for mem in members:
let ff = freeFn(reg, mem.cType)
if mem.owns and ff.len > 0:
body.add(" " & ff & "(&v->" & mem.name & ");")
body.add("}")
reg.codecs.add(body.join("\n"))
reg.owns[t.name] = owns
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
let t = nimType.strip()
if t.startsWith("ptr ") or t == "pointer":
return (CPtrType, false)
let leaf = leafCType(t)
if leaf.ok:
return (leaf.cType, leaf.owns)
let seqInner = genericInnerType(t, "seq[")
if seqInner.len > 0:
let inner = seqInner.strip()
if inner == "byte" or inner == "uint8":
return ("NimFfiBytes", true)
let (elemC, _) = ensureCType(reg, inner)
let name = reg.libType & "Seq_" & cToken(elemC)
if name notin reg.emitted:
reg.emitted.incl(name)
emitSeqType(reg, name, elemC)
return (name, true)
var optInner = genericInnerType(t, "Option[")
if optInner.len == 0:
optInner = genericInnerType(t, "Maybe[")
if optInner.len > 0:
let (elemC, elemOwns) = ensureCType(reg, optInner.strip())
let name = reg.libType & "Opt_" & cToken(elemC)
if name notin reg.emitted:
reg.emitted.incl(name)
emitOptType(reg, name, elemC, elemOwns)
return (name, reg.owns.getOrDefault(name, false))
if t notin reg.emitted:
reg.emitted.incl(t)
if t in reg.typeTable:
emitStructType(reg, reg.typeTable[t])
else:
reg.decls.add("/* unknown type referenced: " & t & " */")
(t, reg.owns.getOrDefault(t, false))
proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
## Synthesises the per-proc Req struct as an FFITypeMeta so it flows through
## the same monomorphisation path as user-declared types. Pointer/handle
## params ride the wire as the opaque uint64 pointer type.
var fields: seq[FFIFieldMeta] = @[]
for ep in p.extraParams:
let typeName = if ep.ridesAsPtr(): "pointer" else: ep.typeName
fields.add(FFIFieldMeta(name: ep.name, typeName: typeName))
FFITypeMeta(name: reqStructName(p), fields: fields)
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
## Scalars / opaque pointers / string views pass by value; composite
## aggregates (seq, Option, user structs) pass by const pointer.
if ridesAsPtr:
return true
leafCType(nimType.strip()).ok
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
if p.returnRidesAsPtr():
return CPtrType
ensureCType(reg, p.returnTypeName).cType
proc buildReqParams(
reg: var CTypeReg, eps: seq[FFIParamMeta]
): tuple[params, assigns: seq[string]] =
var params: seq[string] = @[]
var assigns: seq[string] = @[]
for ep in eps:
let rides = ep.ridesAsPtr()
let cType =
if rides:
CPtrType
else:
ensureCType(reg, ep.typeName).cType
if paramByValue(ep.typeName, rides):
params.add(cType & " " & ep.name)
assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";")
else:
params.add("const " & cType & "* " & ep.name)
assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";")
(params, assigns)
proc evNames(
libType, libName: string, ev: FFIEventMeta
): tuple[fnType, boxType, tramp, regName: string] =
let pascal = capitalizeFirstLetter(ev.nimProcName)
let snake = camelToSnakeCase(ev.nimProcName)
(
libType & pascal & "Fn",
libType & pascal & "Box",
libName & "_" & snake & "_trampoline",
libName & "_ctx_add_" & snake & "_listener",
)
proc emitEventMachinery(
lines: var seq[string],
reg: CTypeReg,
libType, libName: string,
events: seq[FFIEventMeta],
) =
if events.len == 0:
return
lines.add("/* Event listener machinery */")
for ev in events:
let n = evNames(libType, libName, ev)
let payC = ev.payloadTypeName
let payFree = freeFn(reg, payC)
lines.add(
"typedef void (*" & n.fnType & ")(const " & payC & "* evt, void* user_data);"
)
lines.add(
"typedef struct { " & n.fnType & " fn; void* user_data; } " & n.boxType & ";"
)
lines.add(
"static void " & n.tramp & "(int ret, const char* msg, size_t len, void* ud) {"
)
lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;")
lines.add(" " & n.boxType & "* box = (" & n.boxType & "*)ud;")
lines.add(" if (!box->fn) return;")
lines.add(" CborParser parser;")
lines.add(" CborValue it;")
lines.add(
" if (cbor_parser_init((const uint8_t*)msg, len, 0, &parser, &it) != CborNoError) return;"
)
lines.add(" if (!cbor_value_is_map(&it)) return;")
lines.add(" CborValue payloadField;")
lines.add(
" if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;"
)
lines.add(" " & payC & " payload;")
lines.add(" memset(&payload, 0, sizeof(payload));")
lines.add(
" if (" & decFn(reg, payC) & "(&payloadField, &payload) != CborNoError) return;"
)
lines.add(" box->fn(&payload, box->user_data);")
if payFree.len > 0:
lines.add(" " & payFree & "(&payload);")
lines.add("}")
lines.add("")
proc emitContextStruct(
lines: var seq[string], ctxType: string, events: seq[FFIEventMeta]
) =
lines.add("/* ============================================================ */")
lines.add("/* High-level context wrapper */")
lines.add("/* ============================================================ */")
if events.len > 0:
lines.add("typedef struct {")
lines.add(" uint64_t id;")
lines.add(" void* box;")
lines.add("} " & ctxType & "Listener;")
lines.add("")
lines.add("typedef struct {")
lines.add(" void* ptr;")
if events.len > 0:
lines.add(" " & ctxType & "Listener* listeners;")
lines.add(" size_t listeners_len;")
lines.add(" size_t listeners_cap;")
lines.add("} " & ctxType & ";")
lines.add("")
proc emitCallBox(lines: var seq[string], fnType, boxType: string) =
lines.add("typedef struct { " & fnType & " fn; void* user_data; } " & boxType & ";")
proc emitReplyTrampolineHead(lines: var seq[string], tramp, boxType, fallback: string) =
## Opens a reply trampoline: cast the user-data back to the call box, bail if
## the caller passed no callback (nothing to deliver to, and leaving early
## avoids allocating a result nobody receives), then deliver a non-zero `ret`
## as an error. The error text in msg/len is not NUL-terminated, so copy it.
lines.add(
"static void " & tramp & "(int ret, const char* msg, size_t len, void* ud) {"
)
lines.add(" " & boxType & "* box = (" & boxType & "*)ud;")
lines.add(" if (!box->fn) {")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" if (ret != 0) {")
lines.add(" char* em = nimffi_dup_cstr_n(msg ? msg : \"\", msg ? len : 0);")
lines.add(
" box->fn(ret, NULL, em ? em : \"" & fallback & "\", box->user_data);"
)
lines.add(" free(em);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
proc emitConstructors(
lines: var seq[string],
reg: var CTypeReg,
ctxType, libType, libName: string,
ctors: seq[FFIProcMeta],
) =
if ctors.len == 0:
return
let fnType = libType & "CreateFn"
let boxType = libType & "CreateBox"
let tramp = libName & "_create_trampoline"
lines.add(
"typedef void (*" & fnType & ")(int err_code, " & ctxType &
"* ctx, const char* err_msg, void* user_data);"
)
emitCallBox(lines, fnType, boxType)
emitReplyTrampolineHead(lines, tramp, boxType, "FFI create failed")
lines.add(" char* err = NULL;")
lines.add(" NimFfiStr addr;")
lines.add(" memset(&addr, 0, sizeof(addr));")
lines.add(
" if (nimffi_decode_from_buf(" & libName &
"_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {"
)
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
lines.add(" free(err);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" char* endp = NULL;")
lines.add(
" unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;"
)
lines.add(" bool ok = addr.data && addr.len > 0 && endp && *endp == '\\0';")
lines.add(" nimffi_free_str(&addr);")
lines.add(" if (!ok) {")
lines.add(
" box->fn(-1, NULL, \"FFI create returned non-numeric address\", box->user_data);"
)
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(
" " & ctxType & "* ctx = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));"
)
lines.add(" if (!ctx) {")
lines.add(" box->fn(-1, NULL, \"out of memory\", box->user_data);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" ctx->ptr = (void*)(uintptr_t)a;")
lines.add(" box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);")
lines.add(" free(box);")
lines.add("}")
lines.add("")
for ctor in ctors:
let reqName = reqStructName(ctor)
let (params, assigns) = buildReqParams(reg, ctor.extraParams)
let head = "static inline int " & libName & "_ctx_create("
let sig =
if params.len > 0:
head & params.join(", ") & ", " & fnType & " on_created, void* user_data) {"
else:
head & fnType & " on_created, void* user_data) {"
lines.add(sig)
lines.add(" " & reqName & " ffi_req;")
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
for a in assigns:
lines.add(a)
lines.add(" uint8_t* req_buf = NULL;")
lines.add(" size_t req_len = 0;")
lines.add(" char* err = NULL;")
lines.add(
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
)
lines.add(
" if (on_created) on_created(-1, NULL, err ? err : \"encode failed\", user_data);"
)
lines.add(" free(err);")
lines.add(" return -1;")
lines.add(" }")
lines.add(
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
)
lines.add(" if (!box) {")
lines.add(" free(req_buf);")
lines.add(
" if (on_created) on_created(-1, NULL, \"out of memory\", user_data);"
)
lines.add(" return -1;")
lines.add(" }")
lines.add(" box->fn = on_created;")
lines.add(" box->user_data = user_data;")
lines.add(" (void)" & ctor.procName & "(req_buf, req_len, " & tramp & ", box);")
lines.add(" free(req_buf);")
lines.add(" return 0;")
lines.add("}")
lines.add("")
proc emitDestructor(
lines: var seq[string],
ctxType, libName, dtorProcName: string,
events: seq[FFIEventMeta],
) =
lines.add("static inline void " & libName & "_ctx_destroy(" & ctxType & "* ctx) {")
lines.add(" if (!ctx) return;")
if dtorProcName.len > 0:
lines.add(" if (ctx->ptr) { " & dtorProcName & "(ctx->ptr); ctx->ptr = NULL; }")
if events.len > 0:
lines.add(
" for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box);"
)
lines.add(" free(ctx->listeners);")
lines.add(" free(ctx);")
lines.add("}")
lines.add("")
proc emitListenerApi(
lines: var seq[string], ctxType, libType, libName: string, events: seq[FFIEventMeta]
) =
if events.len == 0:
return
for ev in events:
let n = evNames(libType, libName, ev)
lines.add(
"static inline uint64_t " & n.regName & "(" & ctxType & "* ctx, " & n.fnType &
" fn, void* user_data) {"
)
lines.add(
" " & n.boxType & "* box = (" & n.boxType & "*)malloc(sizeof(" & n.boxType &
"));"
)
lines.add(" if (!box) return 0;")
lines.add(" box->fn = fn;")
lines.add(" box->user_data = user_data;")
lines.add(
" uint64_t id = " & libName & "_add_event_listener(ctx->ptr, \"" & ev.wireName &
"\", " & n.tramp & ", box);"
)
lines.add(" if (id == 0) { free(box); return 0; }")
lines.add(" if (ctx->listeners_len == ctx->listeners_cap) {")
lines.add(" size_t ncap = ctx->listeners_cap ? ctx->listeners_cap * 2 : 4;")
lines.add(
" " & ctxType & "Listener* grown = (" & ctxType &
"Listener*)realloc(ctx->listeners, ncap * sizeof(" & ctxType & "Listener));"
)
lines.add(
" if (!grown) { " & libName &
"_remove_event_listener(ctx->ptr, id); free(box); return 0; }"
)
lines.add(" ctx->listeners = grown;")
lines.add(" ctx->listeners_cap = ncap;")
lines.add(" }")
lines.add(" ctx->listeners[ctx->listeners_len].id = id;")
lines.add(" ctx->listeners[ctx->listeners_len].box = box;")
lines.add(" ctx->listeners_len++;")
lines.add(" return id;")
lines.add("}")
lines.add("")
lines.add(
"static inline bool " & libName & "_ctx_remove_event_listener(" & ctxType &
"* ctx, uint64_t id) {"
)
lines.add(" if (id == 0) return false;")
lines.add(" int rc = " & libName & "_remove_event_listener(ctx->ptr, id);")
lines.add(" for (size_t i = 0; i < ctx->listeners_len; i++) {")
lines.add(" if (ctx->listeners[i].id == id) {")
lines.add(" free(ctx->listeners[i].box);")
lines.add(" ctx->listeners[i] = ctx->listeners[ctx->listeners_len - 1];")
lines.add(" ctx->listeners_len--;")
lines.add(" break;")
lines.add(" }")
lines.add(" }")
lines.add(" return rc == 0;")
lines.add("}")
lines.add("")
proc emitMethod(
lines: var seq[string],
reg: var CTypeReg,
ctxType, libType, libName: string,
m: FFIProcMeta,
) =
let stripped = stripLibPrefix(m.procName, libName)
let reqName = reqStructName(m)
let retC = cReturnType(reg, m)
let retFree = freeFn(reg, retC)
let (params, assigns) = buildReqParams(reg, m.extraParams)
let methodPascal = snakeToPascalCase(stripped)
let fnType = libType & methodPascal & "ReplyFn"
let boxType = libType & methodPascal & "CallBox"
let tramp = libName & "_" & stripped & "_reply_trampoline"
lines.add(
"typedef void (*" & fnType & ")(int err_code, const " & retC &
"* reply, const char* err_msg, void* user_data);"
)
emitCallBox(lines, fnType, boxType)
emitReplyTrampolineHead(lines, tramp, boxType, "FFI call failed")
lines.add(" char* err = NULL;")
lines.add(" " & retC & " out;")
lines.add(" memset(&out, 0, sizeof(out));")
lines.add(
" int dec = nimffi_decode_from_buf(" & libName & "_decv_" & cToken(retC) &
", (const uint8_t*)msg, len, &out, &err);"
)
lines.add(" if (dec != 0) {")
lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);")
lines.add(" free(err);")
# A partial decode may have allocated some fields; reclaim them (out is
# zeroed, so the typed free skips what was never written).
if retFree.len > 0:
lines.add(" " & retFree & "(&out);")
lines.add(" free(box);")
lines.add(" return;")
lines.add(" }")
lines.add(" box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);")
if retFree.len > 0:
lines.add(" " & retFree & "(&out);")
lines.add(" free(box);")
lines.add("}")
let head =
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
let sig =
if params.len > 0:
head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {"
else:
head & fnType & " on_reply, void* user_data) {"
lines.add(sig)
lines.add(" " & reqName & " ffi_req;")
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
for a in assigns:
lines.add(a)
lines.add(" uint8_t* req_buf = NULL;")
lines.add(" size_t req_len = 0;")
lines.add(" char* err = NULL;")
lines.add(
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
", &ffi_req, &req_buf, &req_len, &err) != 0) {"
)
lines.add(
" if (on_reply) on_reply(-1, NULL, err ? err : \"encode failed\", user_data);"
)
lines.add(" free(err);")
lines.add(" return -1;")
lines.add(" }")
lines.add(
" " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));"
)
lines.add(" if (!box) {")
lines.add(" free(req_buf);")
lines.add(" if (on_reply) on_reply(-1, NULL, \"out of memory\", user_data);")
lines.add(" return -1;")
lines.add(" }")
lines.add(" box->fn = on_reply;")
lines.add(" box->user_data = user_data;")
lines.add(
" int ret = " & m.procName & "(ctx->ptr, " & tramp & ", box, req_buf, req_len);"
)
lines.add(" free(req_buf);")
lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {")
lines.add(
" if (on_reply) on_reply(-1, NULL, \"RET_MISSING_CALLBACK (internal error)\", user_data);"
)
lines.add(" free(box);")
lines.add(" return -1;")
lines.add(" }")
lines.add(" return 0;")
lines.add("}")
lines.add("")
proc newCTypeReg(
libName, libType: string, types: seq[FFITypeMeta], procs: seq[FFIProcMeta]
): CTypeReg =
var reg = CTypeReg(libName: libName, libType: libType)
for t in types:
reg.typeTable[t.name] = t
for p in procs:
if p.kind != FFIKind.DTOR:
let rt = reqTypeMeta(p)
reg.typeTable[rt.name] = rt
reg
proc monomorphiseAll(
reg: var CTypeReg,
types: seq[FFITypeMeta],
procs, methods: seq[FFIProcMeta],
events: seq[FFIEventMeta],
): tuple[reqTypes, respTypes: seq[string]] =
## Walks every user type, per-proc Req envelope, return type and event
## payload through ensureCType, emitting their structs/codecs into `reg` in
## dependency order. Returns the Req and response C type names the buffer
## adapters need.
for t in types:
discard ensureCType(reg, t.name)
var reqTypes: seq[string] = @[]
for p in procs:
if p.kind != FFIKind.DTOR:
let n = reqStructName(p)
discard ensureCType(reg, n)
reqTypes.add(n)
var respTypes: seq[string] = @[]
for m in methods:
respTypes.add(cReturnType(reg, m))
for ev in events:
discard ensureCType(reg, ev.payloadTypeName)
(reqTypes, respTypes)
func generateCPreludeHeader*(): string =
## The `nim_ffi_prelude.h` shared header: owned string/byte types plus the
## libc/TinyCBOR includes every nim-ffi C binding needs. Identical across
## libraries, so it is emitted verbatim from the template.
HeaderPreludeTpl & "\n"
func generateCCborHeader*(): string =
## The `nim_ffi_cbor.h` shared header: leaf CBOR codecs and buffer drivers.
## Includes the prelude (its guard is inside the template) and is library-
## agnostic, so it too is emitted verbatim.
CborHelpersTpl & "\n"
proc generateCLibHeader*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
events: seq[FFIEventMeta] = @[],
): string =
## The `<lib>.h` header: library-specific structs, monomorphised codecs and
## the async API. Pulls the two shared headers in via the cbor header.
let classified = classifyProcs(procs)
let ctors = classified.ctors
let methods = classified.methods
let libType = libTypeName(ctors, libName)
let ctxType = libType & "Ctx"
var reg = newCTypeReg(libName, libType, types, procs)
let (reqTypes, respTypes) = monomorphiseAll(reg, types, procs, methods, events)
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_H_INCLUDED"
var lines: seq[string] = @[]
lines.add("#ifndef " & guard)
lines.add("#define " & guard)
lines.add("#include \"" & CborHeaderName & "\"")
lines.add("")
lines.add("/* ============================================================ */")
lines.add("/* Generated types (user-declared + per-proc request envelopes) */")
lines.add("/* ============================================================ */")
lines.add("")
for decl in reg.decls:
lines.add(decl)
lines.add("")
for codec in reg.codecs:
lines.add(codec)
lines.add("")
lines.add("/* ============================================================ */")
lines.add("/* C ABI declarations (symbols exported by the Nim dylib) */")
lines.add("/* ============================================================ */")
lines.add("#ifdef __cplusplus")
lines.add("extern \"C\" {")
lines.add("#endif")
lines.add("")
for p in procs:
case p.kind
of FFIKind.FFI:
lines.add(
"int " & p.procName & "(void* ctx, FFICallback callback, void* user_data, " &
"const uint8_t* req_cbor, size_t req_cbor_len);"
)
of FFIKind.CTOR:
lines.add(
"void* " & p.procName & "(const uint8_t* req_cbor, size_t req_cbor_len, " &
"FFICallback callback, void* user_data);"
)
of FFIKind.DTOR:
lines.add("int " & p.procName & "(void* ctx);")
lines.add(
"uint64_t " & libName & "_add_event_listener(void* ctx, const char* event_name, " &
"FFICallback callback, void* user_data);"
)
lines.add(
"int " & libName & "_remove_event_listener(void* ctx, uint64_t listener_id);"
)
lines.add("")
lines.add("#ifdef __cplusplus")
lines.add("} /* extern \"C\" */")
lines.add("#endif")
lines.add("")
# Per-Req encode / per-response decode void* adapters for the buffer drivers.
var adaptersDone = initHashSet[string]()
lines.add("/* CBOR buffer adapters (typed codec → void* driver signature) */")
for n in reqTypes:
let tok = cToken(n)
if ("enc" & tok) notin adaptersDone:
adaptersDone.incl("enc" & tok)
lines.add(
"static inline CborError " & libName & "_encv_" & tok &
"(CborEncoder* e, const void* v) { return " & reg.libName & "_enc_" & n &
"(e, (const " & n & "*)v); }"
)
var respSet = respTypes
respSet.add("NimFfiStr") # ctor address payload
for n in respSet:
let tok = cToken(n)
if ("dec" & tok) notin adaptersDone:
adaptersDone.incl("dec" & tok)
lines.add(
"static inline CborError " & libName & "_decv_" & tok &
"(CborValue* it, void* v) { return " & decFn(reg, n) & "(it, (" & n & "*)v); }"
)
lines.add("")
emitEventMachinery(lines, reg, libType, libName, events)
emitContextStruct(lines, ctxType, events)
emitConstructors(lines, reg, ctxType, libType, libName, ctors)
emitDestructor(lines, ctxType, libName, classified.dtorProcName, events)
emitListenerApi(lines, ctxType, libType, libName, events)
for m in methods:
emitMethod(lines, reg, ctxType, libType, libName, m)
lines.add("#endif /* " & guard & " */")
lines.join("\n") & "\n"
proc generateCCMakeLists*(libName, nimSrcRelPath: string): string =
let src = nimSrcRelPath.replace("\\", "/")
CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src))
proc generateCBindings*(
procs: seq[FFIProcMeta],
types: seq[FFITypeMeta],
libName: string,
outputDir: string,
nimSrcRelPath: string,
events: seq[FFIEventMeta] = @[],
) =
createDir(outputDir)
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
writeFile(outputDir / CborHeaderName, generateCCborHeader())
writeFile(
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
)
writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath))