mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-16 13:09:24 +00:00
1290 lines
45 KiB
Nim
1290 lines
45 KiB
Nim
## C99 binding generator. `abi = cbor` (default) emits three CBOR headers;
|
|
## `abi = c` emits one header whose structs are the C ABI directly. Lacking
|
|
## generics, each distinct `seq[T]`/`Option[T]` is monomorphised per type.
|
|
|
|
import std/[os, strutils, tables, sets]
|
|
import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir
|
|
|
|
## Fixed 64-bit wire type for any Nim `ptr T`/`pointer` (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 header names; must match the include guards baked into the templates.
|
|
PreludeHeaderName* = "nim_ffi_prelude.h"
|
|
CborHeaderName* = "nim_ffi_cbor.h"
|
|
|
|
const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [
|
|
skBool: ("bool", "bool"),
|
|
skI8: ("int8_t", "i8"),
|
|
skI16: ("int16_t", "i16"),
|
|
skI32: ("int32_t", "i32"),
|
|
skI64: ("int64_t", "i64"),
|
|
skU8: ("uint8_t", "u8"),
|
|
skU16: ("uint16_t", "u16"),
|
|
skU32: ("uint32_t", "u32"),
|
|
skU64: ("uint64_t", "u64"),
|
|
skF32: ("float", "f32"),
|
|
skF64: ("double", "f64"),
|
|
]
|
|
|
|
func leafSuffix(cType: string): string =
|
|
## Leaf codec suffix for `cType`; "" for composites.
|
|
for s in ScalarKind:
|
|
if scalarCInfoTable[s].cType == cType:
|
|
return scalarCInfoTable[s].suffix
|
|
return
|
|
case cType
|
|
of "NimFfiStr": "str"
|
|
of "NimFfiBytes": "bytes"
|
|
else: ""
|
|
|
|
func cToken(cType: string): string =
|
|
## PascalCase token for monomorphised names.
|
|
let suffix = leafSuffix(cType)
|
|
if suffix.len > 0:
|
|
return capitalizeFirstLetter(suffix)
|
|
return cType
|
|
|
|
type CTypeReg = object
|
|
libName: string ## snake_case symbol prefix
|
|
libType: string ## PascalCase container-name prefix
|
|
typeTable: Table[string, FFITypeMeta]
|
|
emitted: HashSet[string]
|
|
owns: Table[string, bool] ## C type name → owns-heap-memory
|
|
decls: seq[string]
|
|
codecs: seq[string]
|
|
|
|
func encFn(reg: CTypeReg, cType: string): string =
|
|
let suffix = leafSuffix(cType)
|
|
if suffix.len > 0:
|
|
return "nimffi_enc_" & suffix
|
|
return reg.libName & "_enc_" & cType
|
|
|
|
func decFn(reg: CTypeReg, cType: string): string =
|
|
let suffix = leafSuffix(cType)
|
|
if suffix.len > 0:
|
|
return "nimffi_dec_" & suffix
|
|
return reg.libName & "_dec_" & cType
|
|
|
|
func freeFn(reg: CTypeReg, cType: string): string =
|
|
## Free-function name for `cType`, or "" when it owns no heap memory.
|
|
return
|
|
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, t: FFIType): tuple[cType: string, owns: bool] =
|
|
## Lowers an `FFIType` to a C type, monomorphising each `seq[T]`/`Option[T]`
|
|
## on first sight. `owns` marks a type the caller must free.
|
|
case t.kind
|
|
of ftPtr:
|
|
return (CPtrType, false)
|
|
of ftScalar:
|
|
return (scalarCInfoTable[t.scalar].cType, false)
|
|
of ftStr:
|
|
return ("NimFfiStr", true)
|
|
of ftBytes:
|
|
return ("NimFfiBytes", true)
|
|
of ftSeq:
|
|
let (elemC, _) = ensureCType(reg, t.elem)
|
|
let name = reg.libType & "Seq_" & cToken(elemC)
|
|
if name notin reg.emitted:
|
|
reg.emitted.incl(name)
|
|
emitSeqType(reg, name, elemC)
|
|
return (name, true)
|
|
of ftOpt:
|
|
let (elemC, elemOwns) = ensureCType(reg, t.elem)
|
|
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))
|
|
of ftStruct:
|
|
let name = t.name
|
|
if name notin reg.emitted:
|
|
reg.emitted.incl(name)
|
|
if name in reg.typeTable:
|
|
emitStructType(reg, reg.typeTable[name])
|
|
else:
|
|
reg.decls.add("/* unknown type referenced: " & name & " */")
|
|
return (name, reg.owns.getOrDefault(name, false))
|
|
|
|
proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] =
|
|
return ensureCType(reg, parseFFIType(nimType))
|
|
|
|
proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta =
|
|
## Synthesises the per-proc Req struct; pointer/handle params ride as uint64.
|
|
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))
|
|
return FFITypeMeta(name: reqStructName(p), fields: fields)
|
|
|
|
func paramByValue(nimType: string, ridesAsPtr: bool): bool =
|
|
## Scalars/pointers/string views pass by value; aggregates by const pointer.
|
|
if ridesAsPtr:
|
|
return true
|
|
return parseFFIType(nimType).kind in {ftScalar, ftStr, ftPtr}
|
|
|
|
proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string =
|
|
if p.returnRidesAsPtr():
|
|
return CPtrType
|
|
return 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 & ";")
|
|
return (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)
|
|
return (
|
|
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: recover the box, fail if no callback, deliver a
|
|
## non-zero `ret` as an error (msg/len isn't 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(
|
|
" /* Non-terminal progress ping: keep the box for the terminal reply. */"
|
|
)
|
|
lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;")
|
|
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);")
|
|
# Reclaim fields a partial decode allocated (out is zeroed).
|
|
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
|
|
return reg
|
|
|
|
proc monomorphiseAll(
|
|
reg: var CTypeReg,
|
|
types: seq[FFITypeMeta],
|
|
procs, methods: seq[FFIProcMeta],
|
|
events: seq[FFIEventMeta],
|
|
): tuple[reqTypes, respTypes: seq[string]] =
|
|
## Runs every type, Req, return type and event payload through ensureCType,
|
|
## returning 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)
|
|
return (reqTypes, respTypes)
|
|
|
|
func generateCPreludeHeader*(): string =
|
|
## The library-agnostic `nim_ffi_prelude.h`, emitted verbatim.
|
|
return HeaderPreludeTpl & "\n"
|
|
|
|
func generateCCborHeader*(): string =
|
|
## The library-agnostic `nim_ffi_cbor.h`, emitted verbatim.
|
|
return CborHelpersTpl & "\n"
|
|
|
|
proc generateCLibHeader*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
events: seq[FFIEventMeta] = @[],
|
|
): string =
|
|
## The `<lib>.h` header: library structs, monomorphised codecs and async API.
|
|
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 & " */")
|
|
return lines.join("\n") & "\n"
|
|
|
|
proc generateCCMakeLists*(libName, nimSrcRelPath: string): string =
|
|
let src = nimSrcRelPath.replace("\\", "/")
|
|
return CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src))
|
|
|
|
# `abi = c` binding: structs are the C ABI directly (no CBOR), matching the Nim-side wire layout byte-for-byte.
|
|
|
|
const AbiCPtrType = "void*"
|
|
const AbiCMakeListsTpl = staticRead("templates/c/CMakeLists_abi.txt.tpl")
|
|
|
|
func abiLeafCType(t: string): tuple[ok: bool, cType: string] =
|
|
## Nim leaf type → `abi = c` wire C type; `ok` is false for composites.
|
|
return
|
|
case t
|
|
of "int", "int64":
|
|
(true, "int64_t")
|
|
of "int32":
|
|
(true, "int32_t")
|
|
of "int16":
|
|
(true, "int16_t")
|
|
of "int8":
|
|
(true, "int8_t")
|
|
of "uint", "uint64":
|
|
(true, "uint64_t")
|
|
of "uint32":
|
|
(true, "uint32_t")
|
|
of "uint16":
|
|
(true, "uint16_t")
|
|
of "uint8", "byte":
|
|
(true, "uint8_t")
|
|
of "bool":
|
|
(true, "bool")
|
|
of "float", "float64":
|
|
(true, "double")
|
|
of "float32":
|
|
(true, "float")
|
|
of "pointer":
|
|
(true, AbiCPtrType)
|
|
of "string", "cstring":
|
|
(true, "const char*")
|
|
else:
|
|
(false, "")
|
|
|
|
type AbiReg = object
|
|
typeTable: Table[string, FFITypeMeta]
|
|
emitted: HashSet[string]
|
|
decls: seq[string]
|
|
|
|
proc ensureAbiStruct(reg: var AbiReg, typeName: string)
|
|
|
|
proc abiWireValueCType(reg: var AbiReg, nimType: string): string =
|
|
## `abi = c` C type for a value-position field (a top-level `seq` splits in two).
|
|
let t = nimType.strip()
|
|
if t.startsWith("ptr ") or t == "pointer":
|
|
return AbiCPtrType
|
|
let leaf = abiLeafCType(t)
|
|
if leaf.ok:
|
|
return leaf.cType
|
|
var optInner = genericInnerType(t, "Option[")
|
|
if optInner.len == 0:
|
|
optInner = genericInnerType(t, "Maybe[")
|
|
if optInner.len > 0:
|
|
return abiWireValueCType(reg, optInner.strip()) & "*"
|
|
if genericInnerType(t, "seq[").len > 0:
|
|
raise newException(
|
|
ValueError, "abi = c: `seq` has no single-field wire form, so it can't nest: " & t
|
|
)
|
|
if genericInnerType(t, "array[").len > 0:
|
|
raise newException(
|
|
ValueError, "abi = c: array fields are not yet supported by the C backend: " & t
|
|
)
|
|
if t in reg.typeTable:
|
|
ensureAbiStruct(reg, t)
|
|
return t
|
|
raise newException(ValueError, "abi = c: unknown field type: " & t)
|
|
|
|
proc abiFieldDecls(reg: var AbiReg, name, nimType: string): seq[string] =
|
|
let seqInner = genericInnerType(nimType.strip(), "seq[")
|
|
if seqInner.len > 0:
|
|
let elemC = abiWireValueCType(reg, seqInner.strip())
|
|
return @[elemC & "* " & name & "_items;", "ptrdiff_t " & name & "_len;"]
|
|
return @[abiWireValueCType(reg, nimType) & " " & name & ";"]
|
|
|
|
proc emitAbiStruct(reg: var AbiReg, t: FFITypeMeta) =
|
|
var members: seq[string] = @[]
|
|
for f in t.fields:
|
|
for line in abiFieldDecls(reg, f.name, f.typeName):
|
|
members.add(" " & line)
|
|
if members.len == 0:
|
|
members.add(" uint8_t _placeholder; /* C forbids empty structs */")
|
|
reg.decls.add("typedef struct {\n" & members.join("\n") & "\n} " & t.name & ";")
|
|
|
|
proc ensureAbiStruct(reg: var AbiReg, typeName: string) =
|
|
if typeName in reg.emitted:
|
|
return
|
|
reg.emitted.incl(typeName)
|
|
if typeName in reg.typeTable:
|
|
emitAbiStruct(reg, reg.typeTable[typeName])
|
|
else:
|
|
reg.decls.add("/* unknown type referenced: " & typeName & " */")
|
|
|
|
proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg =
|
|
var reg = AbiReg()
|
|
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
|
|
return reg
|
|
|
|
func abiParamByValue(nimType: string, ridesAsPtr: bool): bool =
|
|
## Scalars/pointers/string views pass by value; aggregates by const pointer.
|
|
if ridesAsPtr:
|
|
return true
|
|
return abiLeafCType(nimType.strip()).ok
|
|
|
|
proc abiReqParamsAndAssigns(
|
|
reg: var AbiReg, extraParams: seq[FFIParamMeta]
|
|
): tuple[params, assigns: seq[string]] =
|
|
var params, assigns: seq[string] = @[]
|
|
for ep in extraParams:
|
|
let rides = ep.ridesAsPtr()
|
|
let cType =
|
|
if rides:
|
|
AbiCPtrType
|
|
else:
|
|
abiWireValueCType(reg, ep.typeName)
|
|
if abiParamByValue(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 & ";")
|
|
return (params, assigns)
|
|
|
|
proc abiMethodReplyInfo(
|
|
reg: var AbiReg, libType: string, m: FFIProcMeta
|
|
): tuple[fnType, replyParam: string] =
|
|
## Reply-callback typedef name plus the C type of its `reply` argument.
|
|
let pascal = snakeToPascalCase(stripLibPrefix(m.procName, m.libName))
|
|
let fnType = libType & pascal & "ReplyFn"
|
|
if m.returnRidesAsPtr():
|
|
raise newException(
|
|
ValueError,
|
|
"abi = c: handle/pointer returns are not yet supported by the C backend: " &
|
|
m.procName,
|
|
)
|
|
let rt = m.returnTypeName.strip()
|
|
let leaf = abiLeafCType(rt)
|
|
let replyParam =
|
|
if rt == "string" or rt == "cstring":
|
|
"const char*"
|
|
elif leaf.ok:
|
|
"const " & leaf.cType & "*"
|
|
else:
|
|
ensureAbiStruct(reg, rt)
|
|
"const " & rt & "*"
|
|
return (fnType, replyParam)
|
|
|
|
proc emitAbiReplyTypedefs(
|
|
lines: var seq[string], reg: var AbiReg, libType: string, methods: seq[FFIProcMeta]
|
|
) =
|
|
for m in methods:
|
|
let info = abiMethodReplyInfo(reg, libType, m)
|
|
lines.add(
|
|
"typedef void (*" & info.fnType & ")(int err_code, " & info.replyParam &
|
|
" reply, const char* err_msg, void* user_data);"
|
|
)
|
|
|
|
proc emitAbiExternDecls(
|
|
lines: var seq[string],
|
|
reg: var AbiReg,
|
|
libName, libType: string,
|
|
procs: seq[FFIProcMeta],
|
|
) =
|
|
let createRawFn = libType & "CreateRawFn"
|
|
var haveCtor = false
|
|
for p in procs:
|
|
if p.kind == FFIKind.CTOR:
|
|
haveCtor = true
|
|
if haveCtor:
|
|
lines.add(
|
|
"typedef void (*" & createRawFn &
|
|
")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);"
|
|
)
|
|
lines.add("#ifdef __cplusplus")
|
|
lines.add("extern \"C\" {")
|
|
lines.add("#endif")
|
|
lines.add("")
|
|
for p in procs:
|
|
let reqStruct = reqStructName(p)
|
|
case p.kind
|
|
of FFIKind.FFI:
|
|
let info = abiMethodReplyInfo(reg, libType, p)
|
|
lines.add(
|
|
"int " & p.procName & "(void* ctx, " & info.fnType &
|
|
" on_reply, void* user_data, const " & reqStruct & "* req);"
|
|
)
|
|
of FFIKind.CTOR:
|
|
lines.add(
|
|
"void* " & p.procName & "(const " & reqStruct & "* req, " & createRawFn &
|
|
" on_created, void* user_data);"
|
|
)
|
|
of FFIKind.DTOR:
|
|
lines.add("int " & p.procName & "(void* ctx);")
|
|
lines.add("")
|
|
lines.add("#ifdef __cplusplus")
|
|
lines.add("} /* extern \"C\" */")
|
|
lines.add("#endif")
|
|
lines.add("")
|
|
|
|
proc emitAbiCtxAndCtor(
|
|
lines: var seq[string],
|
|
reg: var AbiReg,
|
|
libName, libType, ctxType: string,
|
|
ctors: seq[FFIProcMeta],
|
|
) =
|
|
lines.add("typedef struct {")
|
|
lines.add(" void* ptr;")
|
|
lines.add("} " & ctxType & ";")
|
|
lines.add("")
|
|
if ctors.len == 0:
|
|
return
|
|
let createFn = libType & "CreateFn"
|
|
let createBox = libType & "CreateBox"
|
|
let createRawFn = libType & "CreateRawFn"
|
|
let tramp = libName & "_create_trampoline"
|
|
lines.add(
|
|
"typedef void (*" & createFn & ")(int err_code, " & ctxType &
|
|
"* ctx, const char* err_msg, void* user_data);"
|
|
)
|
|
lines.add(
|
|
"typedef struct { " & createFn & " fn; void* user_data; } " & createBox & ";"
|
|
)
|
|
lines.add(
|
|
"static void " & tramp &
|
|
"(int ret, const char* ctx_addr, const char* err_msg, void* ud) {"
|
|
)
|
|
lines.add(" " & createBox & "* box = (" & createBox & "*)ud;")
|
|
lines.add(" if (!box) return;")
|
|
lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;")
|
|
lines.add(" if (!box->fn) { free(box); return; }")
|
|
lines.add(" if (ret != 0) {")
|
|
lines.add(
|
|
" box->fn(ret, NULL, err_msg ? err_msg : \"FFI create failed\", box->user_data);"
|
|
)
|
|
lines.add(" free(box);")
|
|
lines.add(" return;")
|
|
lines.add(" }")
|
|
lines.add(" char* endp = NULL;")
|
|
lines.add(" unsigned long long a = ctx_addr ? strtoull(ctx_addr, &endp, 10) : 0;")
|
|
lines.add(" bool ok = ctx_addr && *ctx_addr && endp && *endp == '\\0';")
|
|
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 reqStruct = reqStructName(ctor)
|
|
let (params, assigns) = abiReqParamsAndAssigns(reg, ctor.extraParams)
|
|
let head = "static inline int " & libName & "_ctx_create("
|
|
let sig =
|
|
if params.len > 0:
|
|
head & params.join(", ") & ", " & createFn & " on_created, void* user_data) {"
|
|
else:
|
|
head & createFn & " on_created, void* user_data) {"
|
|
lines.add(sig)
|
|
lines.add(" " & reqStruct & " ffi_req;")
|
|
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
|
for a in assigns:
|
|
lines.add(a)
|
|
lines.add(
|
|
" " & createBox & "* box = (" & createBox & "*)malloc(sizeof(" & createBox &
|
|
"));"
|
|
)
|
|
lines.add(" if (!box) {")
|
|
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 & "(&ffi_req, " & tramp & ", box);")
|
|
lines.add(" return 0;")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
proc emitAbiDestructor(lines: var seq[string], ctxType, libName, dtorProcName: string) =
|
|
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; }")
|
|
lines.add(" free(ctx);")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
proc emitAbiMethod(
|
|
lines: var seq[string],
|
|
reg: var AbiReg,
|
|
ctxType, libName, libType: string,
|
|
m: FFIProcMeta,
|
|
) =
|
|
let stripped = stripLibPrefix(m.procName, m.libName)
|
|
let reqStruct = reqStructName(m)
|
|
let info = abiMethodReplyInfo(reg, libType, m)
|
|
let (params, assigns) = abiReqParamsAndAssigns(reg, m.extraParams)
|
|
let head =
|
|
"static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, "
|
|
let sig =
|
|
if params.len > 0:
|
|
head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {"
|
|
else:
|
|
head & info.fnType & " on_reply, void* user_data) {"
|
|
lines.add(sig)
|
|
lines.add(" " & reqStruct & " ffi_req;")
|
|
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
|
for a in assigns:
|
|
lines.add(a)
|
|
lines.add(" return " & m.procName & "(ctx->ptr, on_reply, user_data, &ffi_req);")
|
|
lines.add("}")
|
|
lines.add("")
|
|
|
|
proc generateCAbiLibHeader*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
events: seq[FFIEventMeta] = @[],
|
|
): string =
|
|
if events.len > 0:
|
|
raise newException(
|
|
ValueError, "abi = c: the C backend does not yet support {.ffiEvent.} listeners"
|
|
)
|
|
let classified = classifyProcs(procs)
|
|
let libType = libTypeName(classified.ctors, libName)
|
|
let ctxType = libType & "Ctx"
|
|
|
|
var reg = newAbiReg(types, procs)
|
|
for t in types:
|
|
ensureAbiStruct(reg, t.name)
|
|
for p in procs:
|
|
if p.kind != FFIKind.DTOR:
|
|
ensureAbiStruct(reg, reqStructName(p))
|
|
|
|
let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED"
|
|
var lines: seq[string] = @[]
|
|
lines.add("#ifndef " & guard)
|
|
lines.add("#define " & guard)
|
|
lines.add("#include <stdint.h>")
|
|
lines.add("#include <stddef.h>")
|
|
lines.add("#include <stdbool.h>")
|
|
lines.add("#include <stdlib.h>")
|
|
lines.add("#include <string.h>")
|
|
lines.add("")
|
|
lines.add("#define NIMFFI_RET_OK 0")
|
|
lines.add("#define NIMFFI_RET_ERR 1")
|
|
lines.add("#define NIMFFI_RET_MISSING_CALLBACK 2")
|
|
lines.add("/* Non-terminal: the request is still running. Fires every ~5s with `msg`")
|
|
lines.add(
|
|
" carrying the elapsed milliseconds as decimal text; always followed by a"
|
|
)
|
|
lines.add(" terminal RET_OK/RET_ERR. Ignore it unless you want progress. */")
|
|
lines.add("#define NIMFFI_RET_STALE_WARN 3")
|
|
lines.add("")
|
|
lines.add(
|
|
"/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated"
|
|
)
|
|
lines.add(" `const char*` valid only for the duration of the call they cross. */")
|
|
for decl in reg.decls:
|
|
lines.add(decl)
|
|
lines.add("")
|
|
|
|
emitAbiReplyTypedefs(lines, reg, libType, classified.methods)
|
|
lines.add("")
|
|
emitAbiExternDecls(lines, reg, libName, libType, procs)
|
|
|
|
lines.add("/* High-level context wrapper */")
|
|
emitAbiCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors)
|
|
emitAbiDestructor(lines, ctxType, libName, classified.dtorProcName)
|
|
for m in classified.methods:
|
|
emitAbiMethod(lines, reg, ctxType, libName, libType, m)
|
|
|
|
lines.add("#endif /* " & guard & " */")
|
|
return lines.join("\n") & "\n"
|
|
|
|
proc generateCAbiCMakeLists*(libName, nimSrcRelPath: string): string =
|
|
let src = nimSrcRelPath.replace("\\", "/")
|
|
return AbiCMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src))
|
|
|
|
func libWireFormat(procs: seq[FFIProcMeta], types: seq[FFITypeMeta]): ABIFormat =
|
|
## The single wire format the C header targets (no mixing in one header).
|
|
var seen: set[ABIFormat] = {}
|
|
for p in procs:
|
|
if p.kind != FFIKind.DTOR:
|
|
seen.incl(p.abiFormat)
|
|
if seen.len == 0:
|
|
for t in types:
|
|
seen.incl(t.abiFormat)
|
|
if seen.len > 1:
|
|
raise newException(
|
|
ValueError,
|
|
"abi = c/cbor mismatch: a C library must use one ABI format for all its " &
|
|
"procs and types; a mixed header is not supported",
|
|
)
|
|
return (if ABIFormat.C in seen: ABIFormat.C else: ABIFormat.Cbor)
|
|
|
|
proc generateCBindings*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
outputDir: string,
|
|
nimSrcRelPath: string,
|
|
events: seq[FFIEventMeta] = @[],
|
|
) =
|
|
## Emits the C binding for `libName`, picking the `abi = c` or CBOR shape.
|
|
createDir(outputDir)
|
|
case libWireFormat(procs, types)
|
|
of ABIFormat.C:
|
|
writeFile(
|
|
outputDir / (libName & ".h"), generateCAbiLibHeader(procs, types, libName, events)
|
|
)
|
|
writeFile(
|
|
outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath)
|
|
)
|
|
of ABIFormat.Cbor:
|
|
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
|
|
writeFile(outputDir / CborHeaderName, generateCCborHeader())
|
|
writeFile(
|
|
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
|
|
)
|
|
writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath))
|