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

## [Unreleased]

## [0.5.2] - 2026-05-25

### Added
- **Variadic function support** — `PrepareVariadicCallInterface(cif, convention, nfixedargs, returnType, argTypes)` enables calling C variadic functions such as `printf`, `sprintf`, and custom variadic APIs. On Apple ARM64, variadic arguments are forced onto the stack at the fixed/variadic boundary per Apple's AAPCS64 extension (differs from standard AAPCS64 used on Linux ARM64). On all other platforms the call is functionally identical to `PrepareCallInterface`. Closes [#47](https://github.com/go-webgpu/goffi/issues/47)
- **ARM64 Darwin variadic ABI** — register allocators (GP and FP) are exhausted at `nfixedargs` boundary on `GOOS=darwin`, matching Apple's `clang` and `libffi ffi_prep_cif_var()` behaviour. This fixes incorrect variadic calls on Apple Silicon (M1/M2/M3/M4) where variadic arguments were incorrectly placed in X1-X7 instead of the stack
- `cmd/variadic-test` — standalone verification program that @unxed and others can run on Apple Silicon to confirm variadic function calls produce correct results: `go run github.com/go-webgpu/goffi/cmd/variadic-test`
- E2E variadic tests (`ffi/variadic_e2e_test.go`) — two gcc-compiled test functions (`sum_variadic`, `variadic_two_fixed`) exercised on Linux, macOS, FreeBSD, and Windows (amd64 + arm64) where gcc is available

## [0.5.1] - 2026-05-13

### Fixed
Expand Down Expand Up @@ -516,7 +524,7 @@ ffi.CallFunction(&cif, wgpuRequestAdapter, nil,
- Added comprehensive package documentation with usage examples
- Documented all exported functions with parameters, returns, and examples
- Added safety guidelines for `unsafe.Pointer` usage
- Created API audit documentation in `docs/dev/`
- Created API audit documentation
- **NEW**: [docs/PERFORMANCE.md](docs/PERFORMANCE.md) - 650+ lines comprehensive performance guide
- **NEW**: [ROADMAP.md](ROADMAP.md) - Development roadmap to v1.0.0
- **NEW**: [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines
Expand Down Expand Up @@ -747,7 +755,7 @@ if errors.As(err, &icErr) {

### Roadmap

See [API_TODO.md](docs/dev/API_TODO.md) for detailed roadmap to v1.0.
See [ROADMAP.md](ROADMAP.md) for detailed roadmap to v1.0.

**v0.3.0** (ARM64 Support) - Q1 2025
- ARM64 support (Linux + macOS AAPCS64 ABI)
Expand Down
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args)
| **Callbacks** | C→Go safe | `crosscall2` integration, struct args, works from any C thread |
| **Type-safe** | Runtime validation | 5 typed error types with `errors.As()` support |
| **Struct pass/return** | Full ABI | Args: INTEGER/SSE classification. Returns: ≤8B (RAX/XMM0), 9–16B (4 modes: RAX/XMM × RAX/XMM), >16B (sret) |
| **Variadic** | `printf`/`sprintf` | `PrepareVariadicCallInterface` — Apple ARM64 stack-force included |
| **Context** | Timeouts | `CallFunctionContext(ctx, ...)` cancellation |
| **Race detector** | `-race` compatible | `CGO_ENABLED=1 go test -race` works cleanly |
| **Tested** | 89% coverage | CI on Linux, Windows, macOS (CGO=0 and CGO=1) |
Expand Down Expand Up @@ -124,6 +125,47 @@ func main() {
}
```

### Example: Calling a Variadic C Function

Use `PrepareVariadicCallInterface` instead of `PrepareCallInterface` for C variadic functions
(`printf`, `sprintf`, custom variadic APIs). Specify `nfixedargs` — the count of fixed parameters
before `...` in the C prototype. goffi automatically applies Apple's ARM64 stack-force rule on
`darwin/arm64` so the same Go code works correctly on all platforms.

```go
// C prototype: int64_t sum_variadic(int64_t count, ...)
// Call as: sum_variadic(3, 10, 20, 30) → 60

var cif types.CallInterface
err := ffi.PrepareVariadicCallInterface(
&cif,
types.DefaultCall,
1, // nfixedargs: only 'count' is fixed; 10/20/30 are variadic
types.SInt64TypeDescriptor,
[]*types.TypeDescriptor{
types.SInt64TypeDescriptor, // count (fixed)
types.SInt64TypeDescriptor, // arg1 (variadic)
types.SInt64TypeDescriptor, // arg2 (variadic)
types.SInt64TypeDescriptor, // arg3 (variadic)
},
)

count := int64(3)
a1, a2, a3 := int64(10), int64(20), int64(30)
var result int64
ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{
unsafe.Pointer(&count),
unsafe.Pointer(&a1),
unsafe.Pointer(&a2),
unsafe.Pointer(&a3),
})
// result == 60
```

A new CIF must be prepared for each unique combination of variadic argument types. The fixed-arg
portion of the CIF can be reused by re-calling `PrepareVariadicCallInterface` with different
variadic arg type slices.

---

## Performance
Expand Down Expand Up @@ -251,8 +293,8 @@ if err != nil {
**Windows: float return values not captured from XMM0**
- `syscall.SyscallN` returns RAX only. Go `syscall` package limitation.

**Variadic functions not supported** (`printf`, `sprintf`)
- Use non-variadic wrappers. Planned for v0.5.0.
**Apple ARM64: variadic args always go on stack**
- Per Apple's AAPCS64 extension, variadic arguments must be passed on the stack even when GP/FP registers are available. Use `PrepareVariadicCallInterface` (not `PrepareCallInterface`) for variadic C functions on all platforms — goffi handles the Darwin-specific register flush automatically.

**Struct packing follows System V ABI only**
- Windows `#pragma pack` not honored. Manually specify `Size`/`Alignment` in `TypeDescriptor`.
Expand Down Expand Up @@ -292,7 +334,8 @@ if err != nil {
| v0.4.0 | Released | crosscall2 for C-thread callbacks |
| v0.4.1 | Released | ABI compliance audit — 10/11 gaps fixed |
| v0.4.2 | Released | purego compatibility (`-tags nofakecgo`) |
| **v0.5.0** | **Next** | Windows ARM64, FreeBSD, variadic functions, builder API |
| v0.5.1 | Released | Struct ABI, CGO_ENABLED=1, 9-16B XMM return |
| **v0.6.0** | **In progress** | Variadic functions (`PrepareVariadicCallInterface`), builder API |
| v1.0.0 | Planned | API stability (SemVer 2.0), security audit |

See [CHANGELOG.md](CHANGELOG.md) for version history and [ROADMAP.md](ROADMAP.md) for the full plan.
Expand Down
30 changes: 19 additions & 11 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
> **Strategic Approach**: Build production-ready Zero-CGO FFI with benchmarked performance
> **Philosophy**: Performance first, usability second, platform coverage third

**Last Updated**: 2026-05-13 | **Current Version**: v0.5.0 (v0.5.1 pending) | **Strategy**: Benchmarks → Callbacks → ARM64 → Runtime → ABI → v1.0 LTS | **Milestone**: v0.5.1 (struct ABI + CGO=1) → v0.6.0 Variadic/Builder → v1.0.0 LTS
**Last Updated**: 2026-05-25 | **Current Version**: v0.5.2 | **Strategy**: Benchmarks → Callbacks → ARM64 → Runtime → ABI → v1.0 LTS | **Milestone**: v0.5.2 (variadic) → v0.6.0 RegisterFunc/Builder → v1.0.0 LTS

---

Expand Down Expand Up @@ -58,9 +58,11 @@ v0.4.0 (CROSSCALL2 INTEGRATION) ✅ RELEASED 2026-02-27
↓ (usability)
v0.5.0 (PLATFORM COVERAGE) ✅ RELEASED 2026-03-29
↓ (struct ABI + CGO support)
v0.5.1 (STRUCT ABI + CGO_ENABLED=1) → 2026-05 (pending tag)
↓ (variadic + builder API)
v0.6.0 (VARIADIC + BUILDER API) → 2026 Q3
v0.5.1 (STRUCT ABI + CGO_ENABLED=1) ✅ RELEASED 2026-05-13
↓ (variadic + vet fixes)
v0.5.2 (VARIADIC FUNCTIONS) ✅ RELEASED 2026-05-25
↓ (RegisterFunc + builder API)
v0.6.0 (REGISTERFUNC + BUILDER API) → 2026 Q3
↓ (advanced features)
v0.8.0 (ADVANCED FEATURES) → 2026 Q3-Q4
↓ (community adoption + validation)
Expand Down Expand Up @@ -128,7 +130,7 @@ v1.0.0 LTS → Long-term support release (2027 Q1)
- **FreeBSD amd64** support (cross-compile verified)
- 7 platform targets (Linux/Windows/macOS/FreeBSD × amd64 + ARM64)

**v0.5.1** = Struct ABI + CGO_ENABLED=1 (2026-05, pending tag)
**v0.5.1** = Struct ABI + CGO_ENABLED=1 ✅ RELEASED (2026-05-13)
- **CGO_ENABLED=1 support** (PR #37 by @jiyeyuran) — dual-mode build, race detector compatible
- **Struct by-value argument passing** (PR #39, closes #33) — ≤8B/9-16B/>16B, INTEGER/SSE classification
- **Callback struct arguments** (PR #42 by @pekim, closes #41) — C→Go callbacks with struct args
Expand All @@ -137,10 +139,16 @@ v1.0.0 LTS → Long-term support release (2027 Q1)
- **E2E test infrastructure** — gcc-compiled C test library for struct passing verification
- Contributors: @jiyeyuran (CGO path maintainer), @pekim (callback structs)

**v0.6.0** = Variadic + Builder API (2026 Q3)
- Builder pattern API
- **Variadic function support** (printf, sprintf, etc.)
- RegisterFunc convenience API
**v0.5.2** = Variadic functions ✅ RELEASED (2026-05-25)
- **Variadic function support** — `PrepareVariadicCallInterface` with Apple ARM64 stack-force
- `go vet` clean — fixed dl_unix.go unsafe.Pointer warnings, syscall_linux_stub.s return signature
- `cmd/variadic-test` — standalone verification binary for Apple Silicon
- E2E variadic tests with gcc-compiled C test functions

**v0.6.0** = RegisterFunc + Builder API (2026 Q3)
- RegisterFunc convenience API (ADR-008)
- Library struct + OpenLibraryBytes (ADR-009)
- NewFunc/Call/CallCtx ergonomic wrappers (ADR-009)

**v1.0.0** = Long-term support release (2027 Q1)
- API stability guarantee
Expand All @@ -150,9 +158,9 @@ v1.0.0 LTS → Long-term support release (2027 Q1)

---

## 📊 Current Status (v0.5.1)
## 📊 Current Status (v0.5.2)

**Phase**: Struct ABI complete, CGO_ENABLED=1 supported, SSE struct return fixed
**Phase**: Variadic functions supported, go vet clean, planning v0.6.0 (RegisterFunc)

**What Works**:
- ✅ Dynamic library loading (`LoadLibrary`, `GetSymbol`, `FreeLibrary`)
Expand Down
204 changes: 204 additions & 0 deletions cmd/variadic-test/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Goffi Authors

//go:build (linux || darwin || freebsd) && (amd64 || arm64)

// Command variadic-test compiles the bundled C test library and exercises
// PrepareVariadicCallInterface on the current platform.
//
// On Apple ARM64 the key invariant under test is that variadic arguments are
// passed on the stack (not in registers), per Apple's AAPCS64 extension.
// On all other platforms the call is functionally identical to a normal
// PrepareCallInterface call.
//
// Usage:
//
// go run github.com/go-webgpu/goffi/cmd/variadic-test
//
// Exit code 0 = PASS, 1 = FAIL.
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"unsafe"

"github.com/go-webgpu/goffi/ffi"
"github.com/go-webgpu/goffi/types"
)

func main() {
fmt.Printf("Platform: %s/%s\n", runtime.GOOS, runtime.GOARCH)

soPath, err := buildLib()
if err != nil {
fmt.Fprintf(os.Stderr, "SKIP: failed to compile test library (gcc required): %v\n", err)
// Exit 0 so CI passes on machines without gcc.
os.Exit(0)
}

lib, err := ffi.LoadLibrary(soPath)
if err != nil {
fmt.Fprintf(os.Stderr, "FAIL: LoadLibrary(%q): %v\n", soPath, err)
os.Exit(1)
}
defer func() { _ = ffi.FreeLibrary(lib) }()

pass := true
pass = testSumVariadic(lib) && pass
pass = testTwoFixed(lib) && pass

if pass {
fmt.Println("PASS — variadic functions work on this platform")
} else {
fmt.Println("FAIL — one or more variadic tests failed")
os.Exit(1)
}
}

// buildLib compiles testdata/structtest.c into a shared library and returns
// its absolute path. The source file lives next to the goffi source tree,
// which cmd/variadic-test references via a relative path that stays inside
// the module root.
func buildLib() (string, error) {
// Resolve testdata relative to this source file's directory at runtime
// so the command works regardless of cwd.
_, thisFile, _, _ := runtime.Caller(0)
srcDir := filepath.Dir(thisFile)
// srcDir is …/cmd/variadic-test; go up two levels to reach the module root.
moduleRoot := filepath.Join(srcDir, "..", "..")
srcPath := filepath.Join(moduleRoot, "ffi", "testdata", "structtest.c")

var soPath string
switch runtime.GOOS {
case "darwin":
soPath = filepath.Join(os.TempDir(), "libstructtest.dylib")
default:
soPath = filepath.Join(os.TempDir(), "libstructtest.so")
}

cc := os.Getenv("CC")
if cc == "" {
cc = "gcc"
}

cmd := exec.Command(cc, "-shared", "-fPIC", "-O2", "-o", soPath, srcPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("%s: %w", cc, err)
}
return soPath, nil
}

// testSumVariadic tests sum_variadic(int64_t count, ...) with count=3 and
// three variadic int64_t arguments: 10, 20, 30. Expected return: 60.
func testSumVariadic(lib unsafe.Pointer) bool {
sym, err := ffi.GetSymbol(lib, "sum_variadic")
if err != nil {
fmt.Fprintf(os.Stderr, "FAIL: GetSymbol(sum_variadic): %v\n", err)
return false
}

allArgs := []*types.TypeDescriptor{
types.SInt64TypeDescriptor, // count (fixed)
types.SInt64TypeDescriptor, // arg1 (variadic)
types.SInt64TypeDescriptor, // arg2 (variadic)
types.SInt64TypeDescriptor, // arg3 (variadic)
}

var cif types.CallInterface
if err := ffi.PrepareVariadicCallInterface(
&cif,
types.DefaultCall,
1, // nfixedargs: only 'count' is fixed
types.SInt64TypeDescriptor,
allArgs,
); err != nil {
fmt.Fprintf(os.Stderr, "FAIL: PrepareVariadicCallInterface(sum_variadic): %v\n", err)
return false
}

count := int64(3)
a1 := int64(10)
a2 := int64(20)
a3 := int64(30)

avalue := []unsafe.Pointer{
unsafe.Pointer(&count),
unsafe.Pointer(&a1),
unsafe.Pointer(&a2),
unsafe.Pointer(&a3),
}

var result int64
if err := ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil {
fmt.Fprintf(os.Stderr, "FAIL: CallFunction(sum_variadic): %v\n", err)
return false
}

const want = int64(60)
if result != want {
fmt.Fprintf(os.Stderr, "FAIL: sum_variadic(3, 10, 20, 30) = %d, want %d\n", result, want)
return false
}

fmt.Printf(" sum_variadic(3, 10, 20, 30) = %d (want %d) OK\n", result, want)
return true
}

// testTwoFixed tests variadic_two_fixed(int64_t a, int64_t b, ...) with
// a=100, b=200, and one variadic int64_t: 300. Expected return: 600.
func testTwoFixed(lib unsafe.Pointer) bool {
sym, err := ffi.GetSymbol(lib, "variadic_two_fixed")
if err != nil {
fmt.Fprintf(os.Stderr, "FAIL: GetSymbol(variadic_two_fixed): %v\n", err)
return false
}

allArgs := []*types.TypeDescriptor{
types.SInt64TypeDescriptor, // a (fixed)
types.SInt64TypeDescriptor, // b (fixed)
types.SInt64TypeDescriptor, // extra (variadic)
}

var cif types.CallInterface
if err := ffi.PrepareVariadicCallInterface(
&cif,
types.DefaultCall,
2, // nfixedargs: a and b are fixed
types.SInt64TypeDescriptor,
allArgs,
); err != nil {
fmt.Fprintf(os.Stderr, "FAIL: PrepareVariadicCallInterface(variadic_two_fixed): %v\n", err)
return false
}

a := int64(100)
b := int64(200)
extra := int64(300)

avalue := []unsafe.Pointer{
unsafe.Pointer(&a),
unsafe.Pointer(&b),
unsafe.Pointer(&extra),
}

var result int64
if err := ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil {
fmt.Fprintf(os.Stderr, "FAIL: CallFunction(variadic_two_fixed): %v\n", err)
return false
}

const want = int64(600)
if result != want {
fmt.Fprintf(os.Stderr, "FAIL: variadic_two_fixed(100, 200, 300) = %d, want %d\n", result, want)
return false
}

fmt.Printf(" variadic_two_fixed(100, 200, 300) = %d (want %d) OK\n", result, want)
return true
}
Loading
Loading