mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-28 12:19:45 +00:00
196 lines
6.2 KiB
Nim
196 lines
6.2 KiB
Nim
## 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` 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 Inner {.ffi: "abi = c".} = object
|
|
label: string
|
|
weight: int
|
|
|
|
type Outer {.ffi: "abi = c".} = object
|
|
name: string
|
|
count: int
|
|
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: 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: Outer_CWire
|
|
cwirePack(wire, o)
|
|
let back = cwireUnpack(wire)
|
|
cwireFree(wire)
|
|
return back
|
|
|
|
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
|
|
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 "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.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
|