## Go (cgo) binding generator for the nim-ffi framework. ## ## Emits a single `.go` file that wraps the native C ABI behind idiomatic ## Go. It includes the C codegen's `.h` (the native typed-arg declarations) ## and adds the piece every async-only consumer needs: a per-call response ## capture that BLOCKS until the FFI callback fires, then copies the raw result. ## ## 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(" // Native ABI: (msg, len) is the raw result (RET_OK) or error (RET_ERR).") L.add(" // Copy it so it survives past the callback.") 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(" 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")