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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.5.4] - 2026-06-15

### Changed
- **Zero-allocation FFI calls** — added `//go:noescape` to `runtime_cgocall` linkname declarations, eliminating 1 heap allocation (208–248 bytes) per call on all Unix platforms. `syscallArgs` now stays on the goroutine stack. Expected 10–25% performance improvement on the hot path ([#54](https://github.com/go-webgpu/goffi/issues/54))
- **ABI-safe struct layout** — added `structs.HostLayout` (Go 1.23+) to all 8 assembly-interface structures (`syscallArgs`, `callbackArgs`, `dlopenArgs`, `dlsymArgs`, `dlerrorArgs`, `G`, `ThreadStart`). Guarantees C ABI-compatible memory layout regardless of future Go compiler optimizations ([#55](https://github.com/go-webgpu/goffi/issues/55))

## [0.5.3] - 2026-05-28

### Fixed
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,11 @@ variadic arg type slices.

| Benchmark | Time | Allocations |
|-----------|------|-------------|
| Empty function (`getpid`) | 88 ns | 2 allocs |
| Integer argument (`abs`) | 114 ns | 3 allocs |
| String processing (`strlen`) | 98 ns | 3 allocs |
| Empty function (`getpid`) | 88 ns | 0 allocs (Unix) / 2 allocs (Windows) |
| Integer argument (`abs`) | 114 ns | 0 allocs (Unix) / 3 allocs (Windows) |
| String processing (`strlen`) | 98 ns | 0 allocs (Unix) / 3 allocs (Windows) |

Since v0.5.4, `//go:noescape` on `runtime_cgocall` keeps `syscallArgs` on the goroutine stack — true zero-allocation FFI on Unix platforms. Windows uses `syscall.SyscallN` (runtime-managed).

At 60 FPS with ~50 FFI calls per frame, overhead is **5 µs per frame** — 0.03% of the 16.6 ms budget. Unmeasurable in profiling.

Expand Down
2 changes: 2 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ ffi.PrepareCallInterface(cif, types.DefaultCall,
1. Switches to system stack (g0)
2. Marks goroutine as "in syscall" — allows GC to proceed
3. Calls our assembly wrapper

Since v0.5.4, the `runtime_cgocall` linkname declaration has `//go:noescape`, keeping `syscallArgs` on the goroutine stack (zero heap allocations). All ABI-boundary structs use `structs.HostLayout` (Go 1.23+) to guarantee C-compatible memory layout.
4. Restores Go stack on return

We access it via `//go:linkname`:
Expand Down
2 changes: 1 addition & 1 deletion docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
- **Minimum FFI overhead**: ~88 ns (empty function)
- **Typical overhead**: ~100-115 ns (with arguments)
- **Overhead ratio**: ~400-500x vs direct Go call
- **Allocations**: 2-3 per call (runtime.cgocall internals)
- **Allocations**: 2-3 per call (runtime.cgocall internals). Since v0.5.4, `//go:noescape` on `runtime_cgocall` keeps `syscallArgs` on the goroutine stack, eliminating the primary per-call heap allocation on Unix platforms.

### 2. One-Time Costs

Expand Down
2 changes: 2 additions & 0 deletions ffi/callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package ffi

import (
"reflect"
"structs"
"sync"
"unsafe"
)
Expand All @@ -30,6 +31,7 @@ var callbacks struct {
// The assembly code saves all CPU registers (both integer and SSE) into a contiguous
// memory block following this structure.
type callbackArgs struct {
_ structs.HostLayout
index uintptr // Callback index (0-1999)
args unsafe.Pointer // Pointer to register/stack argument block
result uintptr // Return value from Go callback
Expand Down
2 changes: 2 additions & 0 deletions ffi/callback_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package ffi

import (
"reflect"
"structs"
"sync"
"unsafe"
)
Expand All @@ -25,6 +26,7 @@ var callbacks struct {
// callbackArgs represents the argument block passed from assembly to callbackWrap.
// ARM64 AAPCS64 layout: D0-D7 (float), X0-X7 (integer)
type callbackArgs struct {
_ structs.HostLayout
index uintptr // Callback index (0-1999)
args unsafe.Pointer // Pointer to register/stack argument block
result uintptr // Return value from Go callback
Expand Down
5 changes: 5 additions & 0 deletions internal/dl/dl_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ package dl

import (
"fmt"
"structs"
"unsafe"
)

// RTLD constants are platform-specific - see dl_linux.go and dl_darwin.go

//go:linkname runtime_cgocall runtime.cgocall
//go:noescape
func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32

// Assembly stubs (JMP to dynamic symbols)
Expand All @@ -41,6 +43,7 @@ var dlerror_stubABI0 = uintptr(unsafe.Pointer(&dlerror_stub))

// dlopenArgs is the argument struct for dlopen_wrapper
type dlopenArgs struct {
_ structs.HostLayout
fn uintptr // offset 0 - function pointer
path *byte // offset 8 - C string path
mode int // offset 16 - mode flags
Expand All @@ -50,6 +53,7 @@ type dlopenArgs struct {

// dlsymArgs is the argument struct for dlsym_wrapper
type dlsymArgs struct {
_ structs.HostLayout
fn uintptr // offset 0 - function pointer
handle uintptr // offset 8 - library handle
symbol *byte // offset 16 - C string symbol name
Expand All @@ -59,6 +63,7 @@ type dlsymArgs struct {

// dlerrorArgs is the argument struct for dlerror_wrapper
type dlerrorArgs struct {
_ structs.HostLayout
fn uintptr // offset 0 - function pointer
result *byte // offset 8 - return value (char*)
}
Expand Down
4 changes: 4 additions & 0 deletions internal/fakecgo/libcgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package fakecgo

import "structs"

type (
size_t uintptr
// Sources:
Expand All @@ -28,11 +30,13 @@ const (
)

type G struct {
_ structs.HostLayout
stacklo uintptr
stackhi uintptr
}

type ThreadStart struct {
_ structs.HostLayout
g *G
tls *uintptr
fn uintptr
Expand Down
3 changes: 3 additions & 0 deletions internal/syscall/syscall_arm64.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package syscall

import (
"structs"
"unsafe"
)

//go:linkname runtime_cgocall runtime.cgocall
//go:noescape
func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32

// syscallArgs matches the layout expected by syscallN assembly.
Expand All @@ -29,6 +31,7 @@ func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32
// NOTE: f1-f8 and fr1-fr4 are raw bit patterns. For float32 values, the
// lower 32 bits contain the float32 representation (upper 32 bits are ignored).
type syscallArgs struct {
_ structs.HostLayout
fn uintptr
a1, a2, a3, a4, a5, a6, a7, a8 uintptr // X0-X7 (offsets 8-64)
a9, a10, a11, a12, a13, a14, a15 uintptr // stack spill (offsets 72-120)
Expand Down
3 changes: 3 additions & 0 deletions internal/syscall/syscall_unix_amd64.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
package syscall

import (
"structs"
"unsafe"
)

//go:linkname runtime_cgocall runtime.cgocall
//go:noescape
func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32

// syscallArgs matches the layout expected by syscallN assembly.
Expand All @@ -23,6 +25,7 @@ func runtime_cgocall(fn uintptr, arg unsafe.Pointer) int32
// r1: 192 (RAX return)
// r2: 200 (RDX return, used for 9-16 byte struct returns)
type syscallArgs struct {
_ structs.HostLayout
fn uintptr
a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr
f1, f2, f3, f4, f5, f6, f7, f8 uintptr
Expand Down
Loading