From 1b765fb60129cdf86700ecbc9fd90c4b27650743 Mon Sep 17 00:00:00 2001 From: Andrey Kolkov Date: Wed, 9 Sep 2026 11:43:49 +0300 Subject: [PATCH] docs: clarify avalue double indirection for out-pointer parameters (closes #79) Add comprehensive documentation of the avalue pointer-to-value convention in three places: - ffi/ffi.go godoc: avalue Convention section with WRONG/CORRECT examples - README.md: new 'avalue Convention' section with out-pointer pattern - docs/ARCHITECTURE.md: avalue Indirection Convention in Type System section Also update package doc: fix stale platform list and performance numbers. --- README.md | 28 ++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 13 +++++++++++++ ffi/ffi.go | 34 ++++++++++++++++++++++++++-------- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d15430e..e11f5de 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,34 @@ Structs >16 bytes are returned via hidden pointer (sret) — goffi handles this See [`examples/struct/`](examples/struct/) for a complete working example with compile-and-run. +### avalue Convention: Pointer-to-Value + +Every `avalue[i]` must be a **pointer TO the argument value**. GoFFI dereferences it to read +the actual value passed to C. This follows the [libffi convention](https://sourceware.org/libffi/). + +For most cases this is straightforward — `unsafe.Pointer(&x)` for scalars, `unsafe.Pointer(&buf)` for +pointers. The non-obvious case is **out-pointer parameters** (where C writes a result through a pointer): + +```go +// C: int get_device(uint32_t index, void **device) +// The function writes *device — an out-pointer parameter. + +// WRONG — C receives NULL (GoFFI reads the VALUE of device, which is nil): +var device unsafe.Pointer +_, _ = ffi.CallFunction(&cif, fn, unsafe.Pointer(&ret), + []unsafe.Pointer{unsafe.Pointer(&index), unsafe.Pointer(&device)}) + +// CORRECT — C receives &device (GoFFI reads the VALUE of devicePtr, which is &device): +var device unsafe.Pointer +devicePtr := unsafe.Pointer(&device) +_, _ = ffi.CallFunction(&cif, fn, unsafe.Pointer(&ret), + []unsafe.Pointer{unsafe.Pointer(&index), unsafe.Pointer(&devicePtr)}) +// After call: device holds the handle written by C +``` + +**Rule of thumb:** if the C parameter is `T *out` (C writes through it), you need an intermediate +`outPtr := unsafe.Pointer(&out)` and pass `unsafe.Pointer(&outPtr)` in avalue. + --- ## Performance diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index af86ab0..5dfdb9a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -324,6 +324,19 @@ pointType := &types.TypeDescriptor{ Five typed error types for precise error handling: `InvalidCallInterfaceError`, `LibraryError`, `CallingConventionError`, `TypeValidationError`, `UnsupportedPlatformError`. +### avalue Indirection Convention + +Following the libffi convention, `avalue[i]` is a **pointer TO the argument value**. GoFFI dereferences `avalue[i]` once to read the value placed into the register or stack slot: + +``` +C parameter: int x → avalue[i] = &x → GoFFI reads *&x = x value +C parameter: char *buf → avalue[i] = &buf → GoFFI reads *&buf = buf address +C parameter: int *out → avalue[i] = &outPtr → GoFFI reads *&outPtr = &out + where outPtr = &out (C receives address of out) +``` + +The third case (out-pointer) requires an intermediate Go variable. Without it, GoFFI would read the current value of `out` (likely zero) instead of its address, and the C function receives NULL. + --- ## Variadic Function Support diff --git a/ffi/ffi.go b/ffi/ffi.go index 33f6ab3..a504afd 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -46,16 +46,13 @@ // // # Supported Platforms // -// - Linux AMD64 (System V ABI) -// - Windows AMD64 (Win64 ABI) -// - macOS AMD64 (planned) -// - ARM64 (planned) +// - Linux, Windows, macOS, FreeBSD (AMD64 + ARM64) — 8 production targets +// - Android ARM64 (API 29+, guarded preview) // // # Performance // -// This implementation uses hand-optimized assembly for each platform's calling -// convention. Overhead is approximately 50-60ns per call, which is negligible -// for most use cases (e.g., WebGPU rendering). +// Hand-optimized assembly per platform ABI. Overhead: 88-114 ns/op with +// errno capture. sync.Pool for callback stack-move safety (0 allocs steady state). // // # Safety // @@ -248,11 +245,32 @@ func PrepareVariadicCallInterface( // - Once the C function starts executing, it CANNOT be interrupted mid-flight. // - For cancellable operations, the C library itself must support cancellation. // +// # avalue Convention (critical for correctness) +// +// Each avalue[i] is a pointer TO the argument value, following the libffi convention. +// GoFFI dereferences avalue[i] to read the value passed to the C function: +// +// Input scalar (int x): avalue[i] = unsafe.Pointer(&x) +// Input pointer (char *buf): avalue[i] = unsafe.Pointer(&buf) // GoFFI reads buf → C gets buf +// Out-pointer (int *result): avalue[i] = unsafe.Pointer(&resultPtr) +// where resultPtr = unsafe.Pointer(&result) +// +// The out-pointer case requires an intermediate variable. Without it, GoFFI reads +// the VALUE of result (likely 0) instead of its ADDRESS, and the C function receives NULL: +// +// // WRONG — C receives NULL: +// var handle unsafe.Pointer +// avalue := []unsafe.Pointer{unsafe.Pointer(&handle)} // GoFFI reads handle (nil) +// +// // CORRECT — C receives &handle: +// var handle unsafe.Pointer +// handlePtr := unsafe.Pointer(&handle) +// avalue := []unsafe.Pointer{unsafe.Pointer(&handlePtr)} // GoFFI reads handlePtr (&handle) +// // Safety: // - All argument pointers must remain valid during the call // - Return value buffer must be large enough for the result type // - Use runtime.KeepAlive() if needed to prevent premature GC of arguments -// - Use runtime.Pinner to pin pointers under a moving GC func CallFunctionContext( ctx context.Context, cif *types.CallInterface,