mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 08:19:55 +00:00
"token" was overloaded (auth tokens, cgo handles, lexer tokens) and didn't say
what it is — a per-call correlation id linking an outgoing {.ffiHost.} call to
the answer that arrives later (possibly from another thread). Renamed across the
runtime (ffi_host / ffi_context), the macro, the exported C ABI (FFIHostFn,
<lib>_host_complete), the Go trampoline, and the tests; regenerated bindings.
The unrelated request-path cgo.Handle result-slot (also informally called a
"token" in go.nim comments) is left as-is — different mechanism.
16 host unit tests + the examples/host_demo Go round-trip stay green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
788 lines
28 KiB
Nim
788 lines
28 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
|
|
import ./c as cgen
|
|
|
|
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"]
|
|
|
|
# --- {.ffi.}-struct param support -------------------------------------------
|
|
# A registered {.ffi.} type is passed to the native ABI as a flat C-POD struct
|
|
# (see codegen/c.emitCStructs); cgo exposes it as `C.<Type>`. We mirror each
|
|
# type as an idiomatic Go struct and marshal it into the C struct per call,
|
|
# freeing the C-side allocations once the call returns (the native path deep-
|
|
# copies every argument, so the C buffers are safe to release immediately).
|
|
|
|
proc isFFIStruct(typeName: string, types: seq[FFITypeMeta]): bool =
|
|
let t = typeName.strip()
|
|
for ty in types:
|
|
if ty.name == t:
|
|
return true
|
|
return false
|
|
|
|
proc isSeqT(t: string): bool =
|
|
t.strip().startsWith("seq[") and t.strip().endsWith("]")
|
|
|
|
proc isOptT(t: string): bool =
|
|
let s = t.strip()
|
|
(s.startsWith("Option[") or s.startsWith("Maybe[")) and s.endsWith("]")
|
|
|
|
proc seqElemT(t: string): string =
|
|
t.strip()["seq[".len .. ^2].strip()
|
|
|
|
proc optElemT(t: string): string =
|
|
let s = t.strip()
|
|
let p = if s.startsWith("Maybe["): "Maybe[".len else: "Option[".len
|
|
s[p .. ^2].strip()
|
|
|
|
proc isStringT(t: string): bool =
|
|
t.strip() in ["string", "cstring"]
|
|
|
|
proc goFieldType(typeName: string, types: seq[FFITypeMeta]): string =
|
|
## Idiomatic Go type for a struct field: seq -> slice, Option -> pointer,
|
|
## string -> string, scalar -> mapped, nested {.ffi.} struct -> its Go name.
|
|
let t = typeName.strip()
|
|
if isSeqT(t):
|
|
"[]" & goFieldType(seqElemT(t), types)
|
|
elif isOptT(t):
|
|
"*" & goFieldType(optElemT(t), types)
|
|
else:
|
|
nimTypeToGo(t)
|
|
|
|
proc cgoElemType(typeName: string, types: seq[FFITypeMeta]): string =
|
|
## The cgo type for one scalar/element value.
|
|
let t = typeName.strip()
|
|
if isFFIStruct(t, types): "C." & t
|
|
elif isStringT(t): "*C.char"
|
|
else: cgoArgType(t)
|
|
|
|
proc paramSupported(typeName: string, types: seq[FFITypeMeta]): bool =
|
|
let t = typeName.strip()
|
|
supported(t) or isFFIStruct(t, types)
|
|
|
|
proc allSupported(p: FFIProcMeta, types: seq[FFITypeMeta]): bool =
|
|
for ep in p.extraParams:
|
|
if ep.isPtr or not paramSupported(ep.typeName, types):
|
|
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)
|
|
|
|
# --- Go marshalling code generators -----------------------------------------
|
|
# All emit tab-indented Go lines. `dst` is a cgo l-value, `src` a Go expression.
|
|
# String/struct/seq marshalling appends a cleanup to the local `frees` slice.
|
|
|
|
proc marshalValue(
|
|
dst, src, typeName: string, types: seq[FFITypeMeta], tok: string
|
|
): seq[string] =
|
|
## Convert a single Go value `src` into the cgo field `dst`.
|
|
var lines: seq[string] = @[]
|
|
let t = typeName.strip()
|
|
if isStringT(t):
|
|
lines.add("\tcs_" & tok & " := C.CString(" & src & ")")
|
|
lines.add(
|
|
"\tfrees = append(frees, func() { C.free(unsafe.Pointer(cs_" & tok & ")) })"
|
|
)
|
|
lines.add("\t" & dst & " = cs_" & tok)
|
|
elif isFFIStruct(t, types):
|
|
lines.add("\tcf_" & tok & ", ff_" & tok & " := (" & src & ").toC()")
|
|
lines.add("\tfrees = append(frees, ff_" & tok & "...)")
|
|
lines.add("\t" & dst & " = cf_" & tok)
|
|
elif t == "bool":
|
|
lines.add("\tif " & src & " {")
|
|
lines.add("\t\t" & dst & " = 1")
|
|
lines.add("\t} else {")
|
|
lines.add("\t\t" & dst & " = 0")
|
|
lines.add("\t}")
|
|
else:
|
|
lines.add("\t" & dst & " = " & cgoArgType(t) & "(" & src & ")")
|
|
return lines
|
|
|
|
proc cgoZeroVal(typeName: string, types: seq[FFITypeMeta]): string =
|
|
## A zero value of the element type, for `unsafe.Sizeof` in C array allocation.
|
|
let t = typeName.strip()
|
|
if isFFIStruct(t, types): "C." & t & "{}"
|
|
elif isStringT(t): "(*C.char)(nil)"
|
|
else: cgoArgType(t) & "(0)"
|
|
|
|
proc goMarshalField(f: FFIFieldMeta, types: seq[FFITypeMeta]): seq[string] =
|
|
## Populate `c.<field>` (and `c.<field>_len` / `c.<field>_present`) from
|
|
## `v.<Field>`, matching the C-POD layout in codegen/c.emitCStructs.
|
|
var lines: seq[string] = @[]
|
|
let cname = f.name
|
|
let gofld = capitalizeFirstLetter(f.name)
|
|
let t = f.typeName.strip()
|
|
if isSeqT(t):
|
|
let elem = seqElemT(t)
|
|
let cElem = cgoElemType(elem, types)
|
|
lines.add("\tif n_" & cname & " := len(v." & gofld & "); n_" & cname & " > 0 {")
|
|
lines.add(
|
|
"\t\tarr_" & cname & " := C.malloc(C.size_t(n_" & cname &
|
|
") * C.size_t(unsafe.Sizeof(" & cgoZeroVal(elem, types) & ")))"
|
|
)
|
|
lines.add(
|
|
"\t\tsl_" & cname & " := unsafe.Slice((*" & cElem & ")(arr_" & cname & "), n_" &
|
|
cname & ")"
|
|
)
|
|
lines.add("\t\tfor i := 0; i < n_" & cname & "; i++ {")
|
|
for ln in marshalValue(
|
|
"sl_" & cname & "[i]", "v." & gofld & "[i]", elem, types, cname & "_e"
|
|
):
|
|
lines.add("\t\t" & ln)
|
|
lines.add("\t\t}")
|
|
lines.add("\t\tc." & cname & " = (*" & cElem & ")(arr_" & cname & ")")
|
|
lines.add("\t\tc." & cname & "_len = C.size_t(n_" & cname & ")")
|
|
lines.add("\t\ta_" & cname & " := arr_" & cname)
|
|
lines.add("\t\tfrees = append(frees, func() { C.free(a_" & cname & ") })")
|
|
lines.add("\t}")
|
|
elif isOptT(t):
|
|
let elem = optElemT(t)
|
|
lines.add("\tif v." & gofld & " != nil {")
|
|
lines.add("\t\tc." & cname & "_present = 1")
|
|
for ln in marshalValue("c." & cname, "*v." & gofld, elem, types, cname & "_o"):
|
|
lines.add("\t" & ln)
|
|
lines.add("\t}")
|
|
else:
|
|
for ln in marshalValue("c." & cname, "v." & gofld, t, types, cname):
|
|
lines.add(ln)
|
|
return lines
|
|
|
|
# --- reverse marshalling: C-POD -> Go (for typed struct returns) ------------
|
|
# Used inside the result callback, where the POD is still alive; everything is
|
|
# copied out into Go-owned memory before the library frees the POD.
|
|
|
|
proc fromCExpr(src, typeName: string, types: seq[FFITypeMeta]): string =
|
|
## A Go expression converting the cgo value `src` to its Go type.
|
|
let t = typeName.strip()
|
|
if isStringT(t): "C.GoString(" & src & ")"
|
|
elif isFFIStruct(t, types): t & "FromC(&" & src & ")"
|
|
elif t == "bool": "(" & src & " != 0)"
|
|
else: nimTypeToGo(t) & "(" & src & ")"
|
|
|
|
proc goUnmarshalField(f: FFIFieldMeta, types: seq[FFITypeMeta]): seq[string] =
|
|
## Copy `c.<field>` (+ `_len` / `_present`) out into `v.<Field>`.
|
|
var lines: seq[string] = @[]
|
|
let cname = f.name
|
|
let gofld = capitalizeFirstLetter(f.name)
|
|
let t = f.typeName.strip()
|
|
if isSeqT(t):
|
|
let elem = seqElemT(t)
|
|
let elemGo = goFieldType(elem, types)
|
|
lines.add("\tif c." & cname & "_len > 0 {")
|
|
lines.add(
|
|
"\t\tsrc_" & cname & " := unsafe.Slice(c." & cname & ", int(c." & cname & "_len))"
|
|
)
|
|
lines.add("\t\tv." & gofld & " = make([]" & elemGo & ", int(c." & cname & "_len))")
|
|
lines.add("\t\tfor i := range src_" & cname & " {")
|
|
lines.add(
|
|
"\t\t\tv." & gofld & "[i] = " & fromCExpr("src_" & cname & "[i]", elem, types)
|
|
)
|
|
lines.add("\t\t}")
|
|
lines.add("\t}")
|
|
elif isOptT(t):
|
|
let elem = optElemT(t)
|
|
lines.add("\tif c." & cname & "_present != 0 {")
|
|
lines.add("\t\ttmp_" & cname & " := " & fromCExpr("c." & cname, elem, types))
|
|
lines.add("\t\tv." & gofld & " = &tmp_" & cname)
|
|
lines.add("\t}")
|
|
else:
|
|
lines.add("\tv." & gofld & " = " & fromCExpr("c." & cname, t, types))
|
|
return lines
|
|
|
|
proc emitGoTypesAndToC(types: seq[FFITypeMeta]): seq[string] =
|
|
## A Go struct + `toC()` marshaller + `fromC()` reader for every {.ffi.} type.
|
|
var lines: seq[string] = @[]
|
|
for ty in types:
|
|
lines.add("// " & ty.name & " mirrors the {.ffi.} type of the same name.")
|
|
lines.add("type " & ty.name & " struct {")
|
|
for f in ty.fields:
|
|
lines.add("\t" & capitalizeFirstLetter(f.name) & " " & goFieldType(f.typeName, types))
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add(
|
|
"// toC marshals " & ty.name &
|
|
" into its C-POD form, returning cleanup funcs to run after the call."
|
|
)
|
|
lines.add("func (v " & ty.name & ") toC() (C." & ty.name & ", []func()) {")
|
|
lines.add("\tvar c C." & ty.name)
|
|
lines.add("\tvar frees []func()")
|
|
for f in ty.fields:
|
|
for ln in goMarshalField(f, types):
|
|
lines.add(ln)
|
|
lines.add("\treturn c, frees")
|
|
lines.add("}")
|
|
lines.add("")
|
|
lines.add(
|
|
"// " & ty.name & "FromC copies a C-POD " & ty.name & " (e.g. a typed return" &
|
|
") into a Go value."
|
|
)
|
|
lines.add("func " & ty.name & "FromC(c *C." & ty.name & ") " & ty.name & " {")
|
|
lines.add("\tvar v " & ty.name)
|
|
for f in ty.fields:
|
|
for ln in goUnmarshalField(f, types):
|
|
lines.add(ln)
|
|
lines.add("\treturn v")
|
|
lines.add("}")
|
|
lines.add("")
|
|
return lines
|
|
|
|
proc goParamConv(
|
|
extraParams: seq[FFIParamMeta], types: seq[FFITypeMeta]
|
|
): tuple[goParams, conv, callArgs: seq[string]] =
|
|
## Go method/ctor parameter list, the conversion lines that turn each Go arg
|
|
## into a cgo value, and the resulting call-argument expressions.
|
|
var goParams, conv, callArgs: seq[string] = @[]
|
|
for ep in extraParams:
|
|
let nm = ep.name
|
|
let t = ep.typeName.strip()
|
|
goParams.add(nm & " " & goFieldType(t, types))
|
|
if isFFIStruct(t, types):
|
|
conv.add("\tc_" & nm & ", free_" & nm & " := " & nm & ".toC()")
|
|
conv.add("\tdefer func() { for _, f := range free_" & nm & " { f() } }()")
|
|
callArgs.add("c_" & nm)
|
|
elif isStringT(t):
|
|
conv.add("\tc_" & nm & " := C.CString(" & nm & ")")
|
|
conv.add("\tdefer C.free(unsafe.Pointer(c_" & nm & "))")
|
|
callArgs.add("c_" & nm)
|
|
elif t == "bool":
|
|
conv.add("\tc_" & nm & " := C.int(0)")
|
|
conv.add("\tif " & nm & " { c_" & nm & " = 1 }")
|
|
callArgs.add("c_" & nm)
|
|
else:
|
|
callArgs.add(cgoArgType(t) & "(" & nm & ")")
|
|
return (goParams, conv, callArgs)
|
|
|
|
proc generateGoFile*(
|
|
procs: seq[FFIProcMeta],
|
|
types: seq[FFITypeMeta],
|
|
libName: string,
|
|
events: seq[FFIEventMeta] = @[],
|
|
hosts: seq[FFIHostMeta] = @[],
|
|
): 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("/*")
|
|
# ${SRCDIR} expands to this package's directory, so the native header and the
|
|
# built lib (staged next to the .go) are found without extra env vars; the
|
|
# rpath lets the dylib/.so load at runtime from the same directory.
|
|
L.add("#cgo CFLAGS: -I${SRCDIR}")
|
|
L.add("#cgo LDFLAGS: -L${SRCDIR} -l" & libName & " -Wl,-rpath,${SRCDIR}")
|
|
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);"
|
|
)
|
|
# Host callbacks ({.ffiHost.}): a single exported Go trampoline backs every
|
|
# registered host fn; the static helper hands its address to register_host_fn
|
|
# (cgo drops const, so the forward decl uses char*).
|
|
if hosts.len > 0:
|
|
L.add(
|
|
"extern void " & libName &
|
|
"HostTrampoline(uint64_t callId, char* req, size_t reqLen, void* userData);"
|
|
)
|
|
L.add(
|
|
"static int " & libName &
|
|
"RegisterHost(void* ctx, const char* name, void* ud) {"
|
|
)
|
|
# cgo exports the trampoline with `char*` (it drops const); cast to FFIHostFn
|
|
# so the function-pointer types match.
|
|
L.add(
|
|
" return " & libName & "_register_host_fn(ctx, name, (FFIHostFn)" & libName &
|
|
"HostTrampoline, ud);"
|
|
)
|
|
L.add("}")
|
|
# One exported Go result callback per struct-returning proc (it reads the typed
|
|
# return POD in-callback). Forward-declared here so cgo's `char*` shape matches.
|
|
for p in procs:
|
|
if p.kind == FFIKind.FFI and allSupported(p, types) and
|
|
isFFIStruct(p.returnTypeName, types):
|
|
L.add(
|
|
"extern void " & libName & "Result" & methodName(p.procName, libName) &
|
|
"(int ret, char* msg, size_t len, void* ud);"
|
|
)
|
|
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 — only for string/raw-returning procs. Struct-returning
|
|
# procs call the native entry point directly from Go with an exported Go
|
|
# callback (the C RespCb would copy struct bytes whose pointers then dangle).
|
|
for p in procs:
|
|
if p.kind != FFIKind.FFI or not allSupported(p, types):
|
|
continue
|
|
if isFFIStruct(p.returnTypeName, types):
|
|
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\"runtime/cgo\"")
|
|
L.add("\t\"sync\"")
|
|
L.add("\t\"unsafe\"")
|
|
L.add(")")
|
|
L.add("")
|
|
# Carries one typed result from the FFI-thread callback back to the blocked
|
|
# caller goroutine; passed through C as a cgo.Handle token (stable under GC).
|
|
L.add("type resultSlot struct {")
|
|
L.add("\tval any")
|
|
L.add("\terr error")
|
|
L.add("\tdone chan struct{}")
|
|
L.add("}")
|
|
L.add("")
|
|
|
|
# ---- Go mirrors of the {.ffi.} types (with C-POD marshalling) ------------
|
|
for line in emitGoTypesAndToC(types):
|
|
L.add(line)
|
|
|
|
# ---- 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("")
|
|
|
|
# ---- host callbacks ({.ffiHost.}) ----------------------------------------
|
|
# One exported trampoline serves all host fns; the cgo.Handle in userData
|
|
# selects which Go closure. The closure runs on a fresh goroutine so the FFI
|
|
# thread is never blocked (the non-blocking contract), then answers by callId.
|
|
if hosts.len > 0:
|
|
L.add("type hostEntry struct {")
|
|
L.add("\tctx unsafe.Pointer")
|
|
L.add("\tfn func(string) (string, error)")
|
|
L.add("}")
|
|
L.add("")
|
|
L.add("//export " & libName & "HostTrampoline")
|
|
L.add(
|
|
"func " & libName &
|
|
"HostTrampoline(callId C.uint64_t, req *C.char, reqLen C.size_t, userData unsafe.Pointer) {"
|
|
)
|
|
L.add("\te := cgo.Handle(uintptr(userData)).Value().(hostEntry)")
|
|
L.add("\treqStr := C.GoStringN(req, C.int(reqLen))")
|
|
L.add("\tgo func() {")
|
|
L.add("\t\tres, err := e.fn(reqStr)")
|
|
L.add("\t\tif err != nil {")
|
|
L.add("\t\t\tmsg := err.Error()")
|
|
L.add("\t\t\tcmsg := C.CString(msg)")
|
|
L.add(
|
|
"\t\t\tC." & libName &
|
|
"_host_complete(e.ctx, callId, C.int(C.RET_ERR), cmsg, C.size_t(len(msg)))"
|
|
)
|
|
L.add("\t\t\tC.free(unsafe.Pointer(cmsg))")
|
|
L.add("\t\t} else {")
|
|
L.add("\t\t\tcmsg := C.CString(res)")
|
|
L.add(
|
|
"\t\t\tC." & libName &
|
|
"_host_complete(e.ctx, callId, C.int(C.RET_OK), cmsg, C.size_t(len(res)))"
|
|
)
|
|
L.add("\t\t\tC.free(unsafe.Pointer(cmsg))")
|
|
L.add("\t\t}")
|
|
L.add("\t}()")
|
|
L.add("}")
|
|
L.add("")
|
|
for h in hosts:
|
|
let setName = "Set" & capitalizeFirstLetter(h.nimProcName)
|
|
L.add(
|
|
"// " & setName & " registers the host implementation of the '" & h.wireName &
|
|
"' {.ffiHost.} call."
|
|
)
|
|
L.add(
|
|
"func (n *" & nodeType & ") " & setName &
|
|
"(fn func(string) (string, error)) {"
|
|
)
|
|
L.add("\thandle := cgo.NewHandle(hostEntry{ctx: n.ctx, fn: fn})")
|
|
L.add("\tcname := C.CString(\"" & h.wireName & "\")")
|
|
L.add(
|
|
"\tC." & libName & "RegisterHost(n.ctx, cname, unsafe.Pointer(handle))"
|
|
)
|
|
L.add("\tC.free(unsafe.Pointer(cname))")
|
|
L.add("}")
|
|
L.add("")
|
|
|
|
# ---- constructor ---------------------------------------------------------
|
|
if haveCtor:
|
|
let (goParams, conv, callArgs) = goParamConv(ctor.extraParams, types)
|
|
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, types):
|
|
L.add(
|
|
"// SKIPPED " & p.procName &
|
|
": unsupported param (raw pointer or bare seq/Option) for Go codegen"
|
|
)
|
|
L.add("")
|
|
continue
|
|
let mName = methodName(p.procName, libName)
|
|
let (goParams, conv, callArgs) = goParamConv(p.extraParams, types)
|
|
let callArgsStr =
|
|
if callArgs.len > 0:
|
|
", " & callArgs.join(", ")
|
|
else:
|
|
""
|
|
|
|
if isFFIStruct(p.returnTypeName, types):
|
|
# Typed struct return: call the native entry point directly with an
|
|
# exported Go callback; a cgo.Handle token carries the result slot through
|
|
# C (stable under GC) so the async callback can deliver the typed value.
|
|
let retT = p.returnTypeName
|
|
let cbName = libName & "Result" & mName
|
|
L.add("//export " & cbName)
|
|
L.add(
|
|
"func " & cbName &
|
|
"(ret C.int, msg *C.char, length C.size_t, ud unsafe.Pointer) {"
|
|
)
|
|
L.add("\tslot := cgo.Handle(*(*C.uintptr_t)(ud)).Value().(*resultSlot)")
|
|
L.add("\tif ret == C.RET_OK {")
|
|
L.add("\t\tslot.val = " & retT & "FromC((*C." & retT & ")(unsafe.Pointer(msg)))")
|
|
L.add("\t} else {")
|
|
L.add("\t\tslot.err = errors.New(C.GoStringN(msg, C.int(length)))")
|
|
L.add("\t}")
|
|
L.add("\tclose(slot.done)")
|
|
L.add("}")
|
|
L.add("")
|
|
L.add(
|
|
"func (n *" & nodeType & ") " & mName & "(" & goParams.join(", ") & ") (" &
|
|
retT & ", error) {"
|
|
)
|
|
for c in conv:
|
|
L.add(c)
|
|
L.add("\tslot := &resultSlot{done: make(chan struct{})}")
|
|
L.add("\th := cgo.NewHandle(slot)")
|
|
L.add("\tdefer h.Delete()")
|
|
# Box the cgo.Handle in a small C allocation and pass that real C pointer as
|
|
# the void* userData. The handle is a stable GC-safe token; boxing it (vs.
|
|
# casting it straight to unsafe.Pointer) keeps -race/checkptr happy.
|
|
L.add(
|
|
"\thbox := (*C.uintptr_t)(C.malloc(C.size_t(unsafe.Sizeof(C.uintptr_t(0)))))"
|
|
)
|
|
L.add("\t*hbox = C.uintptr_t(h)")
|
|
L.add("\tdefer C.free(unsafe.Pointer(hbox))")
|
|
L.add(
|
|
"\trc := C." & p.procName &
|
|
"(n.ctx, C.FFICallBack(C." & cbName & "), unsafe.Pointer(hbox)" &
|
|
callArgsStr & ")"
|
|
)
|
|
L.add("\tif rc != C.RET_OK {")
|
|
L.add("\t\treturn " & retT & "{}, errors.New(\"" & p.procName & ": dispatch failed\")")
|
|
L.add("\t}")
|
|
L.add("\t<-slot.done")
|
|
L.add("\tif slot.err != nil {")
|
|
L.add("\t\treturn " & retT & "{}, slot.err")
|
|
L.add("\t}")
|
|
L.add("\treturn slot.val.(" & retT & "), nil")
|
|
L.add("}")
|
|
L.add("")
|
|
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] = @[],
|
|
hosts: seq[FFIHostMeta] = @[],
|
|
) =
|
|
writeFile(
|
|
outputDir / (libName & ".go"),
|
|
generateGoFile(procs, types, libName, events, hosts),
|
|
)
|
|
# cgo `#include "<lib>.h"` resolves against this package directory, so emit the
|
|
# native C header here too — the Go package is then self-contained (just stage
|
|
# the built lib next to it).
|
|
writeFile(
|
|
outputDir / (libName & ".h"), generateCHeader(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")
|