mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-21 16:59:30 +00:00
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>
419 lines
14 KiB
Nim
419 lines
14 KiB
Nim
## 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)
|
|
)
|