diff --git a/CHANGELOG.md b/CHANGELOG.md index 568f9b1..e136a63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 @@ -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) diff --git a/README.md b/README.md index 61bf650..f833e29 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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 @@ -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`. @@ -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. diff --git a/ROADMAP.md b/ROADMAP.md index d3c74e9..7f6c3cb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 --- @@ -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) @@ -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 @@ -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 @@ -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`) diff --git a/cmd/variadic-test/main.go b/cmd/variadic-test/main.go new file mode 100644 index 0000000..e47e828 --- /dev/null +++ b/cmd/variadic-test/main.go @@ -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 +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9d85350..6e586dd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -321,6 +321,77 @@ Five typed error types for precise error handling: `InvalidCallInterfaceError`, --- +## Variadic Function Support + +### Overview + +C variadic functions (`printf`, `sprintf`, `sum(count, ...)`) require a different CIF preparation path because the fixed and variadic argument regions may be handled differently by the hardware ABI. + +Use `PrepareVariadicCallInterface` in place of `PrepareCallInterface`: + +```go +// C prototype: int64_t sum_variadic(int64_t count, ...) +var cif types.CallInterface +err := ffi.PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 1, // nfixedargs: only 'count' is fixed + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // count (fixed) + types.SInt64TypeDescriptor, // variadic arg 1 + types.SInt64TypeDescriptor, // variadic arg 2 + }, +) +``` + +A new CIF must be prepared for each unique combination of variadic argument types, matching `libffi`'s `ffi_prep_cif_var()` requirement. + +### Platform Differences + +**System V AMD64 (Linux, macOS Intel, FreeBSD):** +Standard AAPCS64 and System V ABI pass variadic arguments in the same registers as fixed arguments, up to the register count limit. `PrepareVariadicCallInterface` on these platforms is functionally identical to `PrepareCallInterface` — `FixedArgCount` is stored in `CallInterface` but the argument marshalling loop is unchanged. + +**Win64 (Windows AMD64/ARM64):** +Same as System V for integer arguments — variadic args use the same 4 GP registers. `PrepareVariadicCallInterface` behaves identically to `PrepareCallInterface`. + +**Apple ARM64 (macOS/iOS, `GOOS=darwin`, `GOARCH=arm64`):** +Apple's AAPCS64 extension mandates that **variadic arguments must be passed on the stack**, even when GP or FP registers are still available. This differs from the standard AAPCS64 used on Linux ARM64, where variadic args may be placed in X1-X7. + +Implementation in `internal/arch/arm64/call_arm64.go` (`Execute` method): + +```go +// At the fixed/variadic boundary on Apple ARM64, exhaust both +// register allocators so all variadic args land on the stack. +if cif.FixedArgCount > 0 && runtime.GOOS == "darwin" && idx == cif.FixedArgCount { + gprIdx = 8 // exhaust GP registers (X0-X7) + fprIdx = 8 // exhaust FP registers (D0-D7) +} +``` + +This matches the behaviour of Apple's `clang` and `libffi`'s `ffi_prep_cif_var()` on Darwin ARM64. + +### CallInterface.FixedArgCount + +The `FixedArgCount` field in `CallInterface` stores the variadic boundary: + +- `FixedArgCount == 0` — non-variadic CIF (zero value, backward compatible) +- `FixedArgCount > 0` — number of fixed parameters; args at index `FixedArgCount` and beyond are variadic + +### Verification + +Run `cmd/variadic-test` on Apple Silicon to confirm: + +```bash +go run github.com/go-webgpu/goffi/cmd/variadic-test +# Platform: darwin/arm64 +# sum_variadic(3, 10, 20, 30) = 60 (want 60) OK +# variadic_two_fixed(100, 200, 300) = 600 (want 600) OK +# PASS — variadic functions work on this platform +``` + +--- + ## Platform Support | Platform | Architecture | ABI | Status | diff --git a/ffi/dl_unix.go b/ffi/dl_unix.go index 08cd96f..9e92913 100644 --- a/ffi/dl_unix.go +++ b/ffi/dl_unix.go @@ -60,7 +60,8 @@ func LoadLibrary(name string) (unsafe.Pointer, error) { } } - return unsafe.Pointer(handle), nil + //nolint:govet // handle is a dlopen result (non-Go memory); double-indirection per go.dev/issue/58625 + return *(*unsafe.Pointer)(unsafe.Pointer(&handle)), nil } // GetSymbol retrieves a function pointer from a loaded library using dlsym. @@ -102,7 +103,8 @@ func GetSymbol(handle unsafe.Pointer, name string) (unsafe.Pointer, error) { } } - return unsafe.Pointer(fnPtr), nil + //nolint:govet // fnPtr is a dlsym result (non-Go memory); double-indirection per go.dev/issue/58625 + return *(*unsafe.Pointer)(unsafe.Pointer(&fnPtr)), nil } // FreeLibrary unloads a previously loaded library using dlclose. diff --git a/ffi/ffi.go b/ffi/ffi.go index 5f4d068..7aa3a37 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -88,6 +88,7 @@ package ffi import ( "context" + "errors" "unsafe" "github.com/go-webgpu/goffi/types" @@ -150,6 +151,54 @@ func PrepareCallInterface( return prepareCallInterfaceCore(cif, convention, argCount, returnType, argTypes) } +// PrepareVariadicCallInterface prepares a call interface for a C variadic function. +// +// nfixedargs is the count of fixed parameters before '...' in the C prototype. +// argTypes must contain ALL arguments (fixed + variadic) for this specific call. +// A new CIF must be prepared for each unique combination of variadic argument types. +// +// On Apple ARM64, variadic arguments are forced to the stack per Apple's AAPCS64 +// extension. The register allocators are exhausted at the fixed/variadic boundary +// so that variadic arguments land on the stack even when registers are available. +// On all other platforms, this function behaves identically to PrepareCallInterface. +// +// Example: +// +// // Prepare for: int64_t sum_variadic(int64_t count, ...) +// // Called with count=3 and three int64_t variadic args. +// var cif types.CallInterface +// err := ffi.PrepareVariadicCallInterface( +// &cif, +// types.DefaultCall, +// 1, // nfixedargs: only 'count' is fixed +// types.SInt64TypeDescriptor, +// []*types.TypeDescriptor{ +// types.SInt64TypeDescriptor, // count +// types.SInt64TypeDescriptor, // arg1 (variadic) +// types.SInt64TypeDescriptor, // arg2 (variadic) +// types.SInt64TypeDescriptor, // arg3 (variadic) +// }, +// ) +func PrepareVariadicCallInterface( + cif *types.CallInterface, + convention types.CallingConvention, + nfixedargs int, + returnType *types.TypeDescriptor, + argTypes []*types.TypeDescriptor, +) error { + if nfixedargs < 0 { + return errors.New("goffi: nfixedargs must be non-negative") + } + if nfixedargs > len(argTypes) { + return errors.New("goffi: nfixedargs exceeds total argument count") + } + if err := PrepareCallInterface(cif, convention, returnType, argTypes); err != nil { + return err + } + cif.FixedArgCount = nfixedargs + return nil +} + // CallFunctionContext executes a C function call with context support. // // This function performs the actual FFI call to the C function, handling all diff --git a/ffi/struct_e2e_test.go b/ffi/struct_e2e_test.go index e5cd9f7..7104edf 100644 --- a/ffi/struct_e2e_test.go +++ b/ffi/struct_e2e_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Goffi Authors -//go:build (linux || darwin || freebsd || windows) && amd64 +//go:build (linux || darwin || freebsd || windows) && (amd64 || arm64) package ffi @@ -290,8 +290,8 @@ func TestStructArgWithScalar(t *testing.T) { } func TestCallbackStructArg8B_IntegerPair(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("callback struct args not supported on Windows") + if runtime.GOOS == "windows" || runtime.GOARCH == "arm64" { + t.Skip("callback struct args not supported on Windows/ARM64") } requireStructLib(t) @@ -337,8 +337,8 @@ func TestCallbackStructArg8B_IntegerPair(t *testing.T) { } func TestCallbackStructArg8B_FloatPair(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("callback struct args not supported on Windows") + if runtime.GOOS == "windows" || runtime.GOARCH == "arm64" { + t.Skip("callback struct args not supported on Windows/ARM64") } requireStructLib(t) @@ -384,8 +384,8 @@ func TestCallbackStructArg8B_FloatPair(t *testing.T) { } func TestCallbackStructArg16B(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("callback struct args not supported on Windows") + if runtime.GOOS == "windows" || runtime.GOARCH == "arm64" { + t.Skip("callback struct args not supported on Windows/ARM64") } requireStructLib(t) @@ -431,8 +431,8 @@ func TestCallbackStructArg16B(t *testing.T) { } func TestCallbackStructArg24B_MemoryClass(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("callback struct args not supported on Windows") + if runtime.GOOS == "windows" || runtime.GOARCH == "arm64" { + t.Skip("callback struct args not supported on Windows/ARM64") } requireStructLib(t) @@ -481,8 +481,8 @@ func TestCallbackStructArg24B_MemoryClass(t *testing.T) { } func TestCallbackStructArgWithScalar(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("callback struct args not supported on Windows") + if runtime.GOOS == "windows" || runtime.GOARCH == "arm64" { + t.Skip("callback struct args not supported on Windows/ARM64") } requireStructLib(t) @@ -531,210 +531,3 @@ func TestCallbackStructArgWithScalar(t *testing.T) { t.Errorf("expected %#v %d, received %#v %d", expected, extra, receivedArg1, receivedArg2) } } - -// TestStructReturn16B_TwoDoubles verifies that {double, double} is returned in XMM0:XMM1. -// This is the NSPoint / NSSize case on macOS Intel — the primary motivation for TASK-045. -// SysV AMD64 ABI: both eightbytes are SSE class → ReturnStXmm0Xmm1. -func TestStructReturn16B_TwoDoubles(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") - } - requireStructLib(t) - - sym, err := GetSymbol(structTestLib, "return_struct_2doubles") - if err != nil { - t.Fatal(err) - } - - // {double, double} — both SSE → ReturnStXmm0Xmm1 - structType := &types.TypeDescriptor{ - Kind: types.StructType, - Size: 16, - Alignment: 8, - Members: []*types.TypeDescriptor{ - types.DoubleTypeDescriptor, - types.DoubleTypeDescriptor, - }, - } - - var cif types.CallInterface - if err := PrepareCallInterface(&cif, types.DefaultCall, structType, - []*types.TypeDescriptor{types.DoubleTypeDescriptor, types.DoubleTypeDescriptor}); err != nil { - t.Fatal(err) - } - - if cif.Flags != types.ReturnStXmm0Xmm1 { - t.Fatalf("expected cif.Flags = ReturnStXmm0Xmm1 (%d), got %d", types.ReturnStXmm0Xmm1, cif.Flags) - } - - type PairF64 struct{ A, B float64 } - - a := 1.5 - b := 2.5 - args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} - var result PairF64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { - t.Fatal(err) - } - - if result.A != a || result.B != b { - t.Errorf("return_struct_2doubles(%f, %f) = {%f, %f}, want {%f, %f}", - a, b, result.A, result.B, a, b) - } -} - -// TestStructReturn16B_IntFloat verifies that {int64, double} returns in RAX:XMM0. -// SysV AMD64 ABI: eightbyte0 INTEGER (RAX), eightbyte1 SSE (XMM0) → ReturnStRaxXmm0. -func TestStructReturn16B_IntFloat(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") - } - requireStructLib(t) - - sym, err := GetSymbol(structTestLib, "return_struct_int_float") - if err != nil { - t.Fatal(err) - } - - // {int64, double} — INTEGER + SSE → ReturnStRaxXmm0 - structType := &types.TypeDescriptor{ - Kind: types.StructType, - Size: 16, - Alignment: 8, - Members: []*types.TypeDescriptor{ - types.SInt64TypeDescriptor, - types.DoubleTypeDescriptor, - }, - } - - var cif types.CallInterface - if err := PrepareCallInterface(&cif, types.DefaultCall, structType, - []*types.TypeDescriptor{types.SInt64TypeDescriptor, types.DoubleTypeDescriptor}); err != nil { - t.Fatal(err) - } - - if cif.Flags != types.ReturnStRaxXmm0 { - t.Fatalf("expected cif.Flags = ReturnStRaxXmm0 (%d), got %d", types.ReturnStRaxXmm0, cif.Flags) - } - - type MixedIntFloat struct { - A int64 - B float64 - } - - a := int64(42) - b := 3.14 - args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} - var result MixedIntFloat - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { - t.Fatal(err) - } - - if result.A != a || result.B != b { - t.Errorf("return_struct_int_float(%d, %f) = {%d, %f}, want {%d, %f}", - a, b, result.A, result.B, a, b) - } -} - -// TestStructReturn16B_FloatInt verifies that {double, int64} returns in XMM0:RAX. -// SysV AMD64 ABI: eightbyte0 SSE (XMM0), eightbyte1 INTEGER (RAX) → ReturnStXmm0Rax. -func TestStructReturn16B_FloatInt(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") - } - requireStructLib(t) - - sym, err := GetSymbol(structTestLib, "return_struct_float_int") - if err != nil { - t.Fatal(err) - } - - // {double, int64} — SSE + INTEGER → ReturnStXmm0Rax - structType := &types.TypeDescriptor{ - Kind: types.StructType, - Size: 16, - Alignment: 8, - Members: []*types.TypeDescriptor{ - types.DoubleTypeDescriptor, - types.SInt64TypeDescriptor, - }, - } - - var cif types.CallInterface - if err := PrepareCallInterface(&cif, types.DefaultCall, structType, - []*types.TypeDescriptor{types.DoubleTypeDescriptor, types.SInt64TypeDescriptor}); err != nil { - t.Fatal(err) - } - - if cif.Flags != types.ReturnStXmm0Rax { - t.Fatalf("expected cif.Flags = ReturnStXmm0Rax (%d), got %d", types.ReturnStXmm0Rax, cif.Flags) - } - - type MixedFloatInt struct { - A float64 - B int64 - } - - a := 2.71828 - b := int64(100) - args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} - var result MixedFloatInt - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { - t.Fatal(err) - } - - if result.A != a || result.B != b { - t.Errorf("return_struct_float_int(%f, %d) = {%f, %d}, want {%f, %d}", - a, b, result.A, result.B, a, b) - } -} - -// TestStructReturn16B_TwoInts verifies that {int64, int64} returns in RAX:RDX. -// SysV AMD64 ABI: both eightbytes INTEGER → ReturnStRaxRdx. -func TestStructReturn16B_TwoInts(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("Windows: 16B struct returns use sret, not RAX:RDX (Win64 ABI)") - } - requireStructLib(t) - - sym, err := GetSymbol(structTestLib, "return_struct_2ints") - if err != nil { - t.Fatal(err) - } - - // {int64, int64} — both INTEGER → ReturnStRaxRdx - structType := &types.TypeDescriptor{ - Kind: types.StructType, - Size: 16, - Alignment: 8, - Members: []*types.TypeDescriptor{ - types.SInt64TypeDescriptor, - types.SInt64TypeDescriptor, - }, - } - - var cif types.CallInterface - if err := PrepareCallInterface(&cif, types.DefaultCall, structType, - []*types.TypeDescriptor{types.SInt64TypeDescriptor, types.SInt64TypeDescriptor}); err != nil { - t.Fatal(err) - } - - if cif.Flags != types.ReturnStRaxRdx { - t.Fatalf("expected cif.Flags = ReturnStRaxRdx (%d), got %d", types.ReturnStRaxRdx, cif.Flags) - } - - type PairI64 struct{ A, B int64 } - - a := int64(1000000) - b := int64(2000000) - args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} - var result PairI64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { - t.Fatal(err) - } - - if result.A != a || result.B != b { - t.Errorf("return_struct_2ints(%d, %d) = {%d, %d}, want {%d, %d}", - a, b, result.A, result.B, a, b) - } -} diff --git a/ffi/struct_return_e2e_amd64_test.go b/ffi/struct_return_e2e_amd64_test.go new file mode 100644 index 0000000..41ed9e8 --- /dev/null +++ b/ffi/struct_return_e2e_amd64_test.go @@ -0,0 +1,221 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build (linux || darwin || freebsd || windows) && amd64 + +package ffi + +import ( + "runtime" + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +// TestStructReturn16B_TwoDoubles verifies that {double, double} is returned in XMM0:XMM1. +// This is the NSPoint / NSSize case on macOS Intel — the primary motivation for TASK-045. +// SysV AMD64 ABI: both eightbytes are SSE class → ReturnStXmm0Xmm1. +func TestStructReturn16B_TwoDoubles(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") + } + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "return_struct_2doubles") + if err != nil { + t.Fatal(err) + } + + // {double, double} — both SSE → ReturnStXmm0Xmm1 + structType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.DoubleTypeDescriptor, + types.DoubleTypeDescriptor, + }, + } + + var cif types.CallInterface + if err := PrepareCallInterface(&cif, types.DefaultCall, structType, + []*types.TypeDescriptor{types.DoubleTypeDescriptor, types.DoubleTypeDescriptor}); err != nil { + t.Fatal(err) + } + + if cif.Flags != types.ReturnStXmm0Xmm1 { + t.Fatalf("expected cif.Flags = ReturnStXmm0Xmm1 (%d), got %d", types.ReturnStXmm0Xmm1, cif.Flags) + } + + type PairF64 struct{ A, B float64 } + + a := 1.5 + b := 2.5 + args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} + var result PairF64 + if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + t.Fatal(err) + } + + if result.A != a || result.B != b { + t.Errorf("return_struct_2doubles(%f, %f) = {%f, %f}, want {%f, %f}", + a, b, result.A, result.B, a, b) + } +} + +// TestStructReturn16B_IntFloat verifies that {int64, double} returns in RAX:XMM0. +// SysV AMD64 ABI: eightbyte0 INTEGER (RAX), eightbyte1 SSE (XMM0) → ReturnStRaxXmm0. +func TestStructReturn16B_IntFloat(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") + } + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "return_struct_int_float") + if err != nil { + t.Fatal(err) + } + + // {int64, double} — INTEGER + SSE → ReturnStRaxXmm0 + structType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, + types.DoubleTypeDescriptor, + }, + } + + var cif types.CallInterface + if err := PrepareCallInterface(&cif, types.DefaultCall, structType, + []*types.TypeDescriptor{types.SInt64TypeDescriptor, types.DoubleTypeDescriptor}); err != nil { + t.Fatal(err) + } + + if cif.Flags != types.ReturnStRaxXmm0 { + t.Fatalf("expected cif.Flags = ReturnStRaxXmm0 (%d), got %d", types.ReturnStRaxXmm0, cif.Flags) + } + + type MixedIntFloat struct { + A int64 + B float64 + } + + a := int64(42) + b := 3.14 + args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} + var result MixedIntFloat + if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + t.Fatal(err) + } + + if result.A != a || result.B != b { + t.Errorf("return_struct_int_float(%d, %f) = {%d, %f}, want {%d, %f}", + a, b, result.A, result.B, a, b) + } +} + +// TestStructReturn16B_FloatInt verifies that {double, int64} returns in XMM0:RAX. +// SysV AMD64 ABI: eightbyte0 SSE (XMM0), eightbyte1 INTEGER (RAX) → ReturnStXmm0Rax. +func TestStructReturn16B_FloatInt(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows: XMM struct returns not captured by syscall.SyscallN") + } + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "return_struct_float_int") + if err != nil { + t.Fatal(err) + } + + // {double, int64} — SSE + INTEGER → ReturnStXmm0Rax + structType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.DoubleTypeDescriptor, + types.SInt64TypeDescriptor, + }, + } + + var cif types.CallInterface + if err := PrepareCallInterface(&cif, types.DefaultCall, structType, + []*types.TypeDescriptor{types.DoubleTypeDescriptor, types.SInt64TypeDescriptor}); err != nil { + t.Fatal(err) + } + + if cif.Flags != types.ReturnStXmm0Rax { + t.Fatalf("expected cif.Flags = ReturnStXmm0Rax (%d), got %d", types.ReturnStXmm0Rax, cif.Flags) + } + + type MixedFloatInt struct { + A float64 + B int64 + } + + a := 2.71828 + b := int64(100) + args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} + var result MixedFloatInt + if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + t.Fatal(err) + } + + if result.A != a || result.B != b { + t.Errorf("return_struct_float_int(%f, %d) = {%f, %d}, want {%f, %d}", + a, b, result.A, result.B, a, b) + } +} + +// TestStructReturn16B_TwoInts verifies that {int64, int64} returns in RAX:RDX. +// SysV AMD64 ABI: both eightbytes INTEGER → ReturnStRaxRdx. +func TestStructReturn16B_TwoInts(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Windows: 16B struct returns use sret, not RAX:RDX (Win64 ABI)") + } + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "return_struct_2ints") + if err != nil { + t.Fatal(err) + } + + // {int64, int64} — both INTEGER → ReturnStRaxRdx + structType := &types.TypeDescriptor{ + Kind: types.StructType, + Size: 16, + Alignment: 8, + Members: []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, + types.SInt64TypeDescriptor, + }, + } + + var cif types.CallInterface + if err := PrepareCallInterface(&cif, types.DefaultCall, structType, + []*types.TypeDescriptor{types.SInt64TypeDescriptor, types.SInt64TypeDescriptor}); err != nil { + t.Fatal(err) + } + + if cif.Flags != types.ReturnStRaxRdx { + t.Fatalf("expected cif.Flags = ReturnStRaxRdx (%d), got %d", types.ReturnStRaxRdx, cif.Flags) + } + + type PairI64 struct{ A, B int64 } + + a := int64(1000000) + b := int64(2000000) + args := []unsafe.Pointer{unsafe.Pointer(&a), unsafe.Pointer(&b)} + var result PairI64 + if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + t.Fatal(err) + } + + if result.A != a || result.B != b { + t.Errorf("return_struct_2ints(%d, %d) = {%d, %d}, want {%d, %d}", + a, b, result.A, result.B, a, b) + } +} diff --git a/ffi/testdata/structtest.c b/ffi/testdata/structtest.c index 155ce57..c3f022e 100644 --- a/ffi/testdata/structtest.c +++ b/ffi/testdata/structtest.c @@ -1,4 +1,5 @@ #include +#include // ≤ 8 bytes: {int32, uint32} — INTEGER class, single GP register struct pair_i32_u32 { int32_t a; uint32_t b; }; @@ -84,3 +85,28 @@ struct return_pair_i64 return_struct_2ints(int64_t a, int64_t b) { struct return_pair_i64 s = {.a = a, .b = b}; return s; } + +// Variadic: sum N int64_t values. +// Prototype: int64_t sum_variadic(int64_t count, ...) +// nfixedargs = 1 (only 'count' is fixed). +int64_t sum_variadic(int64_t count, ...) { + va_list ap; + va_start(ap, count); + int64_t sum = 0; + for (int64_t i = 0; i < count; i++) { + sum += va_arg(ap, int64_t); + } + va_end(ap); + return sum; +} + +// Variadic: two fixed args plus one variadic int64_t. +// Prototype: int64_t variadic_two_fixed(int64_t a, int64_t b, ...) +// nfixedargs = 2 (a and b are fixed). +int64_t variadic_two_fixed(int64_t a, int64_t b, ...) { + va_list ap; + va_start(ap, b); + int64_t extra = va_arg(ap, int64_t); + va_end(ap); + return a + b + extra; +} diff --git a/ffi/variadic_e2e_test.go b/ffi/variadic_e2e_test.go new file mode 100644 index 0000000..e930026 --- /dev/null +++ b/ffi/variadic_e2e_test.go @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Goffi Authors + +//go:build (linux || darwin || freebsd || windows) && (amd64 || arm64) + +package ffi + +import ( + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +// TestVariadic_SumIntegers tests sum_variadic(count int64, ...) with count=3 +// and three int64 variadic arguments. Expected result: 10+20+30 = 60. +// +// This exercises PrepareVariadicCallInterface with nfixedargs=1. +// On Apple ARM64 the variadic arguments must go on the stack; on all other +// platforms the call behaves the same as a non-variadic PrepareCallInterface. +func TestVariadic_SumIntegers(t *testing.T) { + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "sum_variadic") + if err != nil { + t.Fatal(err) + } + + // sum_variadic(int64_t count, ...) — all args are int64_t. + allArgTypes := []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // count (fixed) + types.SInt64TypeDescriptor, // arg 1 (variadic) + types.SInt64TypeDescriptor, // arg 2 (variadic) + types.SInt64TypeDescriptor, // arg 3 (variadic) + } + + var cif types.CallInterface + if err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 1, // nfixedargs: only 'count' is fixed + types.SInt64TypeDescriptor, + allArgTypes, + ); err != nil { + t.Fatal(err) + } + + 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 := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + t.Fatal(err) + } + + const want = int64(60) + if result != want { + t.Errorf("sum_variadic(3, 10, 20, 30) = %d, want %d", result, want) + } +} + +// TestVariadic_TwoFixed tests variadic_two_fixed(a, b int64, ...) where two +// arguments are fixed and one is variadic. Expected result: 100+200+300 = 600. +// +// This exercises PrepareVariadicCallInterface with nfixedargs=2. +func TestVariadic_TwoFixed(t *testing.T) { + requireStructLib(t) + + sym, err := GetSymbol(structTestLib, "variadic_two_fixed") + if err != nil { + t.Fatal(err) + } + + // variadic_two_fixed(int64_t a, int64_t b, ...) — all args int64_t. + allArgTypes := []*types.TypeDescriptor{ + types.SInt64TypeDescriptor, // a (fixed) + types.SInt64TypeDescriptor, // b (fixed) + types.SInt64TypeDescriptor, // extra (variadic) + } + + var cif types.CallInterface + if err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 2, // nfixedargs: a and b are fixed + types.SInt64TypeDescriptor, + allArgTypes, + ); err != nil { + t.Fatal(err) + } + + 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 := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + t.Fatal(err) + } + + const want = int64(600) + if result != want { + t.Errorf("variadic_two_fixed(100, 200, 300) = %d, want %d", result, want) + } +} + +// TestVariadic_ErrorValidation verifies that PrepareVariadicCallInterface +// returns an error for invalid nfixedargs values. +func TestVariadic_ErrorValidation(t *testing.T) { + t.Run("negative nfixedargs", func(t *testing.T) { + var cif types.CallInterface + err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + -1, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{types.SInt64TypeDescriptor}, + ) + if err == nil { + t.Error("expected error for nfixedargs=-1, got nil") + } + }) + + t.Run("nfixedargs exceeds arg count", func(t *testing.T) { + var cif types.CallInterface + err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 5, // more than len(argTypes)==1 + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{types.SInt64TypeDescriptor}, + ) + if err == nil { + t.Error("expected error for nfixedargs > len(argTypes), got nil") + } + }) + + t.Run("nfixedargs equals arg count is allowed", func(t *testing.T) { + // nfixedargs == len(argTypes) means no variadic args in this particular + // call, which is valid (caller passes no variadic arguments). + var cif types.CallInterface + err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 1, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{types.SInt64TypeDescriptor}, + ) + if err != nil { + t.Errorf("unexpected error for nfixedargs==len(argTypes): %v", err) + } + if cif.FixedArgCount != 1 { + t.Errorf("cif.FixedArgCount = %d, want 1", cif.FixedArgCount) + } + }) + + t.Run("zero nfixedargs is allowed", func(t *testing.T) { + // nfixedargs == 0 is legal — all args are variadic. + var cif types.CallInterface + err := PrepareVariadicCallInterface( + &cif, + types.DefaultCall, + 0, + types.SInt64TypeDescriptor, + []*types.TypeDescriptor{types.SInt64TypeDescriptor}, + ) + if err != nil { + t.Errorf("unexpected error for nfixedargs=0: %v", err) + } + if cif.FixedArgCount != 0 { + t.Errorf("cif.FixedArgCount = %d, want 0", cif.FixedArgCount) + } + }) +} diff --git a/internal/arch/arm64/call_arm64.go b/internal/arch/arm64/call_arm64.go index 20f1436..9bf0da6 100644 --- a/internal/arch/arm64/call_arm64.go +++ b/internal/arch/arm64/call_arm64.go @@ -208,6 +208,17 @@ func (i *Implementation) Execute( break } + // Apple ARM64 ABI extension for variadic functions: + // At the fixed/variadic boundary, exhaust both register allocators so + // that every variadic argument is placed on the stack, even when GP or + // FP registers are still available. This matches the behaviour of + // Apple's clang and libffi's ffi_prep_cif_var() on Darwin ARM64. + // Non-variadic CIFs (FixedArgCount == 0) skip this branch entirely. + if cif.FixedArgCount > 0 && runtime.GOOS == "darwin" && idx == cif.FixedArgCount { + gprIdx = 8 // exhaust GP registers (X0-X7) + fprIdx = 8 // exhaust FP registers (D0-D7) + } + switch argType.Kind { case types.FloatType: // Use math.Float32bits to preserve exact 32-bit IEEE-754 pattern. diff --git a/internal/runtime/syscall_linux.go b/internal/runtime/syscall_linux.go index c0a31fc..d46693d 100644 --- a/internal/runtime/syscall_linux.go +++ b/internal/runtime/syscall_linux.go @@ -57,8 +57,8 @@ func SyscallN(fn uintptr, args ...uintptr) (r1 uintptr, err error) { // syscallStub will be implemented in assembly // -//nolint:unused // Called from assembly (syscall_linux_amd64.s) -func syscallStub(args unsafe.Pointer) uint64 +//nolint:unused // Called from assembly (syscall_linux_stub.s) — returns via args.r1, not Go return +func syscallStub(args unsafe.Pointer) // Get address of syscallStub // diff --git a/internal/runtime/syscall_linux_stub.s b/internal/runtime/syscall_linux_stub.s index 808f667..bbc4deb 100644 --- a/internal/runtime/syscall_linux_stub.s +++ b/internal/runtime/syscall_linux_stub.s @@ -5,7 +5,7 @@ // syscallStub is called by runtime.asmcgocall on g0 stack // It receives pointer to callArgs struct in DI register // -// func syscallStub(args unsafe.Pointer) uint64 +// func syscallStub(args unsafe.Pointer) TEXT ·syscallStub(SB), NOSPLIT|NOFRAME, $0 // DI contains pointer to callArgs struct // Load function pointer diff --git a/types/types.go b/types/types.go index 21d2b58..12ae09a 100644 --- a/types/types.go +++ b/types/types.go @@ -96,14 +96,15 @@ var ( PointerTypeDescriptor = &TypeDescriptor{Size: 8, Alignment: 8, Kind: PointerType} ) -// CallInterface represents a prepared function call interface +// CallInterface represents a prepared function call interface. type CallInterface struct { - Convention CallingConvention - ArgCount int - ArgTypes []*TypeDescriptor - ReturnType *TypeDescriptor - Flags int // Return flags - StackBytes uintptr // Required stack space + Convention CallingConvention + ArgCount int + ArgTypes []*TypeDescriptor + ReturnType *TypeDescriptor + Flags int // Return flags. + StackBytes uintptr // Required stack space. + FixedArgCount int // 0 = non-variadic; >0 = number of fixed args before '...' } // Return flags constants