## C binding generator for the nim-ffi framework. ## ## Emits a single self-contained header `.h` that hides the CBOR transport ## behind ergonomic, typed C functions. nim-ffi 0.2.0's exported ABI takes the ## request as one CBOR buffer; the generated header restores a natural call ## shape — `_(ctx, callback, userData, )` — by ## CBOR-encoding the arguments and forwarding to the real export. ## ## The real export and the ergonomic wrapper share the same C name, so the raw ## symbol is reached through an `__asm__` label (with a platform underscore ## shim) and the inline wrapper keeps the public name. This keeps existing C / ## cgo / JNI / Swift consumers calling the same symbols they always did, just ## with typed arguments instead of a hand-built CBOR blob. ## ## Requests/responses travel as CBOR (RFC 8949), matching the Nim-side ## cbor_serial codec: each request is a map keyed by the Nim parameter names; ## each success response is the CBOR-encoded return value (a text string for ## the common `Result[string, string]` shape). import std/[os, strutils] import ./meta, ./string_helpers ## Fixed 64-bit wire type for any Nim `ptr T` / `pointer`, matching the C++/Rust ## generators so the CBOR payload size is architecture-stable. const CPtrType = "uint64_t" proc nimTypeToC(typeName: string): string = ## Maps a Nim parameter/field type to the C type used in the ergonomic ## wrapper signature. let t = typeName.strip() if t.startsWith("ptr ") or t == "pointer": return "void*" case t of "string", "cstring": "const char*" of "int", "int64", "clong": "int64_t" of "int32", "cint": "int32_t" of "int16": "int16_t" of "int8": "int8_t" of "uint", "uint64", "csize_t": "uint64_t" of "uint32", "cuint": "uint32_t" of "uint16": "uint16_t" of "uint8", "byte": "uint8_t" of "bool": "int" of "float", "float32": "float" of "float64": "double" else: t type CborKind = enum ckText ckUint ckInt ckBool ckUnsupported proc cborKindOf(typeName: string): CborKind = ## How a parameter of `typeName` is encoded into the request map. Only the ## scalar/string shapes the FFI boundary actually carries are handled; nested ## {.ffi.} struct params would need recursive map emission (not used by the ## libwaku / liblogosdelivery surface and intentionally rejected here). let t = typeName.strip() case t of "string", "cstring": ckText of "uint", "uint8", "uint16", "uint32", "uint64", "cuint", "csize_t", "byte": ckUint of "int", "int8", "int16", "int32", "int64", "cint", "clong": ckInt of "bool": ckBool else: ckUnsupported const HeaderPrelude = """ // Generated by nim-ffi C codegen. Do not edit by hand. // // Each function below CBOR-encodes its arguments and forwards to the matching // nim-ffi export. The asynchronous result is delivered to `callback`: // - RET_OK : `msg`/`len` is the CBOR-encoded return value. For functions that // return a string this is a CBOR text string; decode it with // _decode_text(). An empty success encodes as 0xF6 (null) or // an empty text string. // - RET_ERR: `msg`/`len` is the raw error text (not CBOR-encoded). // // Library-initiated events delivered to a listener registered via // _add_event_listener are raw JSON strings (not CBOR). #ifndef NIM_FFI_GEN__H #define NIM_FFI_GEN__H #include #include #include #include #ifdef __cplusplus extern "C" { #endif #ifndef NIM_FFI_RET_CODES #define NIM_FFI_RET_CODES #define RET_OK 0 #define RET_ERR 1 #define RET_MISSING_CALLBACK 2 #endif typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); // --- minimal growable CBOR encoder ---------------------------------------- typedef struct { uint8_t *buf; size_t cap; size_t len; } FfiCbor; static inline FfiCbor ffi_cbor_new(void) { FfiCbor c; c.cap = 256; c.len = 0; c.buf = (uint8_t *)malloc(c.cap); return c; } static inline void ffi_cbor_free(FfiCbor *c) { free(c->buf); c->buf = NULL; } static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) { if (c->len >= c->cap) { c->cap *= 2; c->buf = (uint8_t *)realloc(c->buf, c->cap); } c->buf[c->len++] = b; } // CBOR head: (major << 5) | smallest-fitting length encoding of `arg`. static inline void ffi_cbor_head(FfiCbor *c, uint8_t major, uint64_t arg) { uint8_t mt = (uint8_t)(major << 5); if (arg < 24) { ffi_cbor_put(c, mt | (uint8_t)arg); } else if (arg <= 0xff) { ffi_cbor_put(c, mt | 24); ffi_cbor_put(c, (uint8_t)arg); } else if (arg <= 0xffff) { ffi_cbor_put(c, mt | 25); ffi_cbor_put(c, (uint8_t)(arg >> 8)); ffi_cbor_put(c, (uint8_t)arg); } else if (arg <= 0xffffffffULL) { ffi_cbor_put(c, mt | 26); ffi_cbor_put(c, (uint8_t)(arg >> 24)); ffi_cbor_put(c, (uint8_t)(arg >> 16)); ffi_cbor_put(c, (uint8_t)(arg >> 8)); ffi_cbor_put(c, (uint8_t)arg); } else { ffi_cbor_put(c, mt | 27); for (int s = 56; s >= 0; s -= 8) ffi_cbor_put(c, (uint8_t)(arg >> s)); } } static inline void ffi_cbor_map(FfiCbor *c, size_t n) { ffi_cbor_head(c, 5, n); } static inline void ffi_cbor_text(FfiCbor *c, const char *s) { size_t n = s ? strlen(s) : 0; ffi_cbor_head(c, 3, n); for (size_t i = 0; i < n; i++) ffi_cbor_put(c, (uint8_t)s[i]); } static inline void ffi_cbor_kv_text(FfiCbor *c, const char *k, const char *v) { ffi_cbor_text(c, k); ffi_cbor_text(c, v); } static inline void ffi_cbor_kv_uint(FfiCbor *c, const char *k, uint64_t v) { ffi_cbor_text(c, k); ffi_cbor_head(c, 0, v); } static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_t v) { ffi_cbor_text(c, k); if (v >= 0) ffi_cbor_head(c, 0, (uint64_t)v); else ffi_cbor_head(c, 1, (uint64_t)(-(v + 1))); } static inline void ffi_cbor_kv_bool(FfiCbor *c, const char *k, int v) { ffi_cbor_text(c, k); ffi_cbor_put(c, v ? 0xf5 : 0xf4); } // --- response decoding ----------------------------------------------------- // Zero-copy view of a top-level CBOR text string (the RET_OK payload of a // string-returning function). On success sets *out/*outLen to point INTO `data` // (no allocation) and returns 1; returns 0 for a non-text-string payload (e.g. // the 0xF6 empty-success sentinel). The view borrows `data`, so it is only valid // for as long as `data` is (i.e. the duration of the callback) and is NOT // NUL-terminated — print it with "%.*s", (int)outLen, out. static inline int ffi_text_view(const uint8_t *data, size_t len, const uint8_t **out, size_t *outLen) { if (len < 1 || (data[0] >> 5) != 3) return 0; uint8_t info = data[0] & 0x1f; size_t p = 1; uint64_t slen = 0; if (info < 24) { slen = info; } else if (info == 24) { if (len < p + 1) return 0; slen = data[p++]; } else if (info == 25) { if (len < p + 2) return 0; slen = ((uint64_t)data[p] << 8) | data[p + 1]; p += 2; } else if (info == 26) { if (len < p + 4) return 0; slen = ((uint64_t)data[p] << 24) | ((uint64_t)data[p + 1] << 16) | ((uint64_t)data[p + 2] << 8) | data[p + 3]; p += 4; } else { return 0; } if (len < p + slen) return 0; *out = data + p; *outLen = (size_t)slen; return 1; } // Owning variant: decode a top-level CBOR text string into a freshly malloc'd, // NUL-terminated C string. Returns NULL for a non-text-string payload. The // caller owns the returned pointer and must free() it. Use this when the value // must outlive the callback; otherwise prefer the zero-copy ffi_text_view above. static inline char *ffi_decode_text(const uint8_t *data, size_t len) { const uint8_t *view; size_t slen; if (!ffi_text_view(data, len, &view, &slen)) return NULL; char *out = (char *)malloc(slen + 1); if (!out) return NULL; memcpy(out, view, slen); out[slen] = '\0'; return out; } // Reach the real exported symbol (which shares the wrapper's name) through an // assembler label. macOS prefixes C symbols with an underscore; Linux does not. #if defined(__APPLE__) #define NIM_FFI_SYM(name) "_" name #else #define NIM_FFI_SYM(name) name #endif """ proc emitRequestBuild(lines: var seq[string], p: FFIProcMeta, indent: string): bool = ## Emits the CBOR request-map construction for proc `p` into `ffi_c_`. ## Returns false if any parameter type is unsupported by the C generator. lines.add(indent & "FfiCbor ffi_c_ = ffi_cbor_new();") lines.add(indent & "ffi_cbor_map(&ffi_c_, " & $p.extraParams.len & ");") for ep in p.extraParams: if ep.isPtr: return false case cborKindOf(ep.typeName) of ckText: lines.add( indent & "ffi_cbor_kv_text(&ffi_c_, \"" & ep.name & "\", " & ep.name & ");" ) of ckUint: lines.add( indent & "ffi_cbor_kv_uint(&ffi_c_, \"" & ep.name & "\", (uint64_t)" & ep.name & ");" ) of ckInt: lines.add( indent & "ffi_cbor_kv_int(&ffi_c_, \"" & ep.name & "\", (int64_t)" & ep.name & ");" ) of ckBool: lines.add( indent & "ffi_cbor_kv_bool(&ffi_c_, \"" & ep.name & "\", " & ep.name & ");" ) of ckUnsupported: return false return true proc paramSig(p: FFIProcMeta): string = ## Comma-separated typed parameter list (no leading comma) for the wrapper. var parts: seq[string] = @[] for ep in p.extraParams: let cType = if ep.isPtr: CPtrType else: nimTypeToC(ep.typeName) parts.add(cType & " " & ep.name) return parts.join(", ") proc allParamsSupported(p: FFIProcMeta): bool = ## The C generator only produces ergonomic wrappers when every parameter is a ## scalar or string. Struct / seq / Option params (which C cannot express ## naturally) fall back to the raw CBOR-buffer ABI declaration instead. for ep in p.extraParams: if ep.isPtr or cborKindOf(ep.typeName) == ckUnsupported: return false return true proc generateCHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, events: seq[FFIEventMeta] = @[], ): string = let guard = libName.toUpper() let upperLib = libName.toUpper() var lines: seq[string] = @[] lines.add(HeaderPrelude.replace("", guard).replace("", upperLib)) lines.add("") for p in procs: case p.kind of FFIKind.DTOR: # No request payload — declare the real export directly. lines.add("int " & p.procName & "(void *ctx);") lines.add("") of FFIKind.CTOR: if allParamsSupported(p): let sig = paramSig(p) # void* (, FFICallBack callback, void *userData) lines.add( "extern void *" & p.procName & "__ffi(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData) __asm__(NIM_FFI_SYM(\"" & p.procName & "\"));" ) let ctorHead = "void *" & p.procName & "(" & sig & (if sig.len > 0: ", " else: "") & "FFICallBack callback, void *userData)" # Rename the wrapper's own symbol so the raw export's asm alias above # (which targets the unmangled export name) does not bind to this local # static definition — otherwise the wrapper would call itself. lines.add( "static inline " & ctorHead & " __asm__(\"" & p.procName & "__wrap\");" ) lines.add("static inline " & ctorHead & " {") discard emitRequestBuild(lines, p, " ") lines.add( " void *ffi_r_ = " & p.procName & "__ffi(ffi_c_.buf, ffi_c_.len, callback, userData);" ) lines.add(" ffi_cbor_free(&ffi_c_);") lines.add(" return ffi_r_;") lines.add("}") else: # Non-scalar params: expose the raw CBOR-buffer ABI; the caller builds # the request map itself (the FfiCbor helpers above are still usable). lines.add( "// " & p.procName & ": has struct/seq/Option params; raw CBOR ABI only (build the request map yourself)." ) lines.add( "void *" & p.procName & "(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);" ) lines.add("") of FFIKind.FFI: if allParamsSupported(p): let sig = paramSig(p) let sigComma = if sig.len > 0: ", " & sig else: "" lines.add( "extern int " & p.procName & "__ffi(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen) __asm__(NIM_FFI_SYM(\"" & p.procName & "\"));" ) let ffiHead = "int " & p.procName & "(void *ctx, FFICallBack callback, void *userData" & sigComma & ")" # Rename the wrapper's own symbol so the raw export's asm alias above # (which targets the unmangled export name) does not bind to this local # static definition — otherwise the wrapper would call itself. lines.add( "static inline " & ffiHead & " __asm__(\"" & p.procName & "__wrap\");" ) lines.add("static inline " & ffiHead & " {") discard emitRequestBuild(lines, p, " ") lines.add( " int ffi_r_ = " & p.procName & "__ffi(ctx, callback, userData, ffi_c_.buf, ffi_c_.len);" ) lines.add(" ffi_cbor_free(&ffi_c_);") lines.add(" return ffi_r_;") lines.add("}") else: lines.add( "// " & p.procName & ": has struct/seq/Option params; raw CBOR ABI only (build the request map yourself)." ) lines.add( "int " & p.procName & "(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);" ) lines.add("") # `declareLibrary` always exports the listener-registration ABI. lines.add( "uint64_t " & libName & "_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);" ) lines.add( "int " & libName & "_remove_event_listener(void *ctx, uint64_t listenerId);" ) lines.add("") lines.add("#ifdef __cplusplus") lines.add("} // extern \"C\"") lines.add("#endif") lines.add("") lines.add("#endif /* NIM_FFI_GEN_" & guard & "_H */") return lines.join("\n") proc generateCBindings*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, outputDir: string, nimSrcRelPath: string, events: seq[FFIEventMeta] = @[], ) = writeFile( outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events) )