feat(ffi): cross struct/seq/Option params natively via POD

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 `<T>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 `<T>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 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 10:41:06 +02:00
parent 4af031c421
commit ad493d6f9d
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
4 changed files with 444 additions and 36 deletions

View File

@ -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)

View File

@ -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 { ... } <Name>;` 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* <f>; size_t <f>_len; }`
## - `Option[T]`/`Maybe[T]` -> `{ int <f>_present; T <f>; }`
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>", guard))
lines.add("")
lines.add(emitCStructs(types))
lines.add("")
for p in procs:
let args = typedArgs(p)

View File

@ -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 `<T>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 `<T>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 <cargsTypeName> = object` with one field per param (original types).
## `type <cargsTypeName> = object` with one field per param, each typed as its
## native *wire* type (`<T>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 <cargsFreeName>(p: pointer) {.cdecl, raises:[], gcsafe.}` that frees
## each owned cstring field (with c_free, matching `alloc`) and then the struct.
## `proc <cargsFreeName>(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

287
ffi/internal/native_pod.nim Normal file
View File

@ -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)