mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-07-30 03:23:31 +00:00
feat(ffi): cwire codec composite fields (#88)
This commit is contained in:
parent
6c4657ad7e
commit
f6881274ca
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@ -80,6 +80,18 @@ jobs:
|
||||
nim-versions: ${{ needs.versions.outputs.nim-versions }}
|
||||
nimble-version: ${{ needs.versions.outputs.nimble }}
|
||||
|
||||
c-wire:
|
||||
# The `c`-ABI cwire codec is layout-/allocator-sensitive (malloc/free, flat
|
||||
# struct packing, allocShared for seq/Option), so cover it across the full
|
||||
# OS matrix, not just Linux.
|
||||
name: C Wire Codec
|
||||
needs: versions
|
||||
uses: ./.github/workflows/test.yml
|
||||
with:
|
||||
test: test_c_wire
|
||||
nim-versions: ${{ needs.versions.outputs.nim-versions }}
|
||||
nimble-version: ${{ needs.versions.outputs.nimble }}
|
||||
|
||||
cpp-e2e:
|
||||
# Codegen output doesn't vary with mm, so we matrix over OS and Nim only.
|
||||
# Windows runs MSVC by default and may surface codegen tweaks needed in
|
||||
|
||||
@ -1,10 +1,19 @@
|
||||
## Compile-time helpers used by `ffi_macro.nim` for the `c` (flat C-struct) ABI.
|
||||
## For each `{.ffi: "abi = c".}` object T, emits a `T_CWire` companion (`string`
|
||||
## → `cstring`, POD unchanged) plus `cwirePack` / `cwireUnpack` / `cwireFree`.
|
||||
## For each `{.ffi: "abi = c".}` object T, emits a `T_CWire` companion plus
|
||||
## `cwirePack` / `cwireUnpack` / `cwireFree`. Field mapping: `string`→`cstring`,
|
||||
## `seq[T]`→`<name>_items`+`<name>_len`, `Option[T]`/`Maybe[T]`→`ptr T_w`
|
||||
## (nil=none), nested {.ffi.}→`T_CWire`, `array[N, T]`→inline `array[N, T_w]`,
|
||||
## `tuple[a: T, ...]`→`tuple[a: T_w, ...]`, POD unchanged. seq/Option/array/tuple
|
||||
## nest to any depth, but a `seq` may not nest inside another container (it has
|
||||
## no single-field wire form — only the top-level `_items`/`_len` split).
|
||||
|
||||
import std/macros
|
||||
import ../codegen/meta
|
||||
|
||||
const
|
||||
cwireItemsSuffix = "_items"
|
||||
cwireLenSuffix = "_len"
|
||||
|
||||
var emittedCWireTypes {.compileTime.}: seq[string]
|
||||
|
||||
proc isCWireEmitted(typeName: string): bool {.compileTime.} =
|
||||
@ -23,41 +32,113 @@ proc cwireTypeName(userTypeName: string): string =
|
||||
## Companion-type naming convention; stable so generated tests reach in by name.
|
||||
userTypeName & "_CWire"
|
||||
|
||||
proc seqItemsField(obj, field: NimNode): NimNode =
|
||||
## `obj.<field>_items` — the buffer half of a seq's two-field wire split.
|
||||
newDotExpr(obj, ident($field & cwireItemsSuffix))
|
||||
|
||||
proc seqLenField(obj, field: NimNode): NimNode =
|
||||
## `obj.<field>_len` — the count half of a seq's two-field wire split.
|
||||
newDotExpr(obj, ident($field & cwireLenSuffix))
|
||||
|
||||
proc isStringType(t: NimNode): bool =
|
||||
t.kind == nnkIdent and ($t == "string" or $t == "cstring")
|
||||
|
||||
const cWireSupportedTypes = [
|
||||
"int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32",
|
||||
"uint64", "float", "float32", "float64", "bool", "char", "byte", "string", "cstring",
|
||||
]
|
||||
proc isBracketOf(t: NimNode, heads: openArray[string]): bool =
|
||||
t.kind == nnkBracketExpr and t.len >= 2 and t[0].kind == nnkIdent and $t[0] in heads
|
||||
|
||||
proc assertCWireFieldSupported(
|
||||
typeName, fieldName: string, fieldType: NimNode
|
||||
) {.compileTime.} =
|
||||
## Gate the `abi = c` whitelist: the flat C codec handles only fixed-width
|
||||
## scalars, `bool`, `char`/`byte`, and `string`. Composite types (`seq[T]`,
|
||||
## `Opt[T]`, nested objects, ...) must use `abi = cbor`, which supports every
|
||||
## type. Future PRs widen this list as the codec grows.
|
||||
if fieldType.kind == nnkIdent and $fieldType in cWireSupportedTypes:
|
||||
return
|
||||
proc isSeqType(t: NimNode): bool =
|
||||
isBracketOf(t, ["seq"])
|
||||
|
||||
proc isOptionType(t: NimNode): bool =
|
||||
isBracketOf(t, ["Option", "Maybe"])
|
||||
|
||||
proc isArrayType(t: NimNode): bool =
|
||||
t.kind == nnkBracketExpr and t.len == 3 and t[0].kind == nnkIdent and $t[0] == "array"
|
||||
|
||||
proc isTupleType(t: NimNode): bool =
|
||||
t.kind == nnkTupleTy
|
||||
|
||||
proc tupleComponents(t: NimNode): seq[tuple[name: string, typ: NimNode]] =
|
||||
## Flatten a named-tuple type into `(name, type)` pairs, expanding grouped
|
||||
## declarations like `tuple[a, b: int]` into one entry per name.
|
||||
var comps: seq[tuple[name: string, typ: NimNode]] = @[]
|
||||
for defs in t:
|
||||
if defs.kind != nnkIdentDefs:
|
||||
error("cwire: only named tuples are supported: " & t.repr)
|
||||
let typ = defs[^2]
|
||||
for i in 0 ..< defs.len - 2:
|
||||
comps.add((name: $defs[i], typ: typ))
|
||||
comps
|
||||
|
||||
proc isKnownFFIType(name: string): bool {.compileTime.} =
|
||||
for typeMeta in ffiTypeRegistry:
|
||||
if typeMeta.name == name:
|
||||
return true
|
||||
false
|
||||
|
||||
proc isNestedFFIType(t: NimNode): bool =
|
||||
t.kind == nnkIdent and isKnownFFIType($t)
|
||||
|
||||
proc cwireNeedsFree(t: NimNode): bool =
|
||||
## Whether the wire form of `t` owns shared-memory allocations that
|
||||
## `cwireFree` must release. POD scalars (and aggregates entirely of POD)
|
||||
## own nothing, so their free is elided.
|
||||
if isStringType(t) or isNestedFFIType(t) or isOptionType(t) or isSeqType(t):
|
||||
return true
|
||||
if isArrayType(t):
|
||||
return cwireNeedsFree(t[2])
|
||||
if isTupleType(t):
|
||||
for c in tupleComponents(t):
|
||||
if cwireNeedsFree(c.typ):
|
||||
return true
|
||||
return false
|
||||
false
|
||||
|
||||
proc rejectNestedSeq(t: NimNode) =
|
||||
## `seq` has no single-field wire form (only the top-level `_items`/`_len`
|
||||
## split), so it can't sit inside another container. One message, one place.
|
||||
error(
|
||||
"ffi 'abi = c' type '" & typeName & "': field '" & fieldName & "' of type '" &
|
||||
repr(fieldType) &
|
||||
"' is not supported by the flat C codec (supported: scalars, bool, char, " &
|
||||
"string). Use 'abi = cbor' for composite types."
|
||||
"cwire: `seq` has no single-field wire form, so it can't nest inside " &
|
||||
"another container (use it only as a top-level field): " & t.repr
|
||||
)
|
||||
|
||||
proc wireFieldType(userType: NimNode): NimNode =
|
||||
## Map a user field type AST → its wire-form AST: `string` → `cstring`,
|
||||
## everything else unchanged.
|
||||
if isStringType(userType):
|
||||
proc wireValueType(t: NimNode): NimNode =
|
||||
## Single-field wire form of value type `t`: `string`→`cstring`, nested
|
||||
## {.ffi.}→`T_CWire`, `Option[T]`→`ptr <wireOf T>`, `array[N, T]`→
|
||||
## `array[N, <wireOf T>]`, `tuple[a: T, ...]`→`tuple[a: <wireOf T>, ...]`,
|
||||
## POD unchanged. `seq` has no single-field form, so it errors here.
|
||||
if isStringType(t):
|
||||
return ident("cstring")
|
||||
userType
|
||||
if isNestedFFIType(t):
|
||||
return ident(cwireTypeName($t))
|
||||
if isOptionType(t):
|
||||
return nnkPtrTy.newTree(wireValueType(t[1]))
|
||||
if isArrayType(t):
|
||||
return
|
||||
nnkBracketExpr.newTree(ident("array"), t[1].copyNimTree(), wireValueType(t[2]))
|
||||
if isTupleType(t):
|
||||
let wireTup = nnkTupleTy.newTree()
|
||||
for c in tupleComponents(t):
|
||||
wireTup.add(newIdentDefs(ident(c.name), wireValueType(c.typ)))
|
||||
return wireTup
|
||||
if isSeqType(t):
|
||||
rejectNestedSeq(t)
|
||||
t
|
||||
|
||||
proc wireFieldsFor(fieldName: string, fieldType: NimNode): seq[NimNode] =
|
||||
## IdentDefs for `fieldName: fieldType` in the wire object. A seq because
|
||||
## composite fields later split into two physical fields.
|
||||
@[newIdentDefs(ident(fieldName), wireFieldType(fieldType), newEmptyNode())]
|
||||
## IdentDefs for one field. `seq[T]` splits into `<name>_items: ptr
|
||||
## UncheckedArray[<wireOf T>]` + `<name>_len: int`; else a single IdentDef.
|
||||
if isSeqType(fieldType):
|
||||
let elemWire = wireValueType(fieldType[1])
|
||||
let itemsField = newIdentDefs(
|
||||
ident(fieldName & cwireItemsSuffix),
|
||||
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("UncheckedArray"), elemWire)),
|
||||
newEmptyNode(),
|
||||
)
|
||||
let lenField =
|
||||
newIdentDefs(ident(fieldName & cwireLenSuffix), ident("int"), newEmptyNode())
|
||||
return @[itemsField, lenField]
|
||||
@[newIdentDefs(ident(fieldName), wireValueType(fieldType), newEmptyNode())]
|
||||
|
||||
proc buildCWireTypeDef(
|
||||
userTypeName: string, fieldNames: seq[string], fieldTypes: seq[NimNode]
|
||||
@ -79,32 +160,276 @@ proc buildCWireTypeDef(
|
||||
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
||||
newTree(nnkTypeDef, postfix(wireName, "*"), newEmptyNode(), objTy)
|
||||
|
||||
proc emitOptionPack(dstAccess, srcAccess, userType: NimNode): NimNode
|
||||
proc emitOptionUnpack(dstAccess, srcAccess, userType: NimNode): NimNode
|
||||
proc emitOptionFree(dstAccess, userType: NimNode): NimNode
|
||||
proc emitArrayPack(dstAccess, srcAccess, arrType: NimNode): NimNode
|
||||
proc emitArrayUnpack(dstAccess, srcAccess, arrType: NimNode): NimNode
|
||||
proc emitArrayFree(dstAccess, arrType: NimNode): NimNode
|
||||
proc emitTuplePack(dstAccess, srcAccess, tupType: NimNode): NimNode
|
||||
proc emitTupleUnpack(dstAccess, srcAccess, tupType: NimNode): NimNode
|
||||
proc emitTupleFree(dstAccess, tupType: NimNode): NimNode
|
||||
|
||||
proc emitElemPack(dstElem, srcElem, elemType: NimNode): NimNode =
|
||||
## Pack one value: cstring for `string`, recursive `cwirePack` for nested
|
||||
## ffi types, recursive Option/array/tuple handling, direct copy for POD.
|
||||
if isStringType(elemType):
|
||||
return newAssignment(dstElem, newCall(ident("cwireAllocStr"), srcElem))
|
||||
if isNestedFFIType(elemType):
|
||||
return newCall(ident("cwirePack"), dstElem, srcElem)
|
||||
if isOptionType(elemType):
|
||||
return emitOptionPack(dstElem, srcElem, elemType)
|
||||
if isArrayType(elemType):
|
||||
return emitArrayPack(dstElem, srcElem, elemType)
|
||||
if isTupleType(elemType):
|
||||
return emitTuplePack(dstElem, srcElem, elemType)
|
||||
if isSeqType(elemType):
|
||||
rejectNestedSeq(elemType)
|
||||
newAssignment(dstElem, srcElem)
|
||||
|
||||
proc emitElemUnpack(dstElem, srcElem, elemType: NimNode): NimNode =
|
||||
## Inverse of `emitElemPack`: copy one value back into Nim-managed memory.
|
||||
if isStringType(elemType):
|
||||
return newAssignment(dstElem, newCall(ident("$"), srcElem))
|
||||
if isNestedFFIType(elemType):
|
||||
return newAssignment(dstElem, newCall(ident("cwireUnpack"), srcElem))
|
||||
if isOptionType(elemType):
|
||||
return emitOptionUnpack(dstElem, srcElem, elemType)
|
||||
if isArrayType(elemType):
|
||||
return emitArrayUnpack(dstElem, srcElem, elemType)
|
||||
if isTupleType(elemType):
|
||||
return emitTupleUnpack(dstElem, srcElem, elemType)
|
||||
if isSeqType(elemType):
|
||||
rejectNestedSeq(elemType)
|
||||
newAssignment(dstElem, srcElem)
|
||||
|
||||
proc emitElemFree(elemAccess, elemType: NimNode): NimNode =
|
||||
## Free one value, or `nnkEmpty` for POD (nothing to free).
|
||||
if isStringType(elemType):
|
||||
return newCall(ident("cwireFreeStr"), elemAccess)
|
||||
if isNestedFFIType(elemType):
|
||||
return newCall(ident("cwireFree"), elemAccess)
|
||||
if isOptionType(elemType):
|
||||
return emitOptionFree(elemAccess, elemType)
|
||||
if isArrayType(elemType):
|
||||
return emitArrayFree(elemAccess, elemType)
|
||||
if isTupleType(elemType):
|
||||
return emitTupleFree(elemAccess, elemType)
|
||||
if isSeqType(elemType):
|
||||
rejectNestedSeq(elemType)
|
||||
newEmptyNode()
|
||||
|
||||
proc maybeStmt(n: NimNode): NimNode =
|
||||
## `n` as a one-statement list, or an empty list when `n` is `nnkEmpty`
|
||||
## (nothing to do) — keeps the surrounding `quote` block well-formed.
|
||||
if n.kind == nnkEmpty:
|
||||
return newStmtList()
|
||||
newStmtList(n)
|
||||
|
||||
proc indexLoop(access, idx, body: NimNode): NimNode =
|
||||
## `for <idx> in low(access) .. high(access): body` — `low`/`high` so any
|
||||
## array index range (not just 0-based) is covered.
|
||||
nnkForStmt.newTree(
|
||||
idx,
|
||||
nnkInfix.newTree(
|
||||
ident(".."), newCall(ident("low"), access), newCall(ident("high"), access)
|
||||
),
|
||||
newStmtList(body),
|
||||
)
|
||||
|
||||
proc emitArrayPack(dstAccess, srcAccess, arrType: NimNode): NimNode =
|
||||
## Pack a fixed `array[N, T]` element-by-element into the inline wire array;
|
||||
## the array itself needs no allocation, only its GC'd element contents do.
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let body = emitElemPack(
|
||||
nnkBracketExpr.newTree(dstAccess, idx),
|
||||
nnkBracketExpr.newTree(srcAccess, idx),
|
||||
arrType[2],
|
||||
)
|
||||
indexLoop(srcAccess, idx, body)
|
||||
|
||||
proc emitArrayUnpack(dstAccess, srcAccess, arrType: NimNode): NimNode =
|
||||
## Inverse of `emitArrayPack`: copy each wire element back into the Nim array.
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let body = emitElemUnpack(
|
||||
nnkBracketExpr.newTree(dstAccess, idx),
|
||||
nnkBracketExpr.newTree(srcAccess, idx),
|
||||
arrType[2],
|
||||
)
|
||||
indexLoop(srcAccess, idx, body)
|
||||
|
||||
proc emitArrayFree(dstAccess, arrType: NimNode): NimNode =
|
||||
## Free each array element; `nnkEmpty` when the element type owns nothing.
|
||||
if not cwireNeedsFree(arrType[2]):
|
||||
return newEmptyNode()
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let body = emitElemFree(nnkBracketExpr.newTree(dstAccess, idx), arrType[2])
|
||||
indexLoop(dstAccess, idx, body)
|
||||
|
||||
proc emitTuplePack(dstAccess, srcAccess, tupType: NimNode): NimNode =
|
||||
## Pack each named tuple component into the matching wire component.
|
||||
let body = newStmtList()
|
||||
for c in tupleComponents(tupType):
|
||||
let nm = ident(c.name)
|
||||
body.add(emitElemPack(newDotExpr(dstAccess, nm), newDotExpr(srcAccess, nm), c.typ))
|
||||
body
|
||||
|
||||
proc emitTupleUnpack(dstAccess, srcAccess, tupType: NimNode): NimNode =
|
||||
## Inverse of `emitTuplePack`: copy each wire component back out.
|
||||
let body = newStmtList()
|
||||
for c in tupleComponents(tupType):
|
||||
let nm = ident(c.name)
|
||||
body.add(
|
||||
emitElemUnpack(newDotExpr(dstAccess, nm), newDotExpr(srcAccess, nm), c.typ)
|
||||
)
|
||||
body
|
||||
|
||||
proc emitTupleFree(dstAccess, tupType: NimNode): NimNode =
|
||||
## Free each tuple component that owns allocations; `nnkEmpty` when none do.
|
||||
if not cwireNeedsFree(tupType):
|
||||
return newEmptyNode()
|
||||
let body = newStmtList()
|
||||
for c in tupleComponents(tupType):
|
||||
body.add(maybeStmt(emitElemFree(newDotExpr(dstAccess, ident(c.name)), c.typ)))
|
||||
body
|
||||
|
||||
proc emitSeqPack(dstObj, srcAccess, fieldNameIdent, userType: NimNode): NimNode =
|
||||
## Pack a seq field into a freshly `allocShared`'d `UncheckedArray`; an empty
|
||||
## seq encodes as nil items + 0 len.
|
||||
let elemType = userType[1]
|
||||
let wireElem = wireValueType(elemType)
|
||||
let items = seqItemsField(dstObj, fieldNameIdent)
|
||||
let count = seqLenField(dstObj, fieldNameIdent)
|
||||
let bufType =
|
||||
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("UncheckedArray"), wireElem))
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let elemPack = emitElemPack(
|
||||
nnkBracketExpr.newTree(items, idx), nnkBracketExpr.newTree(srcAccess, idx), elemType
|
||||
)
|
||||
let forLoop = nnkForStmt.newTree(
|
||||
idx,
|
||||
nnkInfix.newTree(
|
||||
ident("..<"), newLit(0), newCall(newDotExpr(srcAccess, ident("len")))
|
||||
),
|
||||
newStmtList(elemPack),
|
||||
)
|
||||
quote:
|
||||
if `srcAccess`.len() == 0:
|
||||
`items` = nil
|
||||
`count` = 0
|
||||
else:
|
||||
`items` = cast[`bufType`](allocShared(sizeof(`wireElem`) * `srcAccess`.len()))
|
||||
`forLoop`
|
||||
`count` = `srcAccess`.len()
|
||||
|
||||
proc emitOptionPack(dstAccess, srcAccess, userType: NimNode): NimNode =
|
||||
## Pack an Option into a `ptr`: some → `allocShared` a box and pack into it,
|
||||
## none → nil. The payload is read into a local once, so a composite inner
|
||||
## type (e.g. `array`/`tuple`) isn't re-`get()`-copied per element.
|
||||
let innerType = userType[1]
|
||||
let wireInner = wireValueType(innerType)
|
||||
let bufType = nnkPtrTy.newTree(wireInner)
|
||||
let innerVal = genSym(nskLet, "innerVal")
|
||||
let elemPack = emitElemPack(nnkBracketExpr.newTree(dstAccess), innerVal, innerType)
|
||||
quote:
|
||||
if `srcAccess`.isSome():
|
||||
`dstAccess` = cast[`bufType`](allocShared(sizeof(`wireInner`)))
|
||||
let `innerVal` = `srcAccess`.get()
|
||||
`elemPack`
|
||||
else:
|
||||
`dstAccess` = nil
|
||||
|
||||
proc emitPackStmt(dstObj, srcObj, fieldNameIdent, userType: NimNode): seq[NimNode] =
|
||||
## Populate `dstObj.<field>` from `srcObj.<field>`: cstring allocation for
|
||||
## strings, direct copy for POD.
|
||||
## Populate `dstObj.<field>` from `srcObj.<field>`, allocating shared-memory
|
||||
## cstrings/arrays as the field's natural type requires.
|
||||
let srcAccess = newDotExpr(srcObj, fieldNameIdent)
|
||||
let dstAccess = newDotExpr(dstObj, fieldNameIdent)
|
||||
if isStringType(userType):
|
||||
return @[newAssignment(dstAccess, newCall(ident("cwireAllocStr"), srcAccess))]
|
||||
@[newAssignment(dstAccess, srcAccess)]
|
||||
if isSeqType(userType):
|
||||
return @[emitSeqPack(dstObj, srcAccess, fieldNameIdent, userType)]
|
||||
@[emitElemPack(dstAccess, srcAccess, userType)]
|
||||
|
||||
proc emitSeqUnpack(dstAccess, srcObj, fieldNameIdent, userType: NimNode): NimNode =
|
||||
## Rebuild a Nim seq from the `<field>_items`/`_len` wire pair.
|
||||
let elemType = userType[1]
|
||||
let items = seqItemsField(srcObj, fieldNameIdent)
|
||||
let count = seqLenField(srcObj, fieldNameIdent)
|
||||
let elemVar = genSym(nskVar, "elem")
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let elemUnpack = emitElemUnpack(elemVar, nnkBracketExpr.newTree(items, idx), elemType)
|
||||
quote:
|
||||
`dstAccess` = @[]
|
||||
for `idx` in 0 ..< `count`:
|
||||
var `elemVar`: `elemType`
|
||||
`elemUnpack`
|
||||
`dstAccess`.add(`elemVar`)
|
||||
|
||||
proc emitOptionUnpack(dstAccess, srcAccess, userType: NimNode): NimNode =
|
||||
## Rebuild an Option from a wire `ptr`: nil → none, else unpack the pointee.
|
||||
let innerType = userType[1]
|
||||
let elemVar = genSym(nskVar, "innerVal")
|
||||
let elemUnpack = emitElemUnpack(elemVar, nnkBracketExpr.newTree(srcAccess), innerType)
|
||||
quote:
|
||||
if `srcAccess`.isNil():
|
||||
`dstAccess` = none(`innerType`)
|
||||
else:
|
||||
var `elemVar`: `innerType`
|
||||
`elemUnpack`
|
||||
`dstAccess` = some(`elemVar`)
|
||||
|
||||
proc emitUnpackStmt(
|
||||
resultObj, srcObj, fieldNameIdent, userType: NimNode
|
||||
): seq[NimNode] =
|
||||
## Fill `resultObj.<field>` from `srcObj.<field>`: `$cstring` copies into
|
||||
## Nim-managed memory, POD is a direct copy.
|
||||
## Fill `resultObj.<field>` from `srcObj.<field>`, copying back into
|
||||
## Nim-managed memory.
|
||||
let srcAccess = newDotExpr(srcObj, fieldNameIdent)
|
||||
let dstAccess = newDotExpr(resultObj, fieldNameIdent)
|
||||
if isStringType(userType):
|
||||
return @[newAssignment(dstAccess, newCall(ident("$"), srcAccess))]
|
||||
@[newAssignment(dstAccess, srcAccess)]
|
||||
if isSeqType(userType):
|
||||
return @[emitSeqUnpack(dstAccess, srcObj, fieldNameIdent, userType)]
|
||||
@[emitElemUnpack(dstAccess, srcAccess, userType)]
|
||||
|
||||
proc emitSeqFree(dstObj, fieldNameIdent, userType: NimNode): NimNode =
|
||||
## Free a seq field: free each element (skipped entirely for POD), then the
|
||||
## shared buffer.
|
||||
let elemType = userType[1]
|
||||
let items = seqItemsField(dstObj, fieldNameIdent)
|
||||
let count = seqLenField(dstObj, fieldNameIdent)
|
||||
let idx = genSym(nskForVar, "i")
|
||||
let elemFree = emitElemFree(nnkBracketExpr.newTree(items, idx), elemType)
|
||||
let freeLoop =
|
||||
if elemFree.kind == nnkEmpty:
|
||||
newStmtList()
|
||||
else:
|
||||
newStmtList(
|
||||
nnkForStmt.newTree(
|
||||
idx, nnkInfix.newTree(ident("..<"), newLit(0), count), newStmtList(elemFree)
|
||||
)
|
||||
)
|
||||
quote:
|
||||
if not `items`.isNil():
|
||||
`freeLoop`
|
||||
deallocShared(`items`)
|
||||
`items` = nil
|
||||
`count` = 0
|
||||
|
||||
proc emitOptionFree(dstAccess, userType: NimNode): NimNode =
|
||||
## Free an Option field: free the pointee (skipped for POD), then the box.
|
||||
let innerType = userType[1]
|
||||
let freeInner = maybeStmt(emitElemFree(nnkBracketExpr.newTree(dstAccess), innerType))
|
||||
quote:
|
||||
if not `dstAccess`.isNil():
|
||||
`freeInner`
|
||||
deallocShared(`dstAccess`)
|
||||
`dstAccess` = nil
|
||||
|
||||
proc emitFreeStmt(dstObj, fieldNameIdent, userType: NimNode): seq[NimNode] =
|
||||
## Release `dstObj.<field>`: free the cstring for strings, nothing for POD.
|
||||
## Release `dstObj.<field>`: free cstrings/arrays/pointers; POD frees nothing.
|
||||
let dstAccess = newDotExpr(dstObj, fieldNameIdent)
|
||||
if isStringType(userType):
|
||||
return @[newCall(ident("cwireFreeStr"), dstAccess)]
|
||||
@[]
|
||||
if isSeqType(userType):
|
||||
return @[emitSeqFree(dstObj, fieldNameIdent, userType)]
|
||||
let elemFree = emitElemFree(dstAccess, userType)
|
||||
if elemFree.kind == nnkEmpty:
|
||||
return @[]
|
||||
@[elemFree]
|
||||
|
||||
proc buildCWireProcs(
|
||||
userTypeName: string, fieldNames: seq[string], fieldTypes: seq[NimNode]
|
||||
@ -184,15 +509,36 @@ proc fieldInfoForType(
|
||||
return (names, types)
|
||||
error("fieldInfoForType: ffi type '" & typeName & "' not in registry")
|
||||
|
||||
proc collectNestedFFITypes(
|
||||
fieldTypes: seq[NimNode], deps: var seq[string]
|
||||
) {.compileTime.} =
|
||||
## Append (deduped) the names of nested ffi types referenced anywhere in
|
||||
## `fieldTypes`, recursing through `seq`/`Option`/`array`/`tuple` to any depth.
|
||||
for t in fieldTypes:
|
||||
if isNestedFFIType(t):
|
||||
let n = $t
|
||||
if n notin deps:
|
||||
deps.add(n)
|
||||
elif isSeqType(t) or isOptionType(t):
|
||||
collectNestedFFITypes(@[t[1]], deps)
|
||||
elif isArrayType(t):
|
||||
collectNestedFFITypes(@[t[2]], deps)
|
||||
elif isTupleType(t):
|
||||
for c in tupleComponents(t):
|
||||
collectNestedFFITypes(@[c.typ], deps)
|
||||
|
||||
proc ensureCWireFor(typeName: string, sink: NimNode) {.compileTime.} =
|
||||
## Idempotent: if `typeName`'s cwire companion has not yet been emitted,
|
||||
## append its TypeSection and conversion procs to `sink` and mark it emitted.
|
||||
## Nested ffi deps are ensured first so the resulting AST is self-contained.
|
||||
if isCWireEmitted(typeName):
|
||||
return
|
||||
markCWireEmitted(typeName)
|
||||
let info = fieldInfoForType(typeName)
|
||||
for i in 0 ..< info.names.len:
|
||||
assertCWireFieldSupported(typeName, info.names[i], info.types[i])
|
||||
var deps: seq[string] = @[]
|
||||
collectNestedFFITypes(info.types, deps)
|
||||
for dep in deps:
|
||||
ensureCWireFor(dep, sink)
|
||||
markCWireEmitted(typeName)
|
||||
let section = newNimNode(nnkTypeSection)
|
||||
section.add(buildCWireTypeDef(typeName, info.names, info.types))
|
||||
sink.add(section)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
## Runtime helpers for the macro-generated `*_CWire` companion types: only the
|
||||
## `cstring` fields need allocation, packed on pack and released on free.
|
||||
## `cstring` fields need allocation here (seq/Option are alloc'd inline by the
|
||||
## macro), packed on pack and released on free.
|
||||
|
||||
import ../alloc
|
||||
|
||||
|
||||
@ -1,54 +1,195 @@
|
||||
## Round-trip correctness for the `c` (flat C-struct) ABI codec — flat path.
|
||||
## Round-trip correctness for the `c` (flat C-struct) ABI codec.
|
||||
##
|
||||
## Each `{.ffi: "abi = c".}` type gets a `<T>_CWire` companion plus
|
||||
## `cwirePack` / `cwireUnpack` / `cwireFree`. This asserts
|
||||
## `cwireUnpack(cwirePack(x)) == x` for the flat scalar+string field shapes,
|
||||
## including the empty-string edge case the cstring encoding must handle.
|
||||
## `cwireUnpack(cwirePack(x)) == x` across the supported field shapes —
|
||||
## scalars, strings, `seq`, `Option`, `array`, named `tuple`, nested {.ffi.}
|
||||
## structs, and nested composites (`seq[Option[T]]`, `array[N, tuple[...]]`,
|
||||
## `Option[array[...]]`, ...) — including the empty/none/empty-string edge
|
||||
## cases the cstring/pointer encoding must handle.
|
||||
##
|
||||
## `genBindings()` flushes the cwire companions for every abi=c type declared
|
||||
## above it (a type-pragma macro can't splice them in at the type site).
|
||||
|
||||
import std/options
|
||||
import unittest2
|
||||
import ffi
|
||||
|
||||
type Flat {.ffi: "abi = c".} = object
|
||||
name: string
|
||||
type Inner {.ffi: "abi = c".} = object
|
||||
label: string
|
||||
weight: int
|
||||
|
||||
type Outer {.ffi: "abi = c".} = object
|
||||
name: string
|
||||
count: int
|
||||
size: uint32
|
||||
ratio: float64
|
||||
flag: bool
|
||||
inner: Inner
|
||||
items: seq[Inner]
|
||||
tags: seq[string]
|
||||
note: Option[string]
|
||||
retries: Option[int]
|
||||
blob: seq[byte]
|
||||
maybeInner: Option[Inner]
|
||||
optItems: seq[Option[Inner]]
|
||||
optTags: seq[Option[string]]
|
||||
|
||||
type Shapes {.ffi: "abi = c".} = object
|
||||
## Exercises array/tuple wire shapes and their cross-nestings (array of
|
||||
## array, tuple in array, array in Option, tuple in seq) alongside GC'd and
|
||||
## nested-{.ffi.} element types.
|
||||
coords: array[3, int]
|
||||
labels: array[2, string]
|
||||
cells: array[2, Inner]
|
||||
matrix: array[2, array[2, int]]
|
||||
point: tuple[x: int, y: int]
|
||||
tagged: tuple[label: string, kv: Inner]
|
||||
grid: array[2, tuple[a: string, b: int]]
|
||||
optBox: Option[array[2, string]]
|
||||
pairs: seq[tuple[name: string, n: int]]
|
||||
|
||||
genBindings()
|
||||
|
||||
proc roundTrip(o: Flat): Flat =
|
||||
## Pack into the flat wire struct, copy back out, then release the wire
|
||||
proc roundTrip(o: Outer): Outer =
|
||||
## Pack into the wire struct, copy back out, then release the wire
|
||||
## allocations — the exact lifecycle a boundary crossing would use.
|
||||
var wire: Flat_CWire
|
||||
var wire: Outer_CWire
|
||||
cwirePack(wire, o)
|
||||
let back = cwireUnpack(wire)
|
||||
cwireFree(wire)
|
||||
return back
|
||||
|
||||
suite "c-ABI cwire round-trip (flat)":
|
||||
test "populated scalars and strings survive pack/unpack/free":
|
||||
let o = Flat(
|
||||
name: "hello",
|
||||
label: "world",
|
||||
count: -42,
|
||||
size: 4_000_000_000'u32,
|
||||
ratio: 3.5,
|
||||
proc roundTrip(o: Shapes): Shapes =
|
||||
## Same pack/unpack/free lifecycle as the `Outer` overload, for the
|
||||
## array/tuple shapes.
|
||||
var wire: Shapes_CWire
|
||||
cwirePack(wire, o)
|
||||
let back = cwireUnpack(wire)
|
||||
cwireFree(wire)
|
||||
return back
|
||||
|
||||
suite "c-ABI cwire round-trip":
|
||||
test "fully-populated value survives pack/unpack/free":
|
||||
let o = Outer(
|
||||
name: "outer",
|
||||
count: 42,
|
||||
flag: true,
|
||||
inner: Inner(label: "core", weight: 7),
|
||||
items: @[Inner(label: "a", weight: 1), Inner(label: "b", weight: 2)],
|
||||
tags: @["x", "y", "z"],
|
||||
note: some("a note"),
|
||||
retries: some(3),
|
||||
blob: @[1'u8, 2, 3, 255],
|
||||
maybeInner: some(Inner(label: "opt", weight: 99)),
|
||||
optItems: @[some(Inner(label: "p", weight: 5)), none(Inner)],
|
||||
optTags: @[some("kept"), none(string), some("")],
|
||||
)
|
||||
check roundTrip(o) == o
|
||||
|
||||
test "empty strings, empty seqs, and none Options survive":
|
||||
let o = Outer(
|
||||
name: "",
|
||||
count: 0,
|
||||
flag: false,
|
||||
inner: Inner(label: "", weight: 0),
|
||||
items: @[],
|
||||
tags: @[],
|
||||
note: none(string),
|
||||
retries: none(int),
|
||||
blob: @[],
|
||||
maybeInner: none(Inner),
|
||||
optItems: @[],
|
||||
optTags: @[],
|
||||
)
|
||||
let back = roundTrip(o)
|
||||
check back == o
|
||||
# Distinct strings must not alias one another.
|
||||
check back.name == "hello"
|
||||
check back.label == "world"
|
||||
check back.note.isNone
|
||||
check back.retries.isNone
|
||||
check back.maybeInner.isNone
|
||||
check back.items.len == 0
|
||||
check back.blob.len == 0
|
||||
check back.optItems.len == 0
|
||||
|
||||
test "empty strings survive pack/unpack/free":
|
||||
let o = Flat(name: "", label: "", count: 0, size: 0, ratio: 0.0, flag: false)
|
||||
test "nested seq elements keep their string fields":
|
||||
let o = Outer(
|
||||
name: "n",
|
||||
inner: Inner(label: "i", weight: 9),
|
||||
items: @[Inner(label: "alpha", weight: 10), Inner(label: "beta", weight: 20)],
|
||||
note: some(""), # some-of-empty-string must stay `some`, not collapse to none
|
||||
)
|
||||
let back = roundTrip(o)
|
||||
check back.items.len == 2
|
||||
check back.items[1].label == "beta"
|
||||
check back.note.isSome
|
||||
check back.note.get == ""
|
||||
|
||||
test "seq[Option[T]] elements round-trip per-element some/none":
|
||||
let o = Outer(
|
||||
name: "rec",
|
||||
optItems: @[
|
||||
some(Inner(label: "x", weight: 1)),
|
||||
none(Inner),
|
||||
some(Inner(label: "z", weight: 3)),
|
||||
],
|
||||
optTags: @[none(string), some("mid"), some("")],
|
||||
)
|
||||
let back = roundTrip(o)
|
||||
check back.optItems.len == 3
|
||||
check back.optItems[0].isSome
|
||||
check back.optItems[0].get.label == "x"
|
||||
check back.optItems[1].isNone
|
||||
check back.optItems[2].get.weight == 3
|
||||
check back.optTags[0].isNone
|
||||
check back.optTags[1].get == "mid"
|
||||
check back.optTags[2].isSome
|
||||
check back.optTags[2].get == ""
|
||||
|
||||
suite "c-ABI cwire array/tuple round-trip":
|
||||
test "fully-populated array & tuple fields survive pack/unpack/free":
|
||||
let o = Shapes(
|
||||
coords: [1, 2, 3],
|
||||
labels: ["alpha", "beta"],
|
||||
cells: [Inner(label: "a", weight: 1), Inner(label: "b", weight: 2)],
|
||||
matrix: [[1, 2], [3, 4]],
|
||||
point: (x: 10, y: 20),
|
||||
tagged: (label: "lbl", kv: Inner(label: "k", weight: 9)),
|
||||
grid: [(a: "g0", b: 0), (a: "g1", b: 1)],
|
||||
optBox: some(["p", "q"]),
|
||||
pairs: @[(name: "n0", n: 0), (name: "n1", n: 1)],
|
||||
)
|
||||
check roundTrip(o) == o
|
||||
|
||||
test "empty seq, none Option, and empty-string elements survive":
|
||||
let o = Shapes(
|
||||
coords: [0, 0, 0],
|
||||
labels: ["", ""],
|
||||
cells: [Inner(label: "", weight: 0), Inner(label: "", weight: 0)],
|
||||
matrix: [[0, 0], [0, 0]],
|
||||
point: (x: 0, y: 0),
|
||||
tagged: (label: "", kv: Inner(label: "", weight: 0)),
|
||||
grid: [(a: "", b: 0), (a: "", b: 0)],
|
||||
optBox: none(array[2, string]),
|
||||
pairs: @[],
|
||||
)
|
||||
let back = roundTrip(o)
|
||||
check back == o
|
||||
check back.name == ""
|
||||
check back.label == ""
|
||||
check back.optBox.isNone
|
||||
check back.pairs.len == 0
|
||||
|
||||
test "GC'd contents inside array/tuple keep their values":
|
||||
let o = Shapes(
|
||||
labels: ["kept", "also kept"],
|
||||
cells: [Inner(label: "x", weight: 5), Inner(label: "y", weight: 6)],
|
||||
tagged: (label: "tag", kv: Inner(label: "deep", weight: 7)),
|
||||
grid: [(a: "first", b: 1), (a: "second", b: 2)],
|
||||
optBox: some(["box0", ""]),
|
||||
pairs: @[(name: "alpha", n: 10), (name: "", n: 20)],
|
||||
)
|
||||
let back = roundTrip(o)
|
||||
check back.labels[1] == "also kept"
|
||||
check back.cells[0].label == "x"
|
||||
check back.tagged.kv.label == "deep"
|
||||
check back.grid[1].a == "second"
|
||||
check back.optBox.get[0] == "box0"
|
||||
check back.optBox.get[1] == ""
|
||||
check back.pairs[1].name == ""
|
||||
check back.pairs[0].n == 10
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user