mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
A single {.ffi.} definition now produces BOTH interfaces, chosen by the
caller at link time rather than by a global compile flag:
- `<name>` — native typed-arg C export. Args travel to the FFI thread in
a c_malloc'd C-POD struct passed by pointer (no CBOR), and the
result is delivered to the callback as raw bytes. This is the
preferred path for same-process callers: no serialization on
either side.
- `<name>_cbor` — the existing CBOR-buffer dispatcher, kept for generic /
cross-language callers.
Both share the user's helper proc; they register distinct handlers keyed by
"<Camel>Req" (CBOR) and "<Camel>ReqNative". FFIThreadRequest gains a `cborMode`
flag and a `payloadFree` hook so the native C-POD payload (which owns duplicated
cstring fields) is released correctly and an empty native result is delivered as
a zero-length buffer instead of the CBOR null sentinel. alloc.nim gains
ffiCMalloc/ffiCFree (prefixed to avoid Nim's style-insensitive clash with
ansi_c.c_malloc/c_free).
Verified end-to-end on a scalar-param lib: native calls return raw strings
("calc v1", "sum=42"); the _cbor variant still returns CBOR.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
400 lines
13 KiB
Nim
400 lines
13 KiB
Nim
## Go (cgo) binding generator for the nim-ffi framework.
|
|
##
|
|
## Emits a single `<lib>.go` file that wraps the native C ABI behind idiomatic
|
|
## Go. It includes the C codegen's `<lib>.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 `<lib>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(<ctor args...>) (*WakuNode, error)
|
|
## - func (n *WakuNode) <Method>(<args...>) (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.<type>` 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 <stdlib.h>")
|
|
L.add("#include <string.h>")
|
|
L.add("#include <pthread.h>")
|
|
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 <libName> => <path to this dir>`.
|
|
let goMod = outputDir / "go.mod"
|
|
if not fileExists(goMod):
|
|
writeFile(goMod, "module " & libName & "\n\ngo 1.21\n")
|