## CBOR-free C99 binding generator for the nim-ffi framework (`-d:targetLang=c_abi`). ## Where the `c` backend speaks CBOR on the wire (vendoring TinyCBOR), this one ## emits a single self-contained header whose flat structs *are* the C ABI: ## they mirror the macro-generated `*_CWire` layout byte-for-byte, so the C ## consumer passes native structs and links no CBOR at all. The Nim dylib ## converts flat struct ⇄ Nim object at the boundary (see the `abi = c` dispatch ## in `ffi/internal/c_macro_helpers.nim`) and keeps CBOR purely as an internal ## transport detail. ## ## Layout contract (must stay in lock-step with `c_macro_helpers.wireValueType` ## / `wireFieldsFor`): `string`→`const char*`, `seq[T]`→`* _items` + ## `ptrdiff_t _len`, `Option[T]`→`*` (NULL = none), nested `{.ffi.}` ## type → its flat struct, `ptr`/`pointer`→`void*`, POD unchanged. import std/[os, strutils, tables, sets] import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir const CPtrType = "void*" ## Wire C type for any Nim `ptr T` / `pointer` (mirrors the `_CWire` `pointer`). const CMakeListsTpl = staticRead("templates/c_abi/CMakeLists.txt.tpl") func leafCTypeAbi(t: string): tuple[ok: bool, cType: string] = ## Maps a Nim leaf type to the flat C type used in a wire struct. `ok` is ## false for composites (seq/Option/user structs), handled separately. 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, CPtrType) of "string", "cstring": (true, "const char*") else: (false, "") type AbiReg = object typeTable: Table[string, FFITypeMeta] ## user structs + synthetic Req structs emitted: HashSet[string] ## struct names already emitted decls: seq[string] ## struct typedefs, dependency order proc ensureAbiStruct(reg: var AbiReg, typeName: string) proc wireValueCType(reg: var AbiReg, nimType: string): string = ## Flat C type for a value-position field (everything except a top-level ## `seq`, which splits into two fields — see `fieldDecls`). let t = nimType.strip() if t.startsWith("ptr ") or t == "pointer": return CPtrType let leaf = leafCTypeAbi(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 wireValueCType(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 inside " & "another container: " & 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 fieldDecls(reg: var AbiReg, name, nimType: string): seq[string] = ## C struct member line(s) for one Nim field. A `seq[T]` becomes the ## `_items` pointer + `_len` count pair; everything else is one ## member. let seqInner = genericInnerType(nimType.strip(), "seq[") if seqInner.len > 0: let elemC = wireValueCType(reg, seqInner.strip()) return @[elemC & "* " & name & "_items;", "ptrdiff_t " & name & "_len;"] @[wireValueCType(reg, nimType) & " " & name & ";"] proc emitAbiStruct(reg: var AbiReg, t: FFITypeMeta) = var members: seq[string] = @[] for f in t.fields: for line in fieldDecls(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 reqTypeMeta(p: FFIProcMeta): FFITypeMeta = ## The per-proc Req envelope as an FFITypeMeta, mirroring the Nim macro. A ## pointer/handle param rides as the opaque `pointer` wire 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) 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 reg 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 leafCTypeAbi(nimType.strip()).ok proc reqParamsAndAssigns( reg: var AbiReg, extraParams: seq[FFIParamMeta] ): tuple[params, assigns: seq[string]] = ## The C parameter list + `ffi_req` field assignments shared by the ctor and ## method wrappers: by-value params copy straight into the request struct, ## by-const-pointer aggregates are dereferenced in. var params, assigns: seq[string] = @[] for ep in extraParams: let rides = ep.ridesAsPtr() let cType = if rides: CPtrType else: wireValueCType(reg, ep.typeName) 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 methodReplyInfo( reg: var AbiReg, libType: string, m: FFIProcMeta ): tuple[fnType, replyParam: string] = ## The reply-callback typedef name plus the C type of its `reply` argument. ## An object return hands back a `const *`; a string return a ## `const char*`. Both are the raw callback the dylib invokes directly. 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 replyParam = if rt == "string" or rt == "cstring": "const char*" elif leafCTypeAbi(rt).ok: "const " & leafCTypeAbi(rt).cType & "*" else: ensureAbiStruct(reg, rt) "const " & rt & "*" (fnType, replyParam) proc emitReplyTypedefs( lines: var seq[string], reg: var AbiReg, libType: string, methods: seq[FFIProcMeta] ) = for m in methods: let info = methodReplyInfo(reg, libType, m) lines.add( "typedef void (*" & info.fnType & ")(int err_code, " & info.replyParam & " reply, const char* err_msg, void* user_data);" ) proc emitExternDecls( 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 = methodReplyInfo(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 emitCtxAndCtor( 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(" /* 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) { 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) = reqParamsAndAssigns(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 emitDestructor(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 emitMethod( 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 = methodReplyInfo(reg, libType, m) let (params, assigns) = reqParamsAndAssigns(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 ") lines.add("#include ") lines.add("#include ") lines.add("#include ") lines.add("#include ") 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("/* Flat 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("") emitReplyTypedefs(lines, reg, libType, classified.methods) lines.add("") emitExternDecls(lines, reg, libName, libType, procs) lines.add("/* High-level context wrapper */") emitCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors) emitDestructor(lines, ctxType, libName, classified.dtorProcName) for m in classified.methods: emitMethod(lines, reg, ctxType, libName, libType, m) lines.add("#endif /* " & guard & " */") lines.join("\n") & "\n" proc generateCAbiCMakeLists*(libName, nimSrcRelPath: string): string = let src = nimSrcRelPath.replace("\\", "/") CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) proc generateCAbiBindings*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, outputDir: string, nimSrcRelPath: string, events: seq[FFIEventMeta] = @[], ) = createDir(outputDir) writeFile( outputDir / (libName & ".h"), generateCAbiLibHeader(procs, types, libName, events) ) writeFile( outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath) )