mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-20 16:29:31 +00:00
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>
92 lines
3.4 KiB
Nim
92 lines
3.4 KiB
Nim
## Cross-thread allocation helpers backed by libc `malloc`/`free`.
|
||
##
|
||
## We deliberately avoid Nim's `allocShared`/`deallocShared` here. Under
|
||
## `--mm:orc` they delegate to the per-thread `allocator` MemRegion stored
|
||
## in TLS; freeing such a buffer from a different thread later walks
|
||
## `chunk.owner` back to that MemRegion. If the original thread has exited
|
||
## by then (e.g. a `std::async` worker that produced the FFI request and
|
||
## was destroyed before the FFI thread ran `deleteRequest`), `chunk.owner`
|
||
## dangles into reclaimed TLS and `addToSharedFreeList` segfaults — TSan on
|
||
## ARM reproduces this from `TimerE2E.ThreadedHammer`. `malloc`/`free` are
|
||
## process-global and thread-lifetime-independent, so freeing on a different
|
||
## thread is safe.
|
||
|
||
import system/ansi_c
|
||
|
||
## Can be shared safely between threads
|
||
type SharedSeq*[T] = tuple[data: ptr UncheckedArray[T], len: int]
|
||
|
||
proc alloc*(str: cstring): cstring =
|
||
## Allocates a fresh null-terminated copy of `str` via `c_malloc`. The
|
||
## returned pointer must be released with `dealloc(cstring)`.
|
||
if str.isNil():
|
||
var ret = cast[cstring](c_malloc(1))
|
||
ret[0] = '\0'
|
||
return ret
|
||
|
||
let ret = cast[cstring](c_malloc(csize_t(len(str) + 1)))
|
||
copyMem(ret, str, len(str) + 1)
|
||
return ret
|
||
|
||
proc alloc*(str: string): cstring =
|
||
## Allocates a fresh null-terminated copy of `str` via `c_malloc`. The
|
||
## returned pointer must be released with `dealloc(cstring)`.
|
||
var ret = cast[cstring](c_malloc(csize_t(str.len + 1)))
|
||
let s = cast[seq[char]](str)
|
||
for i in 0 ..< str.len:
|
||
ret[i] = s[i]
|
||
ret[str.len] = '\0'
|
||
return ret
|
||
|
||
proc dealloc*(p: cstring) {.inline.} =
|
||
## Frees a buffer obtained from one of the `alloc(...)` overloads above.
|
||
## Nil-safe.
|
||
if not p.isNil():
|
||
c_free(cast[pointer](p))
|
||
|
||
proc ffiCMalloc*(T: typedesc): ptr T =
|
||
## Allocates a zero-initialised `T` via `c_malloc` so the buffer can cross
|
||
## threads safely (see the module note). Used to carry a native (non-CBOR)
|
||
## request payload by pointer; release with `ffiCFree`. (Named with the `ffi`
|
||
## prefix so it doesn't collide with `ansi_c.c_free`/`c_malloc` under Nim's
|
||
## style-insensitive identifier rules.)
|
||
let p = cast[ptr T](c_malloc(csize_t(sizeof(T))))
|
||
zeroMem(p, sizeof(T))
|
||
return p
|
||
|
||
proc ffiCFree*(p: pointer) {.inline.} =
|
||
## Frees a buffer obtained from `ffiCMalloc`. Nil-safe.
|
||
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)
|
||
|
||
let data = c_malloc(csize_t(sizeof(T) * s.len))
|
||
copyMem(data, unsafeAddr s[0], sizeof(T) * s.len)
|
||
return (cast[ptr UncheckedArray[T]](data), s.len)
|
||
|
||
proc deallocSharedSeq*[T](s: var SharedSeq[T]) =
|
||
if not s.data.isNil():
|
||
c_free(s.data)
|
||
s.len = 0
|
||
|
||
proc toSeq*[T](s: SharedSeq[T]): seq[T] =
|
||
## Creates a seq[T] from a SharedSeq[T]. No explicit dealloc is required
|
||
## as req[T] is a GC managed type.
|
||
var ret = newSeq[T]()
|
||
for i in 0 ..< s.len:
|
||
ret.add(s.data[i])
|
||
return ret
|