Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 26 additions & 8 deletions ffi/ffi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
//
Expand Down Expand Up @@ -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,
Expand Down
Loading