mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-23 09:49:48 +00:00
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:
parent
4af031c421
commit
ad493d6f9d
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
287
ffi/internal/native_pod.nim
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user