From ad493d6f9dfce4c247eb89339ac7b4abd8f82ad4 Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sun, 31 May 2026 10:41:06 +0200 Subject: [PATCH] feat(ffi): cross struct/seq/Option params natively via POD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The native (zero-serialization) path previously handed `{.ffi.}` struct params to the FFI thread using the Nim object layout (GC'd `string` fields), which does not match the C-POD layout the generated header declares — an ABI mismatch that left struct-param procs uncallable from C and skipped by the Go codegen. Wire the generated POD machinery into both the `{.ffi.}` and `{.ffiCtor.}` native paths: a registered `{.ffi.}` struct now travels as its `Pod` mirror — `clonePod` deep-copies it off the caller's buffers into shared (`c_malloc`) memory on the caller thread, `podToNim` rebuilds the Nim value on the FFI thread, and `freePod` releases it from the CArgs free proc. `string` collapses to `cstring` (alloc/ffiCFree); scalars copy direct. New classifiers (`nativeWireType` / `nativeArgCopyStmt` / `nativeArgUnpackStmt`) keep both paths and the CArgs alloc/free in lockstep so ownership can't drift. The load-bearing invariant: the `Pod` `{.bycopy.}` layout is identical to the C struct emitted by `codegen/c.emitCStructs`, so the `exportc` symbol's ABI matches the header even though Nim's own struct name differs. Keep the two emitters in sync. Validated end-to-end from C (TimerConfig, EchoRequest, and a nested ComplexRequest with seq-of-structs, seq-of-strings and two Options) and clean under ASAN. Struct *returns* still travel as CBOR on the native path; that is left for a follow-up. Co-Authored-By: Claude Opus 4.8 --- ffi/alloc.nim | 10 ++ ffi/codegen/c.nim | 37 +++++ ffi/internal/ffi_macro.nim | 146 +++++++++++++----- ffi/internal/native_pod.nim | 287 ++++++++++++++++++++++++++++++++++++ 4 files changed, 444 insertions(+), 36 deletions(-) create mode 100644 ffi/internal/native_pod.nim diff --git a/ffi/alloc.nim b/ffi/alloc.nim index 4577313..c105757 100644 --- a/ffi/alloc.nim +++ b/ffi/alloc.nim @@ -59,6 +59,16 @@ proc ffiCFree*(p: pointer) {.inline.} = if not p.isNil(): c_free(p) +proc ffiCAllocArray*(T: typedesc, n: int): ptr UncheckedArray[T] = + ## Allocates a zero-initialised array of `n` × `T` via `c_malloc` so it can + ## cross threads safely. Used by the native POD codegen for `seq[T]` fields; + ## release with `ffiCFree`. Returns nil for a non-positive count. + if n <= 0: + return nil + let p = c_malloc(csize_t(sizeof(T) * n)) + zeroMem(p, sizeof(T) * n) + return cast[ptr UncheckedArray[T]](p) + proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] = if s.len == 0: return (cast[ptr UncheckedArray[T]](nil), 0) diff --git a/ffi/codegen/c.nim b/ffi/codegen/c.nim index db935d2..b8fcc48 100644 --- a/ffi/codegen/c.nim +++ b/ffi/codegen/c.nim @@ -56,6 +56,41 @@ proc typedArgs(p: FFIProcMeta): string = parts.add(cParam(ep)) return parts.join(", ") +proc innerOf(t, prefix: string): string = + ## Strips a `Prefix[...]` wrapper, returning the inner type name. + t[prefix.len .. ^2] + +proc emitCStructs(types: seq[FFITypeMeta]): seq[string] = + ## Emits a `typedef struct { ... } ;` for every `{.ffi.}` type so the + ## native header is self-contained (no undefined `TimerConfig` / `EchoRequest` + ## references). Field layout mirrors the Nim object so the struct can be passed + ## by value to the native entry points: + ## - scalar / bool / float / nested `{.ffi.}` struct -> the matching C type + ## - `cstring` (use this, not `string`, for native ABI) -> `const char*` + ## - `seq[T]` -> `{ const T* ; size_t _len; }` + ## - `Option[T]`/`Maybe[T]` -> `{ int _present; T ; }` + var lines: seq[string] = @[] + if types.len > 0: + lines.add("// --- {.ffi.}-annotated types, exposed as C structs ----------") + for t in types: + lines.add("typedef struct {") + for f in t.fields: + let ft = f.typeName.strip() + if ft.startsWith("seq[") and ft.endsWith("]"): + lines.add(" " & nimTypeToC(innerOf(ft, "seq[")) & " *" & f.name & ";") + lines.add(" size_t " & f.name & "_len;") + elif ft.startsWith("Option[") and ft.endsWith("]"): + lines.add(" int " & f.name & "_present;") + lines.add(" " & nimTypeToC(innerOf(ft, "Option[")) & " " & f.name & ";") + elif ft.startsWith("Maybe[") and ft.endsWith("]"): + lines.add(" int " & f.name & "_present;") + lines.add(" " & nimTypeToC(innerOf(ft, "Maybe[")) & " " & f.name & ";") + else: + lines.add(" " & nimTypeToC(ft) & " " & f.name & ";") + lines.add("} " & t.name & ";") + lines.add("") + return lines + const HeaderPrelude = """ // Generated by nim-ffi C codegen. Do not edit by hand. // @@ -97,6 +132,8 @@ proc generateCHeader*( var lines: seq[string] = @[] lines.add(HeaderPrelude.replace("", guard)) lines.add("") + lines.add(emitCStructs(types)) + lines.add("") for p in procs: let args = typedArgs(p) diff --git a/ffi/internal/ffi_macro.nim b/ffi/internal/ffi_macro.nim index 17e4f9c..f30549d 100644 --- a/ffi/internal/ffi_macro.nim +++ b/ffi/internal/ffi_macro.nim @@ -2,6 +2,7 @@ import std/[macros, tables, strutils] import chronos import ../ffi_types import ../codegen/[meta, string_helpers] +import ./native_pod when defined(ffiGenBindings): import ../codegen/rust import ../codegen/cpp @@ -74,7 +75,20 @@ proc registerFFITypeInfo(typeDef: NimNode): NimNode {.compileTime.} = for i in 0 ..< identDef.len - 2: fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName)) + # Names of all {.ffi.} types registered *before* this one — used to classify + # nested-struct fields in the POD machinery (forward refs aren't supported, + # but a type can only reference earlier-declared {.ffi.} types anyway). + var known: seq[string] = @[] + for t in ffiTypeRegistry: + known.add(t.name) + ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas)) + + # Queue the native POD mirror + clone/podToNim/nimToPod/freePod overloads so + # deep nested structures cross the FFI-thread boundary as deep-copied + # shared-memory C-POD graphs (no GC memory, no aliasing). The procs are + # flushed into the next proc-macro expansion (see flushPendingPods). + queuePodMachinery(typeNameStr, fieldMetas, known) return typeDef proc nimTypeNameRepr(typ: NimNode): string = @@ -612,15 +626,78 @@ macro ffiRaw*(prc: untyped): untyped = proc isCstringType(t: NimNode): bool = t.kind == nnkIdent and $t == "cstring" +proc isStringNode(t: NimNode): bool = + t.kind in {nnkIdent, nnkSym} and $t == "string" + +proc isFFIStructType(t: NimNode): bool {.compileTime.} = + ## True if `t` names a registered `{.ffi.}` object type — i.e. one that has a + ## generated `Pod` mirror plus clonePod/podToNim/freePod overloads. + if t.kind in {nnkIdent, nnkSym}: + let s = $t + for reg in ffiTypeRegistry: + if reg.name == s: + return true + return false + +proc nativeWireType(t: NimNode): NimNode {.compileTime.} = + ## The C-ABI-safe type carrying param `t` across the native (non-CBOR) boundary. + ## A registered `{.ffi.}` struct travels as its `Pod` mirror — laid out + ## *identically* to the C-header struct emitted by `codegen/c.emitCStructs`, so + ## the `exportc` symbol's ABI matches the header even though Nim's own struct + ## name differs. `string` collapses to `cstring`; scalars are already POD. + if isFFIStructType(t): + ident($t & "Pod") + elif isStringNode(t): + ident("cstring") + else: + t + +proc nativeArgCopyStmt(cargs, f, t: NimNode): NimNode {.compileTime.} = + ## Caller-thread deep copy of param `f` into the shared-memory CArgs field: + ## a `{.ffi.}` struct is `clonePod`'d (recursive deep copy off the caller's + ## buffers); a string/cstring is duplicated via `alloc`; a scalar is copied. + if isFFIStructType(t): + quote: + `cargs`[].`f` = clonePod(`f`) + elif isStringNode(t) or isCstringType(t): + quote: + `cargs`[].`f` = `f`.alloc() + else: + quote: + `cargs`[].`f` = `f` + +proc nativeArgUnpackStmt(cargs, f, t: NimNode): NimNode {.compileTime.} = + ## FFI-thread reconstruction of the Nim-typed local the user body expects from + ## the shared CArgs field: `podToNim` for a struct, a fresh Nim `string` for a + ## `string` param, the field as-is for a `cstring`/scalar. + if isFFIStructType(t): + quote: + let `f` = podToNim(`cargs`[].`f`) + elif isStringNode(t): + quote: + let `f` = + if `cargs`[].`f`.isNil: + "" + else: + $`cargs`[].`f` + else: + quote: + let `f` = `cargs`[].`f` + proc buildCArgsTypeDef( cargsTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] ): NimNode = - ## `type = object` with one field per param (original types). + ## `type = object` with one field per param, each typed as its + ## native *wire* type (`Pod` for a `{.ffi.}` struct, `cstring` for a string) + ## so the struct owns shared-memory copies that cross the FFI thread safely. ## 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()) + newTree( + nnkIdentDefs, ident(paramNames[i]), nativeWireType(paramTypes[i]), + newEmptyNode(), + ) ) let recList = if fields.len > 0: @@ -644,15 +721,20 @@ proc buildCArgsFreeProc( paramNames: seq[string], paramTypes: seq[NimNode], ): NimNode = - ## `proc (p: pointer) {.cdecl, raises:[], gcsafe.}` that frees - ## each owned cstring field (with c_free, matching `alloc`) and then the struct. + ## `proc (p: pointer) {.cdecl, raises:[], gcsafe.}` that releases + ## every owned field — `freePod` for a `{.ffi.}` struct (recursive), `ffiCFree` + ## for a duplicated string/cstring — and then the struct itself. Built from the + ## same param list as `nativeArgCopyStmt` so allocation and release can't drift. 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]) + let f = ident(paramNames[i]) + if isFFIStructType(paramTypes[i]): + freeBody.add quote do: + freePod(`freeS`[].`f`) + elif isStringNode(paramTypes[i]) or isCstringType(paramTypes[i]): freeBody.add quote do: ffiCFree(cast[pointer](`freeS`[].`f`)) freeBody.add quote do: @@ -910,11 +992,10 @@ macro ffi*(prc: untyped): untyped = 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)) + for i in 0 ..< extraParamNames.len: + let f = ident(extraParamNames[i]) + ndBody.add(nativeArgUnpackStmt(ndCargs, f, extraParamTypes[i])) + ndHelperCall.add(f) ndBody.add quote do: let `ndRet` = (await `ndHelperCall`).valueOr: return err($error) @@ -965,12 +1046,7 @@ macro ffi*(prc: untyped): untyped = 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(nativeArgCopyStmt(neCargs, f, extraParamTypes[i])) neBody.add quote do: let `neReq` = FFIThreadRequest.initNative( callback, @@ -997,7 +1073,7 @@ macro ffi*(prc: untyped): untyped = ] for i in 0 ..< extraParamNames.len: nativeExportParams.add( - newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i]) + newIdentDefs(ident(extraParamNames[i]), nativeWireType(extraParamTypes[i])) ) let nativeExportProc = newProc( name = nativeExportName, @@ -1049,7 +1125,7 @@ macro ffi*(prc: untyped): untyped = nativeExportProc, ffiProc, ) - let stmts = asyncPath() + let stmts = newStmtList(flushPendingPods(), asyncPath()) when defined(ffiDumpMacros): echo stmts.repr @@ -1487,11 +1563,10 @@ macro ffiCtor*(prc: untyped): untyped = 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)) + for i in 0 ..< paramNames.len: + let f = ident(paramNames[i]) + ncBody.add(nativeArgUnpackStmt(ncCargs, f, paramTypes[i])) + ncHelperCall.add(f) let ncMyLib = newDotExpr(newTree(nnkDerefExpr, ncCtx), ident("myLib")) ncBody.add quote do: let `ncLibVal` = (await `ncHelperCall`).valueOr: @@ -1543,12 +1618,7 @@ macro ffiCtor*(prc: untyped): untyped = 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(nativeArgCopyStmt(necCargs, f, paramTypes[i])) necBody.add quote do: let `necReq` = FFIThreadRequest.initNative( callback, @@ -1570,7 +1640,9 @@ macro ffiCtor*(prc: untyped): untyped = 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(paramNames[i]), nativeWireType(paramTypes[i])) + ) nativeCtorParams.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) nativeCtorParams.add(newIdentDefs(ident("userData"), ident("pointer"))) let nativeCtorExportProc = newProc( @@ -1591,8 +1663,9 @@ macro ffiCtor*(prc: untyped): untyped = var `poolIdent`: FFIContextPool[`libTypeName`] let stmts = newStmtList( - typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc, - ctorCargsTypeDef, ctorCargsFreeProc, nativeCtorRegister, nativeCtorExportProc, + flushPendingPods(), typeDef, ffiNewReqProc, helperProc, processProc, addToReg, + poolDecl, ffiProc, ctorCargsTypeDef, ctorCargsFreeProc, nativeCtorRegister, + nativeCtorExportProc, ) when defined(ffiDumpMacros): @@ -1709,7 +1782,7 @@ macro ffiDtor*(prc: untyped): untyped = when not declared(`poolIdent`): var `poolIdent`: FFIContextPool[`libTypeName`] - let stmts = newStmtList(poolDecl, ffiProc) + let stmts = newStmtList(flushPendingPods(), poolDecl, ffiProc) when defined(ffiDumpMacros): echo stmts.repr @@ -1802,9 +1875,10 @@ macro ffiEvent*(wireName: static[string], prc: untyped): untyped = ) ) + let withPods = newStmtList(flushPendingPods(), generated) when defined(ffiDumpMacros): - echo generated.repr - return generated + echo withPods.repr + return withPods # --------------------------------------------------------------------------- # genBindings — codegen entry point diff --git a/ffi/internal/native_pod.nim b/ffi/internal/native_pod.nim new file mode 100644 index 0000000..413af80 --- /dev/null +++ b/ffi/internal/native_pod.nim @@ -0,0 +1,287 @@ +## Compile-time generator for the native "POD mirror" machinery of a `{.ffi.}` +## type. For each registered type T it emits a C-struct-layout mirror `TPod` +## plus four overloads that move data across the FFI-thread boundary as a +## deep-copied POD graph in shared (`c_malloc`) memory — never GC'd memory, +## never aliasing the caller's or Nim's buffers: +## +## clonePod(TPod): TPod deep-copy incoming args off caller memory (request) +## podToNim(TPod): T rebuild the Nim object on the FFI thread +## nimToPod(T): TPod build a shared POD graph from a result / event +## freePod(var TPod) recursive free; generated from the same field list +## as the copy so the two cannot drift +## +## The emitted source is parsed with `parseStmt`; the shape mirrors the +## hand-validated scratch (ASAN- and leak-clean) so the codegen stays auditable. +## Runtime helpers (`alloc`/`dealloc`/`ffiCAllocArray`/`ffiCFree`) come from +## `ffi/alloc` and are visible wherever the user did `import ffi`. + +import std/[strutils, macros] +import ../codegen/meta + +proc podName(t: string): string = + t.strip() & "Pod" + +proc isStringType(t: string): bool = + t in ["string", "cstring"] + +proc isOptionType(t: string): bool = + (t.startsWith("Option[") or t.startsWith("Maybe[")) and t.endsWith("]") + +proc isSeqType(t: string): bool = + t.startsWith("seq[") and t.endsWith("]") + +proc optionInner(t: string): string = + let p = if t.startsWith("Maybe["): 6 else: 7 + t[p .. ^2].strip() + +proc seqInner(t: string): string = + t[4 .. ^2].strip() + +proc podScalarType(t: string): string = + case t.strip() + 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": "cint" + of "float", "float64": "cdouble" + of "float32": "cfloat" + else: t.strip() + +proc elemPodType(t: string, known: seq[string]): string = + ## C-struct field type used for one element of a seq / payload of an Option. + let s = t.strip() + if isStringType(s): + "cstring" + elif s in known: + podName(s) + else: + podScalarType(s) + +# --- element-granular conversion expressions -------------------------------- +# `src` is an expression yielding the element on the source side. + +proc cloneElem(t, src: string, known: seq[string]): string = + let s = t.strip() + if isStringType(s): "alloc(" & src & ")" + elif s in known: "clonePod(" & src & ")" + else: src + +proc toNimElem(t, src: string, known: seq[string]): string = + let s = t.strip() + if s == "string": "(if " & src & ".isNil: \"\" else: $" & src & ")" + elif s == "cstring": src + elif s in known: "podToNim(" & src & ")" + elif s == "bool": "(" & src & " != 0)" + else: src & "." & s + +proc toPodElem(t, src: string, known: seq[string]): string = + let s = t.strip() + if isStringType(s): "alloc(" & src & ")" + elif s in known: "nimToPod(" & src & ")" + elif s == "bool": "(if " & src & ": 1 else: 0).cint" + else: src & "." & podScalarType(s) + +proc freeElem(t, access: string, known: seq[string]): string = + ## Statement (or "" when nothing to free) releasing one element. + let s = t.strip() + if isStringType(s): "dealloc(" & access & ")" + elif s in known: "freePod(" & access & ")" + else: "" + +proc elemUserType(t: string): string = + t.strip() + +# --------------------------------------------------------------------------- +# Per-field source fragments. Each returns indented lines (2 spaces). +# --------------------------------------------------------------------------- + +type FieldSrc = object + podDecl: seq[string] + clone: seq[string] + toNim: seq[string] + toPod: seq[string] + free: seq[string] + +proc fieldSrc(name, typ: string, known: seq[string]): FieldSrc = + var fs = FieldSrc() + let t = typ.strip() + + if isSeqType(t): + let e = seqInner(t) + let ept = elemPodType(e, known) + fs.podDecl = @[" " & name & ": ptr UncheckedArray[" & ept & "]", " " & name & "Len: csize_t"] + fs.clone = + @[ + " r." & name & "Len = s." & name & "Len", + " if s." & name & "Len.int > 0 and not s." & name & ".isNil:", + " r." & name & " = ffiCAllocArray(" & ept & ", s." & name & "Len.int)", + " for i in 0 ..< s." & name & "Len.int:", + " r." & name & "[i] = " & cloneElem(e, "s." & name & "[i]", known), + " else:", + " r." & name & " = nil", + ] + fs.toNim = + @[ + " r." & name & " = newSeq[" & elemUserType(e) & "](s." & name & "Len.int)", + " for i in 0 ..< s." & name & "Len.int:", + " r." & name & "[i] = " & toNimElem(e, "s." & name & "[i]", known), + ] + fs.toPod = + @[ + " r." & name & "Len = s." & name & ".len.csize_t", + " if s." & name & ".len > 0:", + " r." & name & " = ffiCAllocArray(" & ept & ", s." & name & ".len)", + " for i in 0 ..< s." & name & ".len:", + " r." & name & "[i] = " & toPodElem(e, "s." & name & "[i]", known), + " else:", + " r." & name & " = nil", + ] + let fe = freeElem(e, "p." & name & "[i]", known) + fs.free.add(" if not p." & name & ".isNil:") + if fe.len > 0: + fs.free.add(" for i in 0 ..< p." & name & "Len.int:") + fs.free.add(" " & fe) + fs.free.add(" ffiCFree(cast[pointer](p." & name & "))") + fs.free.add(" p." & name & " = nil") + return fs + + if isOptionType(t): + let e = optionInner(t) + let ept = elemPodType(e, known) + fs.podDecl = @[" " & name & "Present: cint", " " & name & ": " & ept] + fs.clone = + @[ + " r." & name & "Present = s." & name & "Present", + " if s." & name & "Present != 0:", + " r." & name & " = " & cloneElem(e, "s." & name, known), + ] + fs.toNim = + @[ + " if s." & name & "Present != 0:", + " r." & name & " = some(" & toNimElem(e, "s." & name, known) & ")", + " else:", + " r." & name & " = none(" & elemUserType(e) & ")", + ] + fs.toPod = + @[ + " r." & name & "Present = (if s." & name & ".isSome: 1 else: 0).cint", + " if s." & name & ".isSome:", + " r." & name & " = " & toPodElem(e, "s." & name & ".get", known), + ] + let fe = freeElem(e, "p." & name, known) + if fe.len > 0: + fs.free = @[" if p." & name & "Present != 0:", " " & fe] + return fs + + if isStringType(t): + fs.podDecl = @[" " & name & ": cstring"] + fs.clone = @[" r." & name & " = alloc(s." & name & ")"] + if t == "cstring": + fs.toNim = @[" r." & name & " = s." & name] + else: + fs.toNim = @[" r." & name & " = (if s." & name & ".isNil: \"\" else: $s." & name & ")"] + fs.toPod = @[" r." & name & " = alloc(s." & name & ")"] + fs.free = @[" dealloc(p." & name & ")", " p." & name & " = nil"] + return fs + + if t in known: # nested {.ffi.} struct, by value + fs.podDecl = @[" " & name & ": " & podName(t)] + fs.clone = @[" r." & name & " = clonePod(s." & name & ")"] + fs.toNim = @[" r." & name & " = podToNim(s." & name & ")"] + fs.toPod = @[" r." & name & " = nimToPod(s." & name & ")"] + fs.free = @[" freePod(p." & name & ")"] + return fs + + # scalar / bool / float + fs.podDecl = @[" " & name & ": " & podScalarType(t)] + fs.clone = @[" r." & name & " = s." & name] + if t == "bool": + fs.toNim = @[" r." & name & " = (s." & name & " != 0)"] + fs.toPod = @[" r." & name & " = (if s." & name & ": 1 else: 0).cint"] + else: + fs.toNim = @[" r." & name & " = s." & name & "." & t] + fs.toPod = @[" r." & name & " = s." & name & "." & podScalarType(t)] + return fs + +proc buildPodSource*( + typeName: string, fields: seq[FFIFieldMeta], known: seq[string] +): string = + ## Emits the full POD-mirror + 4-overload source block for `typeName`. + let pod = podName(typeName) + var frags: seq[FieldSrc] = @[] + for f in fields: + frags.add(fieldSrc(f.name, f.typeName, known)) + + var L: seq[string] = @[] + + # POD mirror type + L.add("type " & pod & " {.bycopy.} = object") + if frags.len == 0: + L.add(" discardField: uint8") # keep the object non-empty / well-formed + else: + for fr in frags: + L.add(fr.podDecl) + L.add("") + + # freePod + L.add("proc freePod(p: var " & pod & ") =") + var freeBody: seq[string] = @[] + for fr in frags: + freeBody.add(fr.free) + if freeBody.len == 0: + L.add(" discard") + else: + L.add(freeBody) + L.add("") + + # clonePod + L.add("proc clonePod(s: " & pod & "): " & pod & " =") + L.add(" var r: " & pod) + for fr in frags: + L.add(fr.clone) + L.add(" return r") + L.add("") + + # podToNim + L.add("proc podToNim(s: " & pod & "): " & typeName & " =") + L.add(" var r: " & typeName) + for fr in frags: + L.add(fr.toNim) + L.add(" return r") + L.add("") + + # nimToPod + L.add("proc nimToPod(s: " & typeName & "): " & pod & " =") + L.add(" var r: " & pod) + for fr in frags: + L.add(fr.toPod) + L.add(" return r") + L.add("") + + return L.join("\n") + +var pendingPodSources* {.compileTime.}: seq[string] + ## POD-machinery source queued by `{.ffi.}` type registration, drained into + ## the next proc-macro expansion. A type-pragma macro can't return a + ## `StmtList` of (type + procs) — Nim rejects it as illformed — so the procs + ## ride along with the following `.ffi.`/ctor/dtor proc instead (statement + ## context, where a `StmtList` is legal). Every type is declared before the + ## proc that uses it, so its overloads are in scope by the time they're called. + +proc queuePodMachinery*( + typeName: string, fields: seq[FFIFieldMeta], known: seq[string] +) {.compileTime.} = + pendingPodSources.add(buildPodSource(typeName, fields, known)) + +proc flushPendingPods*(): NimNode {.compileTime.} = + ## Drains the queued POD machinery into an AST block (empty when none pending). + if pendingPodSources.len == 0: + return newStmtList() + let src = pendingPodSources.join("\n") + pendingPodSources.setLen(0) + return parseStmt(src)