## C binding generator for the nim-ffi framework. ## ## Each `{.ffi.}` / `{.ffiCtor.}` proc is exported twice — a native typed-arg ## entry point under `` (no CBOR, result delivered raw) and a `_cbor` ## variant (CBOR request/response) — so this generator emits BOTH headers, side ## by side, and the consumer chooses per call site: ## ## - `.h` — native ABI: `int (ctx, cb, ud, )`, ## ctor `void *(, cb, ud)`, dtor `int (ctx)`. The ## callback gets the raw result (RET_OK; not NUL-terminated, use len) or raw ## error text (RET_ERR). Best for SAME-process callers (zero serialization). ## - `_cbor.h` — CBOR ABI: `int _cbor(ctx, cb, ud, reqCbor, len)`, ## ctor `void *_cbor(reqCbor, len, cb, ud)`, plus a small CBOR request ## encoder (FfiCbor) and response decoder (ffi_decode_text). The request is a ## CBOR map keyed by the Nim param names (noted per proc). Best for callers ## that cross a process / machine boundary, where serialization is required ## anyway, or any generic / cross-language caller. ## ## Both headers guard their shared definitions (RET codes, FFICallBack) so a ## translation unit may include both. Library-initiated events (registered via ## `_add_event_listener`) arrive as raw JSON in either case. import std/[os, strutils] import ./meta, ./string_helpers ## Fixed 64-bit C type for any Nim `ptr T` / `pointer`, matching the other ## generators so the layout is architecture-stable. const CPtrType = "uint64_t" proc nimTypeToC(typeName: string): string = ## Maps a Nim parameter type to the C type used in the declaration. 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 proc cParam(ep: FFIParamMeta): string = (if ep.isPtr: CPtrType else: nimTypeToC(ep.typeName)) & " " & ep.name proc typedArgs(p: FFIProcMeta): string = var parts: seq[string] = @[] for ep in p.extraParams: parts.add(cParam(ep)) return parts.join(", ") proc innerOf(t, prefix: string): string = ## Strips a `Prefix[...]` wrapper, returning the inner type name. t[prefix.len .. ^2] proc emitCStructs(types: seq[FFITypeMeta]): seq[string] = ## Emits a `typedef struct { ... } ;` for every `{.ffi.}` type so the ## native header is self-contained (no undefined `TimerConfig` / `EchoRequest` ## references). Field layout mirrors the Nim object so the struct can be passed ## by value to the native entry points: ## - scalar / bool / float / nested `{.ffi.}` struct -> the matching C type ## - `cstring` (use this, not `string`, for native ABI) -> `const char*` ## - `seq[T]` -> `{ const T* ; size_t _len; }` ## - `Option[T]`/`Maybe[T]` -> `{ int _present; T ; }` var lines: seq[string] = @[] if types.len > 0: lines.add("// --- {.ffi.}-annotated types, exposed as C structs ----------") for t in types: lines.add("typedef struct {") for f in t.fields: let ft = f.typeName.strip() if ft.startsWith("seq[") and ft.endsWith("]"): lines.add(" " & nimTypeToC(innerOf(ft, "seq[")) & " *" & f.name & ";") lines.add(" size_t " & f.name & "_len;") elif ft.startsWith("Option[") and ft.endsWith("]"): lines.add(" int " & f.name & "_present;") lines.add(" " & nimTypeToC(innerOf(ft, "Option[")) & " " & f.name & ";") elif ft.startsWith("Maybe[") and ft.endsWith("]"): lines.add(" int " & f.name & "_present;") lines.add(" " & nimTypeToC(innerOf(ft, "Maybe[")) & " " & f.name & ";") else: lines.add(" " & nimTypeToC(ft) & " " & f.name & ";") lines.add("} " & t.name & ";") lines.add("") return lines const HeaderPrelude = """ // Generated by nim-ffi C codegen. Do not edit by hand. // // Native (zero-serialization) C ABI. Each call delivers its result to the // callback: on RET_OK, (msg, len) is the raw return value (for string-returning // procs, the string bytes — not NUL-terminated; use len); on RET_ERR, (msg, len) // is the raw error text. A `_cbor` variant of each proc also exists for // generic/cross-language callers that prefer a CBOR request/response. #ifndef NIM_FFI_GEN__H #define NIM_FFI_GEN__H #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 #ifndef NIM_FFI_CALLBACK_T #define NIM_FFI_CALLBACK_T typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); #endif """ proc generateCHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, events: seq[FFIEventMeta] = @[], ): string = let guard = libName.toUpper() var lines: seq[string] = @[] lines.add(HeaderPrelude.replace("", guard)) lines.add("") lines.add(emitCStructs(types)) lines.add("") for p in procs: let args = typedArgs(p) case p.kind of FFIKind.DTOR: lines.add("int " & p.procName & "(void *ctx);") of FFIKind.CTOR: let lead = if args.len > 0: args & ", " else: "" lines.add( "void *" & p.procName & "(" & lead & "FFICallBack callback, void *userData);" ) of FFIKind.FFI: let tail = if args.len > 0: ", " & args else: "" lines.add( "int " & p.procName & "(void *ctx, FFICallBack callback, void *userData" & tail & ");" ) 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") const CborHeaderPrelude = """ // Generated by nim-ffi C codegen. Do not edit by hand. // // CBOR ABI (`_cbor`). Use this for callers that cross a process or machine // boundary (the request has to be serialized anyway) or any generic / cross- // language caller. Build the request with the FfiCbor helpers below — a CBOR map // whose keys are the Nim parameter names (listed per proc) — call the matching // `_cbor`, and decode the RET_OK response (a CBOR-encoded value; for // string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR // delivers raw error text. For same-process callers, prefer the native `` // ABI in the companion .h header. #ifndef NIM_FFI_GEN__CBOR_H #define NIM_FFI_GEN__CBOR_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 #ifndef NIM_FFI_CALLBACK_T #define NIM_FFI_CALLBACK_T typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); #endif #ifndef NIM_FFI_CBOR_HELPERS #define NIM_FFI_CBOR_HELPERS // --- minimal growable CBOR request 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; } 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))); } // --- response decoding ----------------------------------------------------- // Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets // *out/*outLen to point INTO `data` (no allocation; valid only while `data` is) // and returns 1; returns 0 for a non-text-string payload. 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: malloc a NUL-terminated copy. NULL for a non-text payload. // Caller frees. 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; } #endif // NIM_FFI_CBOR_HELPERS """ proc requestFieldsComment(p: FFIProcMeta): string = ## A one-line note of the CBOR request-map keys (the Nim param names) so a ## caller knows what to encode. if p.extraParams.len == 0: return "// request: empty CBOR map (0xA0)" var parts: seq[string] = @[] for ep in p.extraParams: parts.add("\"" & ep.name & "\": " & ep.typeName) return "// request map keys: {" & parts.join(", ") & "}" proc generateCborCHeader*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, events: seq[FFIEventMeta] = @[], ): string = let guard = libName.toUpper() var lines: seq[string] = @[] lines.add(CborHeaderPrelude.replace("", guard)) lines.add("") for p in procs: case p.kind of FFIKind.DTOR: # The destructor takes no request, so it has no CBOR variant. lines.add("int " & p.procName & "(void *ctx);") of FFIKind.CTOR: lines.add(requestFieldsComment(p)) lines.add( "void *" & p.procName & "_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);" ) of FFIKind.FFI: lines.add(requestFieldsComment(p)) lines.add( "int " & p.procName & "_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);" ) lines.add("") 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 & "_CBOR_H */") return lines.join("\n") proc generateCBindings*( procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string, outputDir: string, nimSrcRelPath: string, events: seq[FFIEventMeta] = @[], ) = # Emit both ABIs so consumers can choose per call site: the native (zero-copy, # same-process) one and the CBOR (boundary-crossing / generic) one. writeFile( outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events) ) writeFile( outputDir / (libName & "_cbor.h"), generateCborCHeader(procs, types, libName, events), )