Skip to content

Commit c000a84

Browse files
test(unit): native POD ABI carries every supported field type
A "kitchen sink" {.ffi.} object spanning every supported field shape — all integer widths, both floats, bool, string, sequences (scalars / strings / floats / nested structs), Option/Maybe, and a nested struct by value — is sent in as a C-POD and returned as a typed C-POD, then checked field-for-field against the Nim-native result. This is the native-path complement to the existing CBOR coverage (test_serial for the codec, test_wire_compat for the bytes): it pins nimToPod -> *NativeExport -> clonePod/podToNim of the typed return for the whole type matrix. Compiling also proves the native-POD codegen accepts every type. Passes under orc + refc and clean under ASAN. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c6c7600 commit c000a84

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
## Type coverage for the *native* (POD) ABI specifically: one `{.ffi.}` "kitchen
2+
## sink" object whose fields span every supported field shape — all integer
3+
## widths, both floats, bool, string, sequences (of scalars, strings, floats and
4+
## nested structs), Option/Maybe, and a nested struct by value — is sent in as a
5+
## C-POD struct and returned as a typed C-POD struct, then compared to the value
6+
## the Nim-native API produces.
7+
##
8+
## The CBOR side of this matrix already lives in `test_serial.nim` (the codec)
9+
## and `test_wire_compat.nim` (the bytes); this file pins the parallel guarantee
10+
## for the zero-serialization path: `nimToPod` -> `*NativeExport` ->
11+
## `clonePod`/`podToNim` of the typed return carry every type back unchanged.
12+
##
13+
## Compiling the file also proves the native-POD codegen *accepts* every type
14+
## (an unsupported field would fail the macro expansion). Runs under orc + refc.
15+
16+
import std/[options, locks]
17+
import unittest2
18+
import results
19+
import ffi
20+
21+
type Maybe[T] = Option[T]
22+
23+
type CovLib = object
24+
25+
type CovConfig {.ffi.} = object
26+
label: string
27+
28+
type Inner {.ffi.} = object
29+
tag: string
30+
weight: float64
31+
flag: bool
32+
33+
type AllTypes {.ffi.} = object
34+
b: bool
35+
i: int
36+
i8: int8
37+
i16: int16
38+
i32: int32
39+
i64: int64
40+
u: uint
41+
u8: uint8
42+
u16: uint16
43+
u32: uint32
44+
u64: uint64
45+
f32: float32
46+
f64: float64
47+
s: string
48+
ints: seq[int]
49+
strs: seq[string]
50+
floats: seq[float64]
51+
inners: seq[Inner]
52+
optI: Option[int]
53+
optS: Option[string]
54+
maybeB: Maybe[bool]
55+
inner: Inner
56+
57+
proc cov_create(cfg: CovConfig): Future[Result[CovLib, string]] {.ffiCtor.} =
58+
discard cfg
59+
return ok(CovLib())
60+
61+
proc cov_echo(
62+
lib: CovLib, req: AllTypes
63+
): Future[Result[AllTypes, string]] {.ffi.} =
64+
## Round-trips the whole graph straight back.
65+
discard lib
66+
return ok(req)
67+
68+
proc cov_destroy(lib: CovLib) {.ffiDtor.} =
69+
discard
70+
71+
# A fully-populated sample (distinct, non-default values everywhere).
72+
proc sample(): AllTypes =
73+
AllTypes(
74+
b: true,
75+
i: -123456,
76+
i8: -8,
77+
i16: -1600,
78+
i32: -320000,
79+
i64: -6400000000'i64,
80+
u: 123456'u,
81+
u8: 200'u8,
82+
u16: 60000'u16,
83+
u32: 4000000000'u32,
84+
u64: 18000000000000000000'u64,
85+
f32: 3.5'f32,
86+
f64: 2.718281828459045,
87+
s: "héllo, FFI",
88+
ints: @[1, -2, 3, -4],
89+
strs: @["a", "", "ccc"],
90+
floats: @[1.5, -2.25, 3.125],
91+
inners:
92+
@[Inner(tag: "x", weight: 1.0, flag: false), Inner(tag: "y", weight: -9.5, flag: true)],
93+
optI: some(42),
94+
optS: none(string),
95+
maybeB: some(true),
96+
inner: Inner(tag: "nested", weight: 0.0, flag: true),
97+
)
98+
99+
# --- blocking callback capture ----------------------------------------------
100+
type Cap = object
101+
lock: Lock
102+
cond: Cond
103+
done: bool
104+
ret: cint
105+
pod: AllTypesPod # native typed return, deep-copied (c_malloc) in the callback
106+
hasPod: bool
107+
108+
proc initCap(c: var Cap) =
109+
c.lock.initLock()
110+
c.cond.initCond()
111+
c.done = false
112+
c.hasPod = false
113+
114+
proc deinitCap(c: var Cap) =
115+
c.cond.deinitCond()
116+
c.lock.deinitLock()
117+
118+
proc reset(c: var Cap) =
119+
acquire(c.lock)
120+
c.done = false
121+
c.hasPod = false
122+
release(c.lock)
123+
124+
proc waitCap(c: var Cap) =
125+
acquire(c.lock)
126+
while not c.done:
127+
wait(c.cond, c.lock)
128+
release(c.lock)
129+
130+
proc ackCb(
131+
ret: cint, msg: ptr cchar, len: csize_t, ud: pointer
132+
) {.cdecl, gcsafe, raises: [].} =
133+
let c = cast[ptr Cap](ud)
134+
acquire(c[].lock)
135+
c[].ret = ret
136+
c[].done = true
137+
signal(c[].cond)
138+
release(c[].lock)
139+
140+
proc nativeCb(
141+
ret: cint, msg: ptr cchar, len: csize_t, ud: pointer
142+
) {.cdecl, gcsafe, raises: [].} =
143+
## Native ABI: `msg` is a `ptr AllTypesPod`. Deep-copy it (c_malloc, no GC) so
144+
## it outlives this callback; the test thread rebuilds the Nim value from it.
145+
let c = cast[ptr Cap](ud)
146+
acquire(c[].lock)
147+
c[].ret = ret
148+
if ret == RET_OK and not msg.isNil:
149+
c[].pod = clonePod(cast[ptr AllTypesPod](msg)[])
150+
c[].hasPod = true
151+
c[].done = true
152+
signal(c[].cond)
153+
release(c[].lock)
154+
155+
suite "native POD ABI — every field type":
156+
let want = sample()
157+
158+
test "Nim-native API is the reference round-trip":
159+
let res = waitFor cov_echo(CovLib(), want)
160+
check res.isOk
161+
check res.value == want
162+
163+
test "native POD ABI carries every type in and back out":
164+
var cap: Cap
165+
initCap(cap)
166+
defer:
167+
deinitCap(cap)
168+
169+
# Context via the native ctor (returns the ctx pointer; wait for the body).
170+
var cfgPod = nimToPod(CovConfig(label: "cov"))
171+
let ctx = cov_createNativeCtorExport(cfgPod, ackCb, addr cap)
172+
freePod(cfgPod)
173+
check not ctx.isNil
174+
waitCap(cap)
175+
176+
# Send the whole graph as a C-POD; get a typed C-POD back.
177+
reset(cap)
178+
var argPod = nimToPod(want)
179+
let rc = cov_echoNativeExport(
180+
cast[ptr FFIContext[CovLib]](ctx), nativeCb, addr cap, argPod
181+
)
182+
freePod(argPod) # the export already deep-copied it on the caller thread
183+
check rc == RET_OK
184+
waitCap(cap)
185+
check cap.ret == RET_OK
186+
check cap.hasPod
187+
188+
let got = podToNim(cap.pod)
189+
freePod(cap.pod)
190+
check got == want # all 22 fields survived the POD round-trip
191+
192+
check CovLibFFIPool.destroyFFIContext(cast[ptr FFIContext[CovLib]](ctx)).isOk()

0 commit comments

Comments
 (0)