mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-25 10:49:29 +00:00
feat(codegen): add C and Go (cgo) binding generators
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 <lib>.h with a small CBOR encoder,
ffi_decode_text(), and `static inline <lib>_<proc>(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 <lib>.go cgo package that #includes the
generated <lib>.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 <noreply@anthropic.com>
This commit is contained in:
parent
c43563f82f
commit
028dbb56e6
22
ffi.nimble
22
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" &
|
||||
|
||||
418
ffi/codegen/c.nim
Normal file
418
ffi/codegen/c.nim
Normal file
@ -0,0 +1,418 @@
|
||||
## C binding generator for the nim-ffi framework.
|
||||
##
|
||||
## Emits a single self-contained header `<lib>.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 — `<lib>_<proc>(ctx, callback, userData, <typed args...>)` — 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
|
||||
// <LIB>_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
|
||||
// <LIB>_add_event_listener are raw JSON strings (not CBOR).
|
||||
#ifndef NIM_FFI_GEN_<GUARD>_H
|
||||
#define NIM_FFI_GEN_<GUARD>_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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>", guard).replace("<LIB>", 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* <name>(<typed args...>, 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)
|
||||
)
|
||||
403
ffi/codegen/go.nim
Normal file
403
ffi/codegen/go.nim
Normal file
@ -0,0 +1,403 @@
|
||||
## Go (cgo) binding generator for the nim-ffi framework.
|
||||
##
|
||||
## Emits a single `<lib>.go` file that wraps the CBOR ABI behind idiomatic Go.
|
||||
## It builds on the C codegen's `<lib>.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 `<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(" 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 <libName> => <path to this dir>`.
|
||||
let goMod = outputDir / "go.mod"
|
||||
if not fileExists(goMod):
|
||||
writeFile(goMod, "module " & libName & "\n\ngo 1.21\n")
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user