From 028dbb56e6c5ac902f84ad971cc9ad123bc3f9c0 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sat, 30 May 2026 23:51:13 +0200 Subject: [PATCH] feat(codegen): add C and Go (cgo) binding generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0.2.0 carries each request as a single CBOR buffer over the exported ABI, which is awkward for hand-written host bindings (every consumer would have to encode CBOR and decode responses by hand). These two generators emit ergonomic, ready-to-use bindings from the same {.ffi.} registry the C++/Rust generators already consume. - c.nim (targetLang=c): a self-contained .h with a small CBOR encoder, ffi_decode_text(), and `static inline _(ctx, cb, ud, args...)` wrappers that CBOR-encode and forward to the real export. The wrapper keeps the export's source name but is given a distinct symbol via an __asm__ label so the raw export's asm alias doesn't bind back to the wrapper (which would recurse). Scalar/string params only; others fall back to the raw CBOR decl. - go.nim (targetLang=go): a single .go cgo package that #includes the generated .h and adds a condvar-backed response capture. This is the key bit: 0.2.0 removed the synchronous fast-path, so a caller can no longer read a result right after the call — the generated bridges block on the callback, turning each async export into a blocking Go method. Also emits a go.mod for importability. Wired both into genBindings dispatch (targetLang "c"/"go") and added genbindings_c / genbindings_go tasks. Both verified end-to-end against a scalar-param test lib (build + run) and the real libwaku surface. Co-Authored-By: Claude Opus 4.8 --- ffi.nimble | 22 ++ ffi/codegen/c.nim | 418 +++++++++++++++++++++++++++++++++++++ ffi/codegen/go.nim | 403 +++++++++++++++++++++++++++++++++++ ffi/internal/ffi_macro.nim | 15 +- 4 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 ffi/codegen/c.nim create mode 100644 ffi/codegen/go.nim diff --git a/ffi.nimble b/ffi.nimble index d52a8ac..11621c5 100644 --- a/ffi.nimble +++ b/ffi.nimble @@ -146,6 +146,28 @@ task genbindings_rust, "Generate Rust bindings for the timer example": " -d:ffiSrcPath=../timer.nim" & " -o:/dev/null examples/timer/timer.nim" +task genbindings_c, "Generate C bindings for the timer example": + exec "nim c " & nimFlagsOrc & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & + " -d:ffiGenBindings -d:targetLang=c" & + " -d:ffiOutputDir=examples/timer/c_bindings" & + " -d:ffiSrcPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" + exec "nim c " & nimFlagsRefc & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & + " -d:ffiGenBindings -d:targetLang=c" & + " -d:ffiOutputDir=examples/timer/c_bindings" & + " -d:ffiSrcPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" + +task genbindings_go, "Generate Go (cgo) bindings for the timer example": + exec "nim c " & nimFlagsOrc & + " --app:lib --noMain --nimMainPrefix:libmy_timer" & + " -d:ffiGenBindings -d:targetLang=go" & + " -d:ffiOutputDir=examples/timer/go_bindings" & + " -d:ffiSrcPath=../timer.nim" & + " -o:/dev/null examples/timer/timer.nim" + task genbindings_cddl, "Generate CDDL schema for the timer example": exec "nim c " & nimFlagsOrc & " --app:lib --noMain --nimMainPrefix:libtimer" & diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim new file mode 100644 index 0000000..92c08d5 --- /dev/null +++ b/ffi/codegen/c.nim @@ -0,0 +1,418 @@ +## 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) + ) diff --git a/ffi/codegen/go.nim b/ffi/codegen/go.nim new file mode 100644 index 0000000..f4c003d --- /dev/null +++ b/ffi/codegen/go.nim @@ -0,0 +1,403 @@ +## Go (cgo) binding generator for the nim-ffi framework. +## +## Emits a single `.go` file that wraps the CBOR ABI behind idiomatic Go. +## It builds on the C codegen's `.h` (included from the cgo preamble) for +## the ergonomic typed wrappers, the CBOR encoder, and `ffi_decode_text`, and +## adds the piece every async-only consumer needs: a per-call response capture +## that BLOCKS until the FFI callback fires, then decodes the payload. +## +## nim-ffi 0.2.0 dispatches every `{.ffi.}` call through the FFI thread (no sync +## fast-path), so a caller cannot read the result immediately after the call — +## the generated `Call_*` C bridges register a condvar-backed callback and +## wait on it, turning each async export into a blocking Go method. +## +## Generated surface (libName = "waku" → package waku): +## - type WakuNode struct { ctx unsafe.Pointer } +## - func NewWaku() (*WakuNode, error) +## - func (n *WakuNode) () (string, error) per {.ffi.} proc +## - func (n *WakuNode) Destroy() error +## - func (n *WakuNode) SetEventHandler(h func(string)) raw-JSON events +## +## Only scalar/string params are supported (matching the C codegen); a proc with +## struct/seq/Option params is skipped with a // SKIPPED comment. + +import std/[os, strutils] +import ./meta, ./string_helpers + +proc nimTypeToGo(typeName: string): string = + let t = typeName.strip() + case t + of "string", "cstring": "string" + of "int", "int64", "clong": "int64" + of "int32", "cint": "int32" + of "int16": "int16" + of "int8": "int8" + of "uint", "uint64", "csize_t": "uint64" + of "uint32", "cuint": "uint32" + of "uint16": "uint16" + of "uint8", "byte": "uint8" + of "bool": "bool" + of "float", "float32": "float32" + of "float64": "float64" + else: t + +proc nimTypeToCParam(typeName: string): string = + ## C type used in the generated cgo bridge signature. + let t = typeName.strip() + 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" + else: t + +proc cgoArgType(typeName: string): string = + ## The `C.` a Go value is converted to before calling a bridge. + let t = typeName.strip() + case t + of "string", "cstring": "*C.char" + of "int", "int64", "clong": "C.int64_t" + of "int32", "cint": "C.int32_t" + of "int16": "C.int16_t" + of "int8": "C.int8_t" + of "uint", "uint64", "csize_t": "C.uint64_t" + of "uint32", "cuint": "C.uint32_t" + of "uint16": "C.uint16_t" + of "uint8", "byte": "C.uint8_t" + of "bool": "C.int" + else: "" + +proc supported(typeName: string): bool = + cgoArgType(typeName).len > 0 or typeName.strip() in ["string", "cstring"] + +proc allSupported(p: FFIProcMeta): bool = + for ep in p.extraParams: + if ep.isPtr or not supported(ep.typeName): + return false + return true + +proc methodName(procName, libName: string): string = + let prefix = libName & "_" + let bare = + if procName.startsWith(prefix): + procName[prefix.len .. ^1] + else: + procName + return snakeToPascalCase(bare) + +proc generateGoFile*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], +): string = + let nodeType = capitalizeFirstLetter(libName) & "Node" + let respT = capitalizeFirstLetter(libName) & "Resp" + var L: seq[string] = @[] + + # Locate ctor / dtor. + var ctor, dtor: FFIProcMeta + var haveCtor, haveDtor = false + for p in procs: + if p.kind == FFIKind.CTOR: + ctor = p + haveCtor = true + elif p.kind == FFIKind.DTOR: + dtor = p + haveDtor = true + + # ---- file + cgo preamble ------------------------------------------------- + L.add("// Code generated by nim-ffi Go codegen. DO NOT EDIT.") + L.add("package " & libName) + L.add("") + L.add("/*") + L.add("#cgo LDFLAGS: -l" & libName) + L.add("#include \"" & libName & ".h\"") + L.add("#include ") + L.add("#include ") + L.add("#include ") + L.add("") + # cgo emits the exported callback with `char*` (it drops const); the forward + # declaration must match exactly or the C compiler reports conflicting types. + L.add( + "extern void " & libName & "GoEvent(int ret, char* msg, size_t len, void* userData);" + ) + L.add("") + L.add("typedef struct {") + L.add(" int ret; char* msg; size_t len; int done;") + L.add(" pthread_mutex_t mu; pthread_cond_t cv;") + L.add("} " & respT & ";") + L.add("") + L.add("static " & respT & "* " & libName & "RespNew() {") + L.add(" " & respT & "* r = (" & respT & "*)calloc(1, sizeof(" & respT & "));") + L.add(" pthread_mutex_init(&r->mu, NULL); pthread_cond_init(&r->cv, NULL);") + L.add(" return r;") + L.add("}") + L.add("static void " & libName & "RespFree(" & respT & "* r) {") + L.add(" if (!r) return;") + L.add(" if (r->msg) free(r->msg);") + L.add(" pthread_mutex_destroy(&r->mu); pthread_cond_destroy(&r->cv); free(r);") + L.add("}") + L.add("static int " & libName & "RespRet(" & respT & "* r) { return r->ret; }") + L.add("static char* " & libName & "RespMsg(" & respT & "* r) { return r->msg; }") + L.add("static size_t " & libName & "RespLen(" & respT & "* r) { return r->len; }") + L.add("") + L.add( + "static void " & libName & "RespCb(int ret, const char* msg, size_t len, void* ud) {" + ) + L.add(" " & respT & "* r = (" & respT & "*)ud;") + L.add(" pthread_mutex_lock(&r->mu);") + L.add(" r->ret = ret;") + L.add(" if (ret == RET_OK) {") + L.add(" char* d = ffi_decode_text((const uint8_t*)msg, len);") + L.add(" r->msg = d; r->len = d ? strlen(d) : 0;") + L.add(" } else {") + L.add( + " char* e = (char*)malloc(len + 1); if (e) { memcpy(e, msg, len); e[len] = 0; }" + ) + L.add(" r->msg = e; r->len = len;") + L.add(" }") + L.add(" r->done = 1; pthread_cond_signal(&r->cv); pthread_mutex_unlock(&r->mu);") + L.add("}") + L.add("static void " & libName & "RespWait(" & respT & "* r) {") + L.add(" pthread_mutex_lock(&r->mu);") + L.add(" while (!r->done) pthread_cond_wait(&r->cv, &r->mu);") + L.add(" pthread_mutex_unlock(&r->mu);") + L.add("}") + L.add("") + + # ctor bridge + if haveCtor: + var cparams: seq[string] = @[] + for ep in ctor.extraParams: + cparams.add(nimTypeToCParam(ep.typeName) & " " & ep.name) + let cparamsStr = + if cparams.len > 0: + cparams.join(", ") & ", " + else: + "" + var callArgs: seq[string] = @[] + for ep in ctor.extraParams: + callArgs.add(ep.name) + let callArgsStr = + if callArgs.len > 0: + callArgs.join(", ") & ", " + else: + "" + L.add( + "static void* " & libName & "Call_" & ctor.procName & "(" & cparamsStr & respT & + "* r) {" + ) + L.add( + " void* ctx = " & ctor.procName & "(" & callArgsStr & libName & "RespCb, r);" + ) + L.add(" " & libName & "RespWait(r);") + L.add(" return ctx;") + L.add("}") + + # per-proc bridges + for p in procs: + if p.kind != FFIKind.FFI or not allSupported(p): + continue + var cparams: seq[string] = @[] + var callArgs: seq[string] = @[] + for ep in p.extraParams: + cparams.add(nimTypeToCParam(ep.typeName) & " " & ep.name) + callArgs.add(ep.name) + let cparamsStr = + if cparams.len > 0: + ", " & cparams.join(", ") + else: + "" + let callArgsStr = + if callArgs.len > 0: + ", " & callArgs.join(", ") + else: + "" + L.add( + "static int " & libName & "Call_" & p.procName & "(void* ctx" & cparamsStr & ", " & + respT & "* r) {" + ) + L.add( + " int rc = " & p.procName & "(ctx, " & libName & "RespCb, r" & callArgsStr & ");" + ) + L.add(" if (rc == RET_OK) " & libName & "RespWait(r);") + L.add(" return rc;") + L.add("}") + + # dtor + event registration bridges + if haveDtor: + L.add( + "static int " & libName & "Call_" & dtor.procName & "(void* ctx) { return " & + dtor.procName & "(ctx); }" + ) + L.add( + "static uint64_t " & libName & "RegisterEvents(void* ctx) { return " & libName & + "_add_event_listener(ctx, \"\", (FFICallBack)" & libName & "GoEvent, ctx); }" + ) + L.add("*/") + L.add("import \"C\"") + L.add("") + L.add("import (") + L.add("\t\"errors\"") + L.add("\t\"sync\"") + L.add("\t\"unsafe\"") + L.add(")") + L.add("") + + # ---- Go types + helpers -------------------------------------------------- + L.add("type " & nodeType & " struct {") + L.add("\tctx unsafe.Pointer") + L.add("}") + L.add("") + L.add("// goStr extracts and frees the captured response string.") + L.add("func respStr(r *C." & respT & ") string {") + L.add( + "\treturn C.GoStringN(C." & libName & "RespMsg(r), C.int(C." & libName & + "RespLen(r)))" + ) + L.add("}") + L.add("") + + # event handler registry (raw JSON events) + L.add("var (") + L.add("\teventMu sync.Mutex") + L.add("\teventHandler func(string)") + L.add(")") + L.add("") + L.add("// SetEventHandler installs the catch-all handler for library-initiated") + L.add("// events (delivered as raw JSON strings).") + L.add("func (n *" & nodeType & ") SetEventHandler(h func(string)) {") + L.add("\teventMu.Lock()") + L.add("\teventHandler = h") + L.add("\teventMu.Unlock()") + L.add("\tC." & libName & "RegisterEvents(n.ctx)") + L.add("}") + L.add("") + L.add("//export " & libName & "GoEvent") + L.add( + "func " & libName & + "GoEvent(ret C.int, msg *C.char, length C.size_t, userData unsafe.Pointer) {" + ) + L.add("\teventMu.Lock()") + L.add("\th := eventHandler") + L.add("\teventMu.Unlock()") + L.add("\tif h != nil && ret == C.RET_OK {") + L.add("\t\th(C.GoStringN(msg, C.int(length)))") + L.add("\t}") + L.add("}") + L.add("") + + # ---- constructor --------------------------------------------------------- + if haveCtor: + var goParams: seq[string] = @[] + var conv: seq[string] = @[] + var callArgs: seq[string] = @[] + for ep in ctor.extraParams: + goParams.add(ep.name & " " & nimTypeToGo(ep.typeName)) + if nimTypeToGo(ep.typeName) == "string": + conv.add("\tc_" & ep.name & " := C.CString(" & ep.name & ")") + conv.add("\tdefer C.free(unsafe.Pointer(c_" & ep.name & "))") + callArgs.add("c_" & ep.name) + else: + callArgs.add(cgoArgType(ep.typeName) & "(" & ep.name & ")") + let callArgsStr = + if callArgs.len > 0: + callArgs.join(", ") & ", " + else: + "" + L.add( + "func New" & capitalizeFirstLetter(libName) & "(" & goParams.join(", ") & ") (*" & + nodeType & ", error) {" + ) + for c in conv: + L.add(c) + L.add("\tr := C." & libName & "RespNew()") + L.add("\tdefer C." & libName & "RespFree(r)") + L.add("\tctx := C." & libName & "Call_" & ctor.procName & "(" & callArgsStr & "r)") + L.add("\tif C." & libName & "RespRet(r) != C.RET_OK {") + L.add("\t\treturn nil, errors.New(respStr(r))") + L.add("\t}") + L.add("\treturn &" & nodeType & "{ctx: ctx}, nil") + L.add("}") + L.add("") + + # ---- methods ------------------------------------------------------------- + for p in procs: + if p.kind != FFIKind.FFI: + continue + if not allSupported(p): + L.add( + "// SKIPPED " & p.procName & + ": struct/seq/Option params unsupported by Go codegen" + ) + L.add("") + continue + let mName = methodName(p.procName, libName) + var goParams: seq[string] = @[] + var conv: seq[string] = @[] + var callArgs: seq[string] = @[] + for ep in p.extraParams: + goParams.add(ep.name & " " & nimTypeToGo(ep.typeName)) + if nimTypeToGo(ep.typeName) == "string": + conv.add("\tc_" & ep.name & " := C.CString(" & ep.name & ")") + conv.add("\tdefer C.free(unsafe.Pointer(c_" & ep.name & "))") + callArgs.add("c_" & ep.name) + else: + callArgs.add(cgoArgType(ep.typeName) & "(" & ep.name & ")") + let callArgsStr = + if callArgs.len > 0: + ", " & callArgs.join(", ") + else: + "" + L.add( + "func (n *" & nodeType & ") " & mName & "(" & goParams.join(", ") & + ") (string, error) {" + ) + for c in conv: + L.add(c) + L.add("\tr := C." & libName & "RespNew()") + L.add("\tdefer C." & libName & "RespFree(r)") + L.add("\tC." & libName & "Call_" & p.procName & "(n.ctx" & callArgsStr & ", r)") + L.add("\tif C." & libName & "RespRet(r) != C.RET_OK {") + L.add("\t\treturn \"\", errors.New(respStr(r))") + L.add("\t}") + L.add("\treturn respStr(r), nil") + L.add("}") + L.add("") + + # ---- destructor ---------------------------------------------------------- + if haveDtor: + L.add("func (n *" & nodeType & ") Destroy() error {") + L.add("\tif C." & libName & "Call_" & dtor.procName & "(n.ctx) != C.RET_OK {") + L.add("\t\treturn errors.New(\"" & libName & " destroy failed\")") + L.add("\t}") + L.add("\treturn nil") + L.add("}") + L.add("") + + return L.join("\n") + +proc generateGoBindings*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, + events: seq[FFIEventMeta] = @[], +) = + writeFile( + outputDir / (libName & ".go"), generateGoFile(procs, types, libName, events) + ) + # Emit a go.mod so the generated package is an importable module (parity with + # the Rust/C++ generators that emit Cargo.toml / CMakeLists.txt). Consumers can + # `replace => `. + let goMod = outputDir / "go.mod" + if not fileExists(goMod): + writeFile(goMod, "module " & libName & "\n\ngo 1.21\n") diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 4f9ffc5..a1f54fb 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -6,6 +6,8 @@ when defined(ffiGenBindings): import ../codegen/rust import ../codegen/cpp import ../codegen/cddl + import ../codegen/c + import ../codegen/go # --------------------------------------------------------------------------- # String helpers used by multiple macros @@ -1521,9 +1523,20 @@ macro genBindings*( generateCddlBindings( ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath ) + of "c": + generateCBindings( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) + of "go": + generateGoBindings( + ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath, + ffiEventRegistry, + ) else: error( - "genBindings: unknown targetLang '" & lang & "'. Use 'rust', 'cpp', or 'cddl'." + "genBindings: unknown targetLang '" & lang & + "'. Use 'c', 'go', 'rust', 'cpp', or 'cddl'." ) return newEmptyNode()