mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-24 02:09:32 +00:00
feat(ffi): emit a native (zero-serialization) C ABI alongside CBOR
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>
This commit is contained in:
parent
028dbb56e6
commit
f3206c30b8
@ -44,6 +44,21 @@ proc dealloc*(p: cstring) {.inline.} =
|
||||
if not p.isNil():
|
||||
c_free(cast[pointer](p))
|
||||
|
||||
proc ffiCMalloc*(T: typedesc): ptr T =
|
||||
## Allocates a zero-initialised `T` via `c_malloc` so the buffer can cross
|
||||
## threads safely (see the module note). Used to carry a native (non-CBOR)
|
||||
## request payload by pointer; release with `ffiCFree`. (Named with the `ffi`
|
||||
## prefix so it doesn't collide with `ansi_c.c_free`/`c_malloc` under Nim's
|
||||
## style-insensitive identifier rules.)
|
||||
let p = cast[ptr T](c_malloc(csize_t(sizeof(T))))
|
||||
zeroMem(p, sizeof(T))
|
||||
return p
|
||||
|
||||
proc ffiCFree*(p: pointer) {.inline.} =
|
||||
## Frees a buffer obtained from `ffiCMalloc`. Nil-safe.
|
||||
if not p.isNil():
|
||||
c_free(p)
|
||||
|
||||
proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] =
|
||||
if s.len == 0:
|
||||
return (cast[ptr UncheckedArray[T]](nil), 0)
|
||||
|
||||
@ -1,32 +1,34 @@
|
||||
## 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.
|
||||
## Each `{.ffi.}` / `{.ffiCtor.}` proc is exported twice — a native typed-arg
|
||||
## entry point under `<name>` (no CBOR, result delivered raw) and a `<name>_cbor`
|
||||
## variant (CBOR request/response) — so this generator emits BOTH headers, side
|
||||
## by side, and the consumer chooses per call site:
|
||||
##
|
||||
## 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.
|
||||
## - `<lib>.h` — native ABI: `int <name>(ctx, cb, ud, <typed args...>)`,
|
||||
## ctor `void *<name>(<typed args...>, cb, ud)`, dtor `int <name>(ctx)`. The
|
||||
## callback gets the raw result (RET_OK; not NUL-terminated, use len) or raw
|
||||
## error text (RET_ERR). Best for SAME-process callers (zero serialization).
|
||||
## - `<lib>_cbor.h` — CBOR ABI: `int <name>_cbor(ctx, cb, ud, reqCbor, len)`,
|
||||
## ctor `void *<name>_cbor(reqCbor, len, cb, ud)`, plus a small CBOR request
|
||||
## encoder (FfiCbor) and response decoder (ffi_decode_text). The request is a
|
||||
## CBOR map keyed by the Nim param names (noted per proc). Best for callers
|
||||
## that cross a process / machine boundary, where serialization is required
|
||||
## anyway, or any generic / cross-language caller.
|
||||
##
|
||||
## 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).
|
||||
## Both headers guard their shared definitions (RET codes, FFICallBack) so a
|
||||
## translation unit may include both. Library-initiated events (registered via
|
||||
## `<lib>_add_event_listener`) arrive as raw JSON in either case.
|
||||
|
||||
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.
|
||||
## Fixed 64-bit C type for any Nim `ptr T` / `pointer`, matching the other
|
||||
## generators so the layout 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.
|
||||
## Maps a Nim parameter type to the C type used in the declaration.
|
||||
let t = typeName.strip()
|
||||
if t.startsWith("ptr ") or t == "pointer":
|
||||
return "void*"
|
||||
@ -45,47 +47,113 @@ proc nimTypeToC(typeName: string): string =
|
||||
of "float64": "double"
|
||||
else: t
|
||||
|
||||
type CborKind = enum
|
||||
ckText
|
||||
ckUint
|
||||
ckInt
|
||||
ckBool
|
||||
ckUnsupported
|
||||
proc cParam(ep: FFIParamMeta): string =
|
||||
(if ep.isPtr: CPtrType else: nimTypeToC(ep.typeName)) & " " & ep.name
|
||||
|
||||
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
|
||||
proc typedArgs(p: FFIProcMeta): string =
|
||||
var parts: seq[string] = @[]
|
||||
for ep in p.extraParams:
|
||||
parts.add(cParam(ep))
|
||||
return parts.join(", ")
|
||||
|
||||
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).
|
||||
// Native (zero-serialization) C ABI. Each call delivers its result to the
|
||||
// callback: on RET_OK, (msg, len) is the raw return value (for string-returning
|
||||
// procs, the string bytes — not NUL-terminated; use len); on RET_ERR, (msg, len)
|
||||
// is the raw error text. A `<name>_cbor` variant of each proc also exists for
|
||||
// generic/cross-language callers that prefer a CBOR request/response.
|
||||
#ifndef NIM_FFI_GEN_<GUARD>_H
|
||||
#define NIM_FFI_GEN_<GUARD>_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.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
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
"""
|
||||
|
||||
proc generateCHeader*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
): string =
|
||||
let guard = libName.toUpper()
|
||||
var lines: seq[string] = @[]
|
||||
lines.add(HeaderPrelude.replace("<GUARD>", guard))
|
||||
lines.add("")
|
||||
|
||||
for p in procs:
|
||||
let args = typedArgs(p)
|
||||
case p.kind
|
||||
of FFIKind.DTOR:
|
||||
lines.add("int " & p.procName & "(void *ctx);")
|
||||
of FFIKind.CTOR:
|
||||
let lead =
|
||||
if args.len > 0:
|
||||
args & ", "
|
||||
else:
|
||||
""
|
||||
lines.add(
|
||||
"void *" & p.procName & "(" & lead & "FFICallBack callback, void *userData);"
|
||||
)
|
||||
of FFIKind.FFI:
|
||||
let tail =
|
||||
if args.len > 0:
|
||||
", " & args
|
||||
else:
|
||||
""
|
||||
lines.add(
|
||||
"int " & p.procName & "(void *ctx, FFICallBack callback, void *userData" & tail &
|
||||
");"
|
||||
)
|
||||
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")
|
||||
|
||||
const CborHeaderPrelude = """
|
||||
// Generated by nim-ffi C codegen. Do not edit by hand.
|
||||
//
|
||||
// CBOR ABI (`<name>_cbor`). Use this for callers that cross a process or machine
|
||||
// boundary (the request has to be serialized anyway) or any generic / cross-
|
||||
// language caller. Build the request with the FfiCbor helpers below — a CBOR map
|
||||
// whose keys are the Nim parameter names (listed per proc) — call the matching
|
||||
// `<name>_cbor`, and decode the RET_OK response (a CBOR-encoded value; for
|
||||
// string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR
|
||||
// delivers raw error text. For same-process callers, prefer the native `<name>`
|
||||
// ABI in the companion <lib>.h header.
|
||||
#ifndef NIM_FFI_GEN_<GUARD>_CBOR_H
|
||||
#define NIM_FFI_GEN_<GUARD>_CBOR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
@ -102,9 +170,14 @@ extern "C" {
|
||||
#define RET_MISSING_CALLBACK 2
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
|
||||
// --- minimal growable CBOR encoder ----------------------------------------
|
||||
#ifndef NIM_FFI_CBOR_HELPERS
|
||||
#define NIM_FFI_CBOR_HELPERS
|
||||
// --- minimal growable CBOR request encoder --------------------------------
|
||||
typedef struct {
|
||||
uint8_t *buf;
|
||||
size_t cap;
|
||||
@ -129,7 +202,6 @@ static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) {
|
||||
}
|
||||
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) {
|
||||
@ -173,18 +245,11 @@ static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_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.
|
||||
// Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets
|
||||
// *out/*outLen to point INTO `data` (no allocation; valid only while `data` is)
|
||||
// and returns 1; returns 0 for a non-text-string payload.
|
||||
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;
|
||||
@ -214,10 +279,8 @@ static inline int ffi_text_view(const uint8_t *data, size_t len,
|
||||
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.
|
||||
// Owning variant: malloc a NUL-terminated copy. NULL for a non-text payload.
|
||||
// Caller frees.
|
||||
static inline char *ffi_decode_text(const uint8_t *data, size_t len) {
|
||||
const uint8_t *view;
|
||||
size_t slen;
|
||||
@ -228,168 +291,49 @@ static inline char *ffi_decode_text(const uint8_t *data, size_t len) {
|
||||
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
|
||||
#endif // NIM_FFI_CBOR_HELPERS
|
||||
"""
|
||||
|
||||
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.
|
||||
proc requestFieldsComment(p: FFIProcMeta): string =
|
||||
## A one-line note of the CBOR request-map keys (the Nim param names) so a
|
||||
## caller knows what to encode.
|
||||
if p.extraParams.len == 0:
|
||||
return "// request: empty CBOR map (0xA0)"
|
||||
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(", ")
|
||||
parts.add("\"" & ep.name & "\": " & ep.typeName)
|
||||
return "// request map keys: {" & 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*(
|
||||
proc generateCborCHeader*(
|
||||
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(CborHeaderPrelude.replace("<GUARD>", guard))
|
||||
lines.add("")
|
||||
|
||||
for p in procs:
|
||||
case p.kind
|
||||
of FFIKind.DTOR:
|
||||
# No request payload — declare the real export directly.
|
||||
# The destructor takes no request, so it has no CBOR variant.
|
||||
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("")
|
||||
lines.add(requestFieldsComment(p))
|
||||
lines.add(
|
||||
"void *" & p.procName &
|
||||
"_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);"
|
||||
)
|
||||
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("")
|
||||
lines.add(requestFieldsComment(p))
|
||||
lines.add(
|
||||
"int " & p.procName &
|
||||
"_cbor(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);"
|
||||
@ -402,7 +346,7 @@ proc generateCHeader*(
|
||||
lines.add("} // extern \"C\"")
|
||||
lines.add("#endif")
|
||||
lines.add("")
|
||||
lines.add("#endif /* NIM_FFI_GEN_" & guard & "_H */")
|
||||
lines.add("#endif /* NIM_FFI_GEN_" & guard & "_CBOR_H */")
|
||||
return lines.join("\n")
|
||||
|
||||
proc generateCBindings*(
|
||||
@ -413,6 +357,12 @@ proc generateCBindings*(
|
||||
nimSrcRelPath: string,
|
||||
events: seq[FFIEventMeta] = @[],
|
||||
) =
|
||||
# Emit both ABIs so consumers can choose per call site: the native (zero-copy,
|
||||
# same-process) one and the CBOR (boundary-crossing / generic) one.
|
||||
writeFile(
|
||||
outputDir / (libName & ".h"), generateCHeader(procs, types, libName, events)
|
||||
)
|
||||
writeFile(
|
||||
outputDir / (libName & "_cbor.h"),
|
||||
generateCborCHeader(procs, types, libName, events),
|
||||
)
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
## 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.
|
||||
## 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 —
|
||||
@ -154,15 +153,12 @@ proc generateGoFile*(
|
||||
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(" // 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; }"
|
||||
" 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->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) {")
|
||||
|
||||
@ -20,12 +20,29 @@ const EmptyErrorMarker = "unknown error"
|
||||
## the callback's msg ptr non-nil and gives the foreign side a recognizable
|
||||
## fallback to log.
|
||||
|
||||
type PayloadFreeProc* = proc(p: pointer) {.cdecl, raises: [], gcsafe.}
|
||||
## Releases a non-CBOR (native C) request payload. The framework's own CBOR
|
||||
## payload is a single `c_malloc` buffer freed with `c_free`; a native payload
|
||||
## is a C-POD args struct that may own further allocations (e.g. duplicated
|
||||
## C strings), so it needs a per-type destructor.
|
||||
|
||||
type FFIThreadRequest* = object
|
||||
callback*: FFICallBack
|
||||
userData*: pointer
|
||||
reqId*: cstring ## Per-proc Req type name used to look up the handler.
|
||||
data*: ptr UncheckedArray[byte] ## Owned CBOR-encoded request payload.
|
||||
data*: ptr UncheckedArray[byte]
|
||||
## Owned request payload: CBOR bytes when `cborMode`, otherwise an opaque
|
||||
## C-POD args struct (see `payloadFree`).
|
||||
dataLen*: int
|
||||
cborMode*: bool
|
||||
## true -> `data` is CBOR; the handler decodes it and the response is
|
||||
## CBOR-encoded (cross-language / generic dispatch path).
|
||||
## false -> `data` is a native C-POD args struct passed by pointer with no
|
||||
## serialization, and the response is delivered as raw bytes
|
||||
## (the same-process "pure C" path).
|
||||
payloadFree*: PayloadFreeProc
|
||||
## When non-nil, `data` is freed by calling this instead of `c_free` — used
|
||||
## for the native C-POD payload, which owns its duplicated string fields.
|
||||
|
||||
proc allocBaseRequest(
|
||||
callback: FFICallBack, userData: pointer, reqId: cstring
|
||||
@ -39,6 +56,8 @@ proc allocBaseRequest(
|
||||
ret[].reqId = reqId.alloc()
|
||||
ret[].data = nil
|
||||
ret[].dataLen = 0
|
||||
ret[].cborMode = true
|
||||
ret[].payloadFree = nil
|
||||
return ret
|
||||
|
||||
proc copySharedPayload(req: ptr FFIThreadRequest, data: ptr byte, dataLen: int) =
|
||||
@ -113,10 +132,32 @@ proc initFromOwnedShared*(
|
||||
adoptOwnedSharedPayload(ret, data, dataLen)
|
||||
return ret
|
||||
|
||||
proc initNative*(
|
||||
T: typedesc[FFIThreadRequest],
|
||||
callback: FFICallBack,
|
||||
userData: pointer,
|
||||
reqId: cstring,
|
||||
payload: pointer,
|
||||
payloadFree: PayloadFreeProc,
|
||||
): ptr type T =
|
||||
## Builds a native (no-CBOR) request: `payload` is an opaque, already-allocated
|
||||
## C-POD args struct passed by pointer (zero serialization). `payloadFree`
|
||||
## releases it (and any duplicated string fields it owns) after the handler
|
||||
## runs. The response is delivered as raw bytes rather than CBOR.
|
||||
var ret = allocBaseRequest(callback, userData, reqId)
|
||||
ret[].data = cast[ptr UncheckedArray[byte]](payload)
|
||||
ret[].dataLen = 0
|
||||
ret[].cborMode = false
|
||||
ret[].payloadFree = payloadFree
|
||||
return ret
|
||||
|
||||
proc deleteRequest*(request: ptr FFIThreadRequest) =
|
||||
if not request[].data.isNil:
|
||||
c_free(request[].data)
|
||||
if not request[].reqId.isNil:
|
||||
if not request[].data.isNil():
|
||||
if not request[].payloadFree.isNil():
|
||||
request[].payloadFree(cast[pointer](request[].data))
|
||||
else:
|
||||
c_free(request[].data)
|
||||
if not request[].reqId.isNil():
|
||||
c_free(cast[pointer](request[].reqId))
|
||||
c_free(request)
|
||||
|
||||
@ -143,12 +184,19 @@ proc handleRes*(res: Result[seq[byte], string], request: ptr FFIThreadRequest) =
|
||||
cast[csize_t](bytes.len),
|
||||
request[].userData,
|
||||
)
|
||||
else:
|
||||
# Always hand the callback a real buffer; CBOR null marks "no value".
|
||||
elif request[].cborMode:
|
||||
# CBOR path: hand the callback a real buffer; CBOR null marks "no value".
|
||||
var sentinel = CborNullByte
|
||||
request[].callback(
|
||||
RET_OK, cast[ptr cchar](addr sentinel), 1.csize_t, request[].userData
|
||||
)
|
||||
else:
|
||||
# Native path: an empty result is just a zero-length payload. Pass a valid
|
||||
# non-nil pointer with len 0 so the callback never sees a nil msg.
|
||||
var empty: byte = 0
|
||||
request[].callback(
|
||||
RET_OK, cast[ptr cchar](addr empty), 0.csize_t, request[].userData
|
||||
)
|
||||
|
||||
proc nilProcess*(reqId: cstring): Future[Result[seq[byte], string]] {.async.} =
|
||||
return err("This request type is not implemented: " & $reqId)
|
||||
|
||||
@ -602,6 +602,73 @@ macro ffiRaw*(prc: untyped): untyped =
|
||||
echo stmts.repr
|
||||
return stmts
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native (zero-serialization) C-POD payload helpers, shared by the {.ffi.} and
|
||||
# {.ffiCtor.} native code paths. The native path passes the typed args to the
|
||||
# FFI thread inside a c_malloc'd struct (by pointer) instead of CBOR, so the
|
||||
# struct keeps the user's original param types and owns copies of any cstrings.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc isCstringType(t: NimNode): bool =
|
||||
t.kind == nnkIdent and $t == "cstring"
|
||||
|
||||
proc buildCArgsTypeDef(
|
||||
cargsTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode]
|
||||
): NimNode =
|
||||
## `type <cargsTypeName> = object` with one field per param (original types).
|
||||
## Empty param lists get a `placeholder` field so the object is well-formed.
|
||||
var fields: seq[NimNode] = @[]
|
||||
for i in 0 ..< paramNames.len:
|
||||
fields.add(
|
||||
newTree(nnkIdentDefs, ident(paramNames[i]), paramTypes[i], newEmptyNode())
|
||||
)
|
||||
let recList =
|
||||
if fields.len > 0:
|
||||
newTree(nnkRecList, fields)
|
||||
else:
|
||||
newTree(
|
||||
nnkRecList,
|
||||
newTree(nnkIdentDefs, ident("placeholder"), ident("uint8"), newEmptyNode()),
|
||||
)
|
||||
return newNimNode(nnkTypeSection).add(
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
cargsTypeName,
|
||||
newEmptyNode(),
|
||||
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList),
|
||||
)
|
||||
)
|
||||
|
||||
proc buildCArgsFreeProc(
|
||||
cargsTypeName, cargsFreeName: NimNode,
|
||||
paramNames: seq[string],
|
||||
paramTypes: seq[NimNode],
|
||||
): NimNode =
|
||||
## `proc <cargsFreeName>(p: pointer) {.cdecl, raises:[], gcsafe.}` that frees
|
||||
## each owned cstring field (with c_free, matching `alloc`) and then the struct.
|
||||
let freeS = genSym(nskLet, "s")
|
||||
var freeBody = newStmtList()
|
||||
freeBody.add quote do:
|
||||
let `freeS` = cast[ptr `cargsTypeName`](p)
|
||||
for i in 0 ..< paramNames.len:
|
||||
if isCstringType(paramTypes[i]):
|
||||
let f = ident(paramNames[i])
|
||||
freeBody.add quote do:
|
||||
ffiCFree(cast[pointer](`freeS`[].`f`))
|
||||
freeBody.add quote do:
|
||||
ffiCFree(p)
|
||||
return newProc(
|
||||
name = cargsFreeName,
|
||||
params = @[newEmptyNode(), newIdentDefs(ident("p"), ident("pointer"))],
|
||||
body = freeBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
ident("gcsafe"),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ffi macro — primary FFI proc / FFI type registration
|
||||
# ---------------------------------------------------------------------------
|
||||
@ -792,10 +859,150 @@ macro ffi*(prc: untyped): untyped =
|
||||
return RET_ERR
|
||||
return RET_OK
|
||||
|
||||
# The CBOR entry point is the generic / cross-language dispatcher; it keeps
|
||||
# the per-proc Req type name as its handler key and is exported under the
|
||||
# `<name>_cbor` symbol. The native typed-arg entry point (below) is the
|
||||
# primary `<name>` symbol and is preferred for same-process callers.
|
||||
let cborExportName = ident(procNameStr & "CborExport")
|
||||
let ffiProc = newProc(
|
||||
name = postfix(cExportProcName, "*"),
|
||||
name = cborExportName,
|
||||
params = exportedParams,
|
||||
body = ffiBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
newTree(
|
||||
nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName & "_cbor")
|
||||
),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Native (zero-serialization) path: the typed args travel to the FFI thread
|
||||
# inside a c_malloc'd C-POD struct passed by pointer — no CBOR — and the
|
||||
# response is delivered as raw bytes. Registered under a distinct
|
||||
# "<Camel>ReqNative" key so it dispatches to its own handler.
|
||||
# -------------------------------------------------------------------------
|
||||
let cargsTypeName = ident(camelName & "CArgs")
|
||||
let cargsFreeName = ident(camelName & "CArgsFree")
|
||||
let nativeReqIdLit = newStrLitNode(camelName & "ReqNative")
|
||||
let nativeExportName = ident(procNameStr & "NativeExport")
|
||||
|
||||
let cargsTypeDef =
|
||||
buildCArgsTypeDef(cargsTypeName, extraParamNames, extraParamTypes)
|
||||
let cargsFreeProc =
|
||||
buildCArgsFreeProc(cargsTypeName, cargsFreeName, extraParamNames, extraParamTypes)
|
||||
|
||||
# Native FFI-thread handler: read the C-POD, call the helper, raw-encode.
|
||||
let ndReq = genSym(nskLet, "ffiReq")
|
||||
let ndCtx = genSym(nskLet, "nativeCtx")
|
||||
let ndCargs = genSym(nskLet, "cargs")
|
||||
let ndRet = genSym(nskLet, "retVal")
|
||||
var ndBody = newStmtList()
|
||||
ndBody.add quote do:
|
||||
let `ndReq` = cast[ptr FFIThreadRequest](request)
|
||||
let `ndCtx` = cast[ptr FFIContext[`libTypeName`]](reqHandler)
|
||||
let `ndCargs` = cast[ptr `cargsTypeName`](`ndReq`[].data)
|
||||
let ndHelperCall = newTree(
|
||||
nnkCall,
|
||||
userProcName,
|
||||
newTree(nnkDerefExpr, newDotExpr(newTree(nnkDerefExpr, ndCtx), ident("myLib"))),
|
||||
)
|
||||
for nm in extraParamNames:
|
||||
let f = ident(nm)
|
||||
ndBody.add quote do:
|
||||
let `f` = `ndCargs`[].`f`
|
||||
ndHelperCall.add(ident(nm))
|
||||
ndBody.add quote do:
|
||||
let `ndRet` = (await `ndHelperCall`).valueOr:
|
||||
return err($error)
|
||||
when typeof(`ndRet`) is string:
|
||||
var rb = newSeq[byte](`ndRet`.len)
|
||||
if `ndRet`.len > 0:
|
||||
copyMem(addr rb[0], unsafeAddr `ndRet`[0], `ndRet`.len)
|
||||
return ok(rb)
|
||||
elif typeof(`ndRet`) is seq[byte]:
|
||||
return ok(`ndRet`)
|
||||
else:
|
||||
return ok(cborEncode(`ndRet`))
|
||||
let seqByteRet = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(
|
||||
ident("Result"),
|
||||
nnkBracketExpr.newTree(ident("seq"), ident("byte")),
|
||||
ident("string"),
|
||||
),
|
||||
)
|
||||
let nativeHandlerProc = newProc(
|
||||
name = newEmptyNode(),
|
||||
params = @[
|
||||
seqByteRet,
|
||||
newIdentDefs(ident("request"), ident("pointer")),
|
||||
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
||||
],
|
||||
body = ndBody,
|
||||
pragmas = nnkPragma.newTree(ident("async")),
|
||||
)
|
||||
let nativeRegister = newAssignment(
|
||||
newTree(nnkBracketExpr, ident("registeredRequests"), nativeReqIdLit),
|
||||
nativeHandlerProc,
|
||||
)
|
||||
|
||||
# Native C export: build the C-POD (duplicating cstrings) and dispatch.
|
||||
let neCargs = genSym(nskLet, "cargs")
|
||||
let neReq = genSym(nskLet, "nreq")
|
||||
let neSend = genSym(nskLet, "sendRes")
|
||||
var neBody = newStmtList()
|
||||
neBody.add quote do:
|
||||
if callback.isNil:
|
||||
return RET_MISSING_CALLBACK
|
||||
if not `asyncPoolIdent`.isValidCtx(cast[pointer](ctx)):
|
||||
let errStr = "ctx is not a valid FFI context"
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
let `neCargs` = ffiCMalloc(`cargsTypeName`)
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
let f = ident(extraParamNames[i])
|
||||
if isCstringType(extraParamTypes[i]):
|
||||
neBody.add quote do:
|
||||
`neCargs`[].`f` = `f`.alloc()
|
||||
else:
|
||||
neBody.add quote do:
|
||||
`neCargs`[].`f` = `f`
|
||||
neBody.add quote do:
|
||||
let `neReq` = FFIThreadRequest.initNative(
|
||||
callback,
|
||||
userData,
|
||||
`nativeReqIdLit`.cstring,
|
||||
cast[pointer](`neCargs`),
|
||||
`cargsFreeName`,
|
||||
)
|
||||
let `neSend` =
|
||||
try:
|
||||
ffi_context.sendRequestToFFIThread(ctx, `neReq`)
|
||||
except Exception as exc:
|
||||
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
||||
if `neSend`.isErr():
|
||||
let errStr = "error in sendRequestToFFIThread: " & `neSend`.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return RET_ERR
|
||||
return RET_OK
|
||||
var nativeExportParams = @[
|
||||
ident("cint"),
|
||||
newIdentDefs(ident("ctx"), ctxType),
|
||||
newIdentDefs(ident("callback"), ident("FFICallBack")),
|
||||
newIdentDefs(ident("userData"), ident("pointer")),
|
||||
]
|
||||
for i in 0 ..< extraParamNames.len:
|
||||
nativeExportParams.add(
|
||||
newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i])
|
||||
)
|
||||
let nativeExportProc = newProc(
|
||||
name = nativeExportName,
|
||||
params = nativeExportParams,
|
||||
body = neBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
@ -837,7 +1044,10 @@ macro ffi*(prc: untyped): untyped =
|
||||
)
|
||||
)
|
||||
|
||||
return newStmtList(helperProc, registerReq, ffiProc)
|
||||
return newStmtList(
|
||||
helperProc, registerReq, cargsTypeDef, cargsFreeProc, nativeRegister,
|
||||
nativeExportProc, ffiProc,
|
||||
)
|
||||
|
||||
let stmts = asyncPath()
|
||||
|
||||
@ -1209,14 +1419,17 @@ macro ffiCtor*(prc: untyped): untyped =
|
||||
ffiBody.add quote do:
|
||||
return cast[pointer](`ctxSym`)
|
||||
|
||||
# CBOR constructor entry point, exported under `<name>_cbor`. The native
|
||||
# typed-arg constructor below is the primary `<name>` symbol.
|
||||
let cborCtorExportName = ident(cleanName & "CborCtorExport")
|
||||
let ffiProc = newProc(
|
||||
name = postfix(cExportProcName, "*"),
|
||||
name = cborCtorExportName,
|
||||
params = exportedParams,
|
||||
body = ffiBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)),
|
||||
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName & "_cbor")),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
@ -1247,12 +1460,139 @@ macro ffiCtor*(prc: untyped): untyped =
|
||||
)
|
||||
)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Native (zero-serialization) constructor: typed args -> C-POD by pointer,
|
||||
# exported as the primary `<name>` symbol. Returns the ctx pointer; the
|
||||
# callback fires with the ctx address as a raw decimal string.
|
||||
# -------------------------------------------------------------------------
|
||||
let ctorCamel = snakeToPascalCase(cleanName)
|
||||
let cargsTypeName = ident(ctorCamel & "CtorCArgs")
|
||||
let cargsFreeName = ident(ctorCamel & "CtorCArgsFree")
|
||||
let nativeCtorReqIdLit = newStrLitNode(ctorCamel & "CtorReqNative")
|
||||
let nativeCtorExportName = ident(cleanName & "NativeCtorExport")
|
||||
|
||||
let ctorCargsTypeDef = buildCArgsTypeDef(cargsTypeName, paramNames, paramTypes)
|
||||
let ctorCargsFreeProc =
|
||||
buildCArgsFreeProc(cargsTypeName, cargsFreeName, paramNames, paramTypes)
|
||||
|
||||
# Native handler: read the C-POD, run the ctor body, store myLib, raw address.
|
||||
let ncReq = genSym(nskLet, "ffiReq")
|
||||
let ncCtx = genSym(nskLet, "nativeCtx")
|
||||
let ncCargs = genSym(nskLet, "cargs")
|
||||
let ncLibVal = genSym(nskLet, "libVal")
|
||||
let ncAddr = genSym(nskLet, "addrStr")
|
||||
var ncBody = newStmtList()
|
||||
ncBody.add quote do:
|
||||
let `ncReq` = cast[ptr FFIThreadRequest](request)
|
||||
let `ncCtx` = cast[ptr FFIContext[`libTypeName`]](reqHandler)
|
||||
let `ncCargs` = cast[ptr `cargsTypeName`](`ncReq`[].data)
|
||||
let ncHelperCall = newTree(nnkCall, userProcName)
|
||||
for nm in paramNames:
|
||||
let f = ident(nm)
|
||||
ncBody.add quote do:
|
||||
let `f` = `ncCargs`[].`f`
|
||||
ncHelperCall.add(ident(nm))
|
||||
let ncMyLib = newDotExpr(newTree(nnkDerefExpr, ncCtx), ident("myLib"))
|
||||
ncBody.add quote do:
|
||||
let `ncLibVal` = (await `ncHelperCall`).valueOr:
|
||||
return err($error)
|
||||
`ncMyLib` = createShared(`libTypeName`)
|
||||
`ncMyLib`[] = `ncLibVal`
|
||||
let `ncAddr` = $cast[uint](`ncCtx`)
|
||||
var rb = newSeq[byte](`ncAddr`.len)
|
||||
if `ncAddr`.len > 0:
|
||||
copyMem(addr rb[0], unsafeAddr `ncAddr`[0], `ncAddr`.len)
|
||||
return ok(rb)
|
||||
let ctorSeqByteRet = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(
|
||||
ident("Result"),
|
||||
nnkBracketExpr.newTree(ident("seq"), ident("byte")),
|
||||
ident("string"),
|
||||
),
|
||||
)
|
||||
let nativeCtorHandler = newProc(
|
||||
name = newEmptyNode(),
|
||||
params = @[
|
||||
ctorSeqByteRet,
|
||||
newIdentDefs(ident("request"), ident("pointer")),
|
||||
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
||||
],
|
||||
body = ncBody,
|
||||
pragmas = nnkPragma.newTree(ident("async")),
|
||||
)
|
||||
let nativeCtorRegister = newAssignment(
|
||||
newTree(nnkBracketExpr, ident("registeredRequests"), nativeCtorReqIdLit),
|
||||
nativeCtorHandler,
|
||||
)
|
||||
|
||||
# Native C export: create the ctx, build the C-POD (dup cstrings), dispatch.
|
||||
let necCtx = genSym(nskLet, "ctx")
|
||||
let necCargs = genSym(nskLet, "cargs")
|
||||
let necReq = genSym(nskLet, "nreq")
|
||||
let necSend = genSym(nskLet, "sendRes")
|
||||
var necBody = newStmtList()
|
||||
necBody.add quote do:
|
||||
when declared(initializeLibrary):
|
||||
initializeLibrary()
|
||||
let `necCtx` = `poolIdent`.createFFIContext().valueOr:
|
||||
if not callback.isNil:
|
||||
let errStr = "ffiCtor: failed to create FFIContext: " & $error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return nil
|
||||
let `necCargs` = ffiCMalloc(`cargsTypeName`)
|
||||
for i in 0 ..< paramNames.len:
|
||||
let f = ident(paramNames[i])
|
||||
if isCstringType(paramTypes[i]):
|
||||
necBody.add quote do:
|
||||
`necCargs`[].`f` = `f`.alloc()
|
||||
else:
|
||||
necBody.add quote do:
|
||||
`necCargs`[].`f` = `f`
|
||||
necBody.add quote do:
|
||||
let `necReq` = FFIThreadRequest.initNative(
|
||||
callback,
|
||||
userData,
|
||||
`nativeCtorReqIdLit`.cstring,
|
||||
cast[pointer](`necCargs`),
|
||||
`cargsFreeName`,
|
||||
)
|
||||
let `necSend` =
|
||||
try:
|
||||
`necCtx`.sendRequestToFFIThread(`necReq`)
|
||||
except Exception as exc:
|
||||
Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg)
|
||||
if `necSend`.isErr():
|
||||
if not callback.isNil:
|
||||
let errStr = "ffiCtor: failed to send request: " & $`necSend`.error
|
||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||
return nil
|
||||
return cast[pointer](`necCtx`)
|
||||
var nativeCtorParams = @[ident("pointer")]
|
||||
for i in 0 ..< paramNames.len:
|
||||
nativeCtorParams.add(newIdentDefs(ident(paramNames[i]), paramTypes[i]))
|
||||
nativeCtorParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||
nativeCtorParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||
let nativeCtorExportProc = newProc(
|
||||
name = nativeCtorExportName,
|
||||
params = nativeCtorParams,
|
||||
body = necBody,
|
||||
pragmas = newTree(
|
||||
nnkPragma,
|
||||
ident("dynlib"),
|
||||
newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)),
|
||||
ident("cdecl"),
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
)
|
||||
|
||||
let poolDecl = quote:
|
||||
when not declared(`poolIdent`):
|
||||
var `poolIdent`: FFIContextPool[`libTypeName`]
|
||||
|
||||
let stmts = newStmtList(
|
||||
typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc
|
||||
typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc,
|
||||
ctorCargsTypeDef, ctorCargsFreeProc, nativeCtorRegister, nativeCtorExportProc,
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
@ -1421,8 +1761,10 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
|
||||
|
||||
let payloadTypeNameStr =
|
||||
case payloadTypeNode.kind
|
||||
of nnkIdent: $payloadTypeNode
|
||||
else: payloadTypeNode.repr
|
||||
of nnkIdent:
|
||||
$payloadTypeNode
|
||||
else:
|
||||
payloadTypeNode.repr
|
||||
|
||||
var userProcName = procName
|
||||
if procName.kind == nnkPostfix:
|
||||
@ -1430,13 +1772,8 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped =
|
||||
|
||||
# The generated body: dispatchFFIEventCbor("wire_name", payload).
|
||||
let wireNameLit = newStrLitNode(wireName)
|
||||
let dispatchBody = newStmtList(
|
||||
newCall(
|
||||
ident("dispatchFFIEventCbor"),
|
||||
wireNameLit,
|
||||
payloadParamName,
|
||||
)
|
||||
)
|
||||
let dispatchBody =
|
||||
newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, payloadParamName))
|
||||
|
||||
var newParams = newSeq[NimNode]()
|
||||
newParams.add(formalParams[0]) # return type (typically empty/void)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user