nim-ffi/ffi/alloc.nim
Ivan FB ad493d6f9d
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>
2026-05-31 18:37:05 +02:00

92 lines
3.4 KiB
Nim
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 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