mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-02 14:19:31 +00:00
927 lines
34 KiB
Nim
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))
|