diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adb724..6a8ebae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2026-07-12 + +### Added +- **errno capture in assembly trampoline** — `CallFunction` now always captures C `errno` inside the assembly trampoline immediately after the C function returns, before the Go runtime can migrate the goroutine to a different OS thread. This is the only thread-safe window for errno capture. goffi is the first pure-Go FFI with correct errno capture on Linux. ([#60](https://github.com/go-webgpu/goffi/issues/60)) +- Platform-specific errno resolution: `__errno_location` (Linux glibc/musl), `__error` (macOS/FreeBSD) via `//go:cgo_import_dynamic` + +### Changed +- **BREAKING: `CallFunction` returns `(syscall.Errno, error)`** — errno is always captured and returned. Callers that don't need errno use `_, err := ffi.CallFunction(...)`. This replaces the opt-in `CallFunctionErrno` which was a pit of failure (you don't know you need errno until the function fails) +- **BREAKING: `CallFunctionContext` returns `(syscall.Errno, error)`** — same change with context support +- **BREAKING: `FunctionCaller.Execute` interface changed** — now accepts `errnoFn uintptr` parameter and returns `(cerrno uintptr, err error)` +- Removed `CallFunctionErrno` / `CallFunctionErrnoContext` (superseded by always-capture) +- Removed `FunctionCallerErrno` interface (merged into `FunctionCaller`) +- Reduced code by 429 lines (eliminated Execute/ExecuteErrno duplication) + +### Migration Guide + +```go +// Before (v0.5.x): +err := ffi.CallFunction(cif, fn, &result, args) + +// After (v0.6.0): +errno, err := ffi.CallFunction(cif, fn, &result, args) +// Or if errno not needed: +_, err := ffi.CallFunction(cif, fn, &result, args) +``` + ## [0.5.6] - 2026-07-05 ### Fixed diff --git a/README.md b/README.md index 850456b..0398abf 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ sym, _ := ffi.GetSymbol(handle, "wgpuCreateInstance") cif := &types.CallInterface{} ffi.PrepareCallInterface(cif, types.DefaultCall, returnType, argTypes) -ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args) +_, _ = ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args) ``` --- @@ -37,6 +37,7 @@ ffi.CallFunction(cif, sym, unsafe.Pointer(&result), args) | **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 | +| **errno** | Always captured | Thread-safe assembly-level capture — first pure-Go FFI on Linux | | **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) | @@ -115,7 +116,7 @@ func main() { strPtr := uintptr(unsafe.Pointer(unsafe.StringData(testStr))) var length uint64 - err = ffi.CallFunction(cif, strlen, unsafe.Pointer(&length), []unsafe.Pointer{unsafe.Pointer(&strPtr)}) + _, err = ffi.CallFunction(cif, strlen, unsafe.Pointer(&length), []unsafe.Pointer{unsafe.Pointer(&strPtr)}) if err != nil { panic(err) } @@ -153,7 +154,7 @@ err := ffi.PrepareVariadicCallInterface( count := int64(3) a1, a2, a3 := int64(10), int64(20), int64(30) var result int64 -ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ +_, _ = ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ unsafe.Pointer(&count), unsafe.Pointer(&a1), unsafe.Pointer(&a2), @@ -228,7 +229,7 @@ cb := ffi.NewCallback(func(status uint32, adapter uintptr, msg uintptr, ud uintp close(done) }) -ffi.CallFunction(cif, wgpuRequestAdapter, nil, args) +_, _ = ffi.CallFunction(cif, wgpuRequestAdapter, nil, args) <-done // Wait for GPU driver callback ``` diff --git a/ROADMAP.md b/ROADMAP.md index 2797391..6cf9faa 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-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 +**Last Updated**: 2026-07-12 | **Current Version**: v0.6.0 | **Strategy**: Benchmarks → Callbacks → ARM64 → Runtime → ABI → v1.0 LTS | **Milestone**: v0.6.0 (errno + stack-move fix) → v0.7.0 RegisterFunc/Builder → v1.0.0 LTS --- @@ -141,11 +141,27 @@ v1.0.0 LTS → Long-term support release (2027 Q1) **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) +**v0.5.3** = FreeBSD ARM64 ✅ RELEASED (2026-05-28) +- Build tag fix for FreeBSD ARM64 (8 platforms total) + +**v0.5.4** = structs.HostLayout ✅ RELEASED (2026-06-15) +- ABI-safe struct layout for all assembly-interface structures + +**v0.5.5** = Example fix + CI ✅ RELEASED (2026-06-15) +- Example avalue pointer bug fix, CI examples build verification + +**v0.5.6** = Callback stack-move fix ✅ RELEASED (2026-07-05) +- Critical: `syscallArgs` moved to sync.Pool (goroutine stack-move safety) +- Discovered by @tie — `TestCallbackGrowStack` reproducer + +**v0.6.0** = errno always-capture ✅ RELEASED (2026-07-12) +- **BREAKING**: `CallFunction` returns `(syscall.Errno, error)` — always captures C errno +- First pure-Go FFI with correct errno capture on Linux +- Assembly-level capture inside trampoline (thread-safe window) +- Platform support: `__errno_location` (Linux), `__error` (macOS/FreeBSD) + +**v0.7.0** = RegisterFunc + Builder API (2026 Q3-Q4) - RegisterFunc convenience API (ADR-008) - Library struct + OpenLibraryBytes (ADR-009) - NewFunc/Call/CallCtx ergonomic wrappers (ADR-009) @@ -158,9 +174,9 @@ v1.0.0 LTS → Long-term support release (2027 Q1) --- -## 📊 Current Status (v0.5.2) +## 📊 Current Status (v0.6.0) -**Phase**: Variadic functions supported, go vet clean, planning v0.6.0 (RegisterFunc) +**Phase**: errno always-capture, stack-move fix, 8 platforms. Planning v0.7.0 (RegisterFunc) **What Works**: - ✅ Dynamic library loading (`LoadLibrary`, `GetSymbol`, `FreeLibrary`) diff --git a/cmd/variadic-test/main.go b/cmd/variadic-test/main.go index e47e828..c06b00c 100644 --- a/cmd/variadic-test/main.go +++ b/cmd/variadic-test/main.go @@ -135,7 +135,7 @@ func testSumVariadic(lib unsafe.Pointer) bool { } var result int64 - if err := ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + 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 } @@ -188,7 +188,7 @@ func testTwoFixed(lib unsafe.Pointer) bool { } var result int64 - if err := ffi.CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + 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 } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ecdae33..a48bfdf 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,7 +37,8 @@ Every goffi call traverses four layers: ┌──────────────────────────────────────────────┐ │ Layer 3: Assembly Wrapper │ │ Load registers per ABI (GP + SSE/FP) │ -│ Call target function, save return values │ +│ Call target function, capture errno, │ +│ save return values │ └──────────────────┬───────────────────────────┘ │ ▼ @@ -74,6 +75,8 @@ ffi.PrepareCallInterface(cif, types.DefaultCall, 3. Calls our assembly wrapper Since v0.5.6, `syscallArgs` is heap-allocated via `sync.Pool` — goroutine stacks may move during C→Go callbacks (`copystack`), and assembly on g0 holds the args pointer across the call. All ABI-boundary structs use `structs.HostLayout` (Go 1.23+) to guarantee C-compatible memory layout. + +Since v0.6.0, `CallFunction` always captures C `errno` inside the assembly trampoline — the only thread-safe window (before `exitsyscall` can migrate the goroutine). Returns `(syscall.Errno, error)`. Uses `__errno_location` (Linux) / `__error` (macOS/FreeBSD) resolved via `//go:cgo_import_dynamic`. 4. Restores Go stack on return We access it via `//go:linkname`: diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index ad773bf..376e133 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -38,6 +38,7 @@ - **Typical overhead**: ~100-115 ns (with arguments) - **Overhead ratio**: ~400-500x vs direct Go call - **Allocations**: 0 in steady state. `syscallArgs` is heap-allocated via `sync.Pool` for callback safety (goroutine stack may move during C→Go callbacks). Pool reuse eliminates per-call allocations after warmup. +- **errno capture**: +3-5 ns per call (always-on since v0.6.0). `CALL __errno_location` + `MOVL (AX), EAX` in assembly — captures errno before thread migration can lose it. ### 2. One-Time Costs diff --git a/examples/simple/main.go b/examples/simple/main.go index b6097b1..28f92dc 100644 --- a/examples/simple/main.go +++ b/examples/simple/main.go @@ -68,7 +68,7 @@ func main() { args := []unsafe.Pointer{unsafe.Pointer(&cstr)} var ret int32 - if err := ffi.CallFunction(cif, sym, unsafe.Pointer(&ret), args); err != nil { + if _, err := ffi.CallFunction(cif, sym, unsafe.Pointer(&ret), args); err != nil { fmt.Println("CallFunction error:", err) return } @@ -80,7 +80,7 @@ func main() { args := []unsafe.Pointer{unsafe.Pointer(&cstr)} // Execute function call - err = ffi.CallFunction(cif, sym, nil, args) + _, err = ffi.CallFunction(cif, sym, nil, args) if err != nil { fmt.Println("CallFunction error:", err) } diff --git a/ffi/benchmark_test.go b/ffi/benchmark_test.go index 743f78f..c25fddf 100644 --- a/ffi/benchmark_test.go +++ b/ffi/benchmark_test.go @@ -49,7 +49,7 @@ func BenchmarkGoffiOverhead(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = CallFunction(cif, sym, unsafe.Pointer(&result), nil) + _, _ = CallFunction(cif, sym, unsafe.Pointer(&result), nil) } } @@ -96,7 +96,7 @@ func BenchmarkGoffiIntArgs(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ + _, _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ unsafe.Pointer(&arg), }) } @@ -146,7 +146,7 @@ func BenchmarkGoffiStringOutput(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{unsafe.Pointer(&strPtr)}) + _, _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{unsafe.Pointer(&strPtr)}) } } @@ -196,7 +196,7 @@ func BenchmarkGoffiMultipleArgs(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ + _, _ = CallFunction(cif, sym, unsafe.Pointer(&result), []unsafe.Pointer{ unsafe.Pointer(&arg1), unsafe.Pointer(&arg2), }) diff --git a/ffi/call.go b/ffi/call.go index 09546c0..b6736fe 100644 --- a/ffi/call.go +++ b/ffi/call.go @@ -4,18 +4,24 @@ import ( "unsafe" "github.com/go-webgpu/goffi/internal/arch" + gosyscall "github.com/go-webgpu/goffi/internal/syscall" "github.com/go-webgpu/goffi/types" ) -// executeFunction calls a function through architecture-dependent mechanism +// executeFunction calls a function through the architecture-dependent mechanism, +// always capturing C errno inside the assembly trampoline. func executeFunction( cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { +) (syscallErrno uintptr, err error) { if arch.Registry.Caller == nil { - return types.ErrUnsupportedArchitecture + return 0, types.ErrUnsupportedArchitecture } - return arch.Registry.Caller.Execute(cif, fn, rvalue, avalue) + // ErrnoFnAddr returns the address of __errno_location/__error on Unix and 0 + // on Windows. The assembly trampoline's conditional (TESTQ/CBZ) skips the + // errno capture when errnoFn is 0, so this is safe on all platforms. + errnoFn := gosyscall.ErrnoFnAddr() + return arch.Registry.Caller.Execute(cif, fn, rvalue, avalue, errnoFn) } diff --git a/ffi/callback_cthread_test.go b/ffi/callback_cthread_test.go index 56ebb09..6ef7069 100644 --- a/ffi/callback_cthread_test.go +++ b/ffi/callback_cthread_test.go @@ -136,7 +136,7 @@ func TestCallback_FromCThread(t *testing.T) { unsafe.Pointer(&argVal), } var rcCreate int32 - if err := CallFunction(cifCreate, create, unsafe.Pointer(&rcCreate), avalueCreate); err != nil { + if _, err := CallFunction(cifCreate, create, unsafe.Pointer(&rcCreate), avalueCreate); err != nil { t.Fatalf("CallFunction(pthread_create): %v", err) } if rcCreate != 0 { @@ -153,7 +153,7 @@ func TestCallback_FromCThread(t *testing.T) { unsafe.Pointer(&retvalAddr), } var rcJoin int32 - if err := CallFunction(cifJoin, join, unsafe.Pointer(&rcJoin), avalueJoin); err != nil { + if _, err := CallFunction(cifJoin, join, unsafe.Pointer(&rcJoin), avalueJoin); err != nil { t.Fatalf("CallFunction(pthread_join): %v", err) } if rcJoin != 0 { diff --git a/ffi/callback_test.go b/ffi/callback_test.go index 79367f2..d80904b 100644 --- a/ffi/callback_test.go +++ b/ffi/callback_test.go @@ -785,7 +785,7 @@ func TestCallbackGrowStack(t *testing.T) { // trips vet's unsafeptr guard against conversions that hide a heap address from // the GC, and the trampoline is code that is never GC-managed and never moved. fn := *(*unsafe.Pointer)(unsafe.Pointer(&cb)) - if err := CallFunction(&cif, fn, unsafe.Pointer(&ret), []unsafe.Pointer{unsafe.Pointer(&arg)}); err != nil { + if _, err := CallFunction(&cif, fn, unsafe.Pointer(&ret), []unsafe.Pointer{unsafe.Pointer(&arg)}); err != nil { t.Error(err) done <- ^uintptr(0) return diff --git a/ffi/coverage_test.go b/ffi/coverage_test.go index a68d07f..8c47da2 100644 --- a/ffi/coverage_test.go +++ b/ffi/coverage_test.go @@ -87,7 +87,7 @@ func TestCallFunctionContext(t *testing.T) { ctx := context.Background() // IMPORTANT: avalue contains pointers TO the argument values - err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) + _, err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) if err != nil { t.Errorf("CallFunctionContext failed: %v", err) } @@ -101,7 +101,7 @@ func TestCallFunctionContext(t *testing.T) { arg := unsafe.Pointer(unsafe.StringData(str)) var retVal int32 - err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) + _, err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) if err != context.Canceled { t.Errorf("Expected context.Canceled, got %v", err) } @@ -116,14 +116,14 @@ func TestCallFunctionContext(t *testing.T) { arg := unsafe.Pointer(unsafe.StringData(str)) var retVal int32 - err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) + _, err := CallFunctionContext(ctx, cif, sym, unsafe.Pointer(&retVal), []unsafe.Pointer{unsafe.Pointer(&arg)}) if err != context.DeadlineExceeded { t.Errorf("Expected context.DeadlineExceeded, got %v", err) } }) t.Run("NilCIF", func(t *testing.T) { - err := CallFunctionContext(context.Background(), nil, sym, nil, nil) + _, err := CallFunctionContext(context.Background(), nil, sym, nil, nil) var icErr *InvalidCallInterfaceError if err == nil || err.(*InvalidCallInterfaceError).Field != "cif" { t.Errorf("Expected InvalidCallInterfaceError for cif, got %v", err) @@ -132,7 +132,7 @@ func TestCallFunctionContext(t *testing.T) { }) t.Run("NilFunction", func(t *testing.T) { - err := CallFunctionContext(context.Background(), cif, nil, nil, nil) + _, err := CallFunctionContext(context.Background(), cif, nil, nil, nil) if err == nil || err.(*InvalidCallInterfaceError).Field != "fn" { t.Errorf("Expected InvalidCallInterfaceError for fn, got %v", err) } diff --git a/ffi/darwin_objc_test.go b/ffi/darwin_objc_test.go index 2406617..7d8f603 100644 --- a/ffi/darwin_objc_test.go +++ b/ffi/darwin_objc_test.go @@ -149,7 +149,7 @@ func (rt *objcRuntime) getClass(t *testing.T, name string) uintptr { namePtr := unsafe.Pointer(&cname[0]) var result uintptr - err := CallFunction( + _, err := CallFunction( &rt.cifCStringToPtr, rt.objcGetClass, unsafe.Pointer(&result), @@ -172,7 +172,7 @@ func (rt *objcRuntime) sel(t *testing.T, name string) uintptr { namePtr := unsafe.Pointer(&cname[0]) var result uintptr - err := CallFunction( + _, err := CallFunction( &rt.cifCStringToPtr, rt.selRegisterName, unsafe.Pointer(&result), @@ -254,7 +254,7 @@ func objcCall(t *testing.T, rt *objcRuntime, retType *types.TypeDescriptor, rval argPtrs = append(argPtrs, arg.ptr) } - if err := CallFunction(cif, rt.objcMsgSend, rvalue, argPtrs); err != nil { + if _, err := CallFunction(cif, rt.objcMsgSend, rvalue, argPtrs); err != nil { t.Fatalf("objc_msgSend failed: %v", err) } runtime.KeepAlive(args) @@ -606,7 +606,7 @@ func TestDarwinCoreGraphicsStructs(t *testing.T) { t.Fatalf("PrepareCallInterface(CGMainDisplayID) failed: %v", err) } var displayID uint32 - err = CallFunction(displayIDCIF, mainDisplayID, unsafe.Pointer(&displayID), nil) + _, err = CallFunction(displayIDCIF, mainDisplayID, unsafe.Pointer(&displayID), nil) if err != nil { t.Fatalf("CGMainDisplayID call failed: %v", err) } @@ -623,7 +623,7 @@ func TestDarwinCoreGraphicsStructs(t *testing.T) { } var bounds nsRect - err = CallFunction(boundsCIF, displayBounds, unsafe.Pointer(&bounds), []unsafe.Pointer{ + _, err = CallFunction(boundsCIF, displayBounds, unsafe.Pointer(&bounds), []unsafe.Pointer{ unsafe.Pointer(&displayID), }) if err != nil { @@ -648,7 +648,7 @@ func TestDarwinCoreGraphicsStructs(t *testing.T) { } var transform uintptr var path uintptr - err = CallFunction(pathCIF, pathCreateRect, unsafe.Pointer(&path), []unsafe.Pointer{ + _, err = CallFunction(pathCIF, pathCreateRect, unsafe.Pointer(&path), []unsafe.Pointer{ unsafe.Pointer(&rect), unsafe.Pointer(&transform), }) @@ -666,7 +666,7 @@ func TestDarwinCoreGraphicsStructs(t *testing.T) { if err != nil { t.Fatalf("PrepareCallInterface(CGPathRelease) failed: %v", err) } - err = CallFunction(releaseCIF, pathRelease, nil, []unsafe.Pointer{ + _, err = CallFunction(releaseCIF, pathRelease, nil, []unsafe.Pointer{ unsafe.Pointer(&path), }) if err != nil { @@ -699,7 +699,7 @@ func TestDarwinCAMetalLayerProperties(t *testing.T) { t.Fatalf("PrepareCallInterface(MTLCreateSystemDefaultDevice) failed: %v", err) } var device uintptr - if err := CallFunction(cifDevice, createDevice, unsafe.Pointer(&device), nil); err != nil { + if _, err := CallFunction(cifDevice, createDevice, unsafe.Pointer(&device), nil); err != nil { t.Fatalf("MTLCreateSystemDefaultDevice call failed: %v", err) } if device == 0 { diff --git a/ffi/errno_test.go b/ffi/errno_test.go new file mode 100644 index 0000000..17272f2 --- /dev/null +++ b/ffi/errno_test.go @@ -0,0 +1,199 @@ +//go:build !windows + +package ffi + +import ( + "runtime" + "syscall" + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +// TestCallFunctionCapturesErrno verifies that CallFunction captures a +// non-zero errno when a POSIX function fails. +// +// Strategy: call open(2) with a path that does not exist. POSIX mandates that +// open returns -1 and sets errno = ENOENT in this case. +func TestCallFunctionCapturesErrno(t *testing.T) { + var libName string + switch runtime.GOOS { + case "linux": + libName = "libc.so.6" + case "darwin": + libName = "libSystem.B.dylib" + case "freebsd": + libName = "libc.so.7" + default: + t.Skipf("errno capture not tested on %s", runtime.GOOS) + } + + handle, err := LoadLibrary(libName) + if err != nil { + t.Fatalf("LoadLibrary(%s) failed: %v", libName, err) + } + defer FreeLibrary(handle) + + openFn, err := GetSymbol(handle, "open") + if err != nil { + t.Fatalf("GetSymbol(open) failed: %v", err) + } + + // Prepare CIF: int open(const char *pathname, int flags) + cif := &types.CallInterface{} + err = PrepareCallInterface(cif, types.UnixCallingConvention, + types.SInt32TypeDescriptor, + []*types.TypeDescriptor{types.PointerTypeDescriptor, types.SInt32TypeDescriptor}, + ) + if err != nil { + t.Fatalf("PrepareCallInterface failed: %v", err) + } + + // Call open("/goffi_test_nonexistent_path_abc\x00", 0) — must return -1 / ENOENT. + path := "/goffi_test_nonexistent_path_abc\x00" + pathPtr := unsafe.Pointer(unsafe.StringData(path)) + flags := int32(0) // O_RDONLY + + var result int32 + cerrno, err := CallFunction(cif, openFn, + unsafe.Pointer(&result), + []unsafe.Pointer{unsafe.Pointer(&pathPtr), unsafe.Pointer(&flags)}, + ) + if err != nil { + t.Fatalf("CallFunction failed: %v", err) + } + + if result != -1 { + t.Fatalf("expected open() to return -1, got %d", result) + } + if cerrno != syscall.ENOENT { + t.Errorf("expected errno=ENOENT(%d), got %d (%v)", + syscall.ENOENT, cerrno, cerrno) + } +} + +// TestCallFunctionZeroErrnoOnSuccess verifies that errno is 0 after a successful call. +// We use strlen(3) which always succeeds and does not set errno. +func TestCallFunctionZeroErrnoOnSuccess(t *testing.T) { + var libName string + switch runtime.GOOS { + case "linux": + libName = "libc.so.6" + case "darwin": + libName = "libSystem.B.dylib" + case "freebsd": + libName = "libc.so.7" + default: + t.Skipf("errno capture not tested on %s", runtime.GOOS) + } + + handle, err := LoadLibrary(libName) + if err != nil { + t.Fatalf("LoadLibrary(%s) failed: %v", libName, err) + } + defer FreeLibrary(handle) + + strlenFn, err := GetSymbol(handle, "strlen") + if err != nil { + t.Fatalf("GetSymbol(strlen) failed: %v", err) + } + + cif := &types.CallInterface{} + err = PrepareCallInterface(cif, types.UnixCallingConvention, + types.UInt64TypeDescriptor, + []*types.TypeDescriptor{types.PointerTypeDescriptor}, + ) + if err != nil { + t.Fatalf("PrepareCallInterface failed: %v", err) + } + + input := "hello\x00" + ptr := unsafe.Pointer(unsafe.StringData(input)) + + var result uint64 + cerrno, err := CallFunction(cif, strlenFn, + unsafe.Pointer(&result), + []unsafe.Pointer{unsafe.Pointer(&ptr)}, + ) + if err != nil { + t.Fatalf("CallFunction failed: %v", err) + } + if result != 5 { + t.Errorf("strlen returned %d, want 5", result) + } + // errno is not guaranteed to be 0 after a successful call (per POSIX), + // but for strlen it should be. We log rather than fail hard. + if cerrno != 0 { + t.Logf("note: errno=%d after successful strlen (unexpected but not fatal)", cerrno) + } +} + +// TestCallFunctionNilCIF verifies that a nil CIF returns an error. +func TestCallFunctionNilCIF(t *testing.T) { + _, err := CallFunction(nil, nil, nil, nil) + if err == nil { + t.Error("expected error for nil CIF, got nil") + } +} + +// TestCallFunctionNilFn verifies that a nil function pointer returns an error. +func TestCallFunctionNilFn(t *testing.T) { + cif := &types.CallInterface{} + prepErr := PrepareCallInterface(cif, types.UnixCallingConvention, + types.VoidTypeDescriptor, nil) + if prepErr != nil { + t.Fatalf("PrepareCallInterface failed: %v", prepErr) + } + _, err := CallFunction(cif, nil, nil, nil) + if err == nil { + t.Error("expected error for nil fn, got nil") + } +} + +// BenchmarkCallFunctionErrnoOverhead measures the errno capture overhead inside +// CallFunction relative to a baseline by calling strlen on a short string. +func BenchmarkCallFunctionErrnoOverhead(b *testing.B) { + var libName string + switch runtime.GOOS { + case "linux": + libName = "libc.so.6" + case "darwin": + libName = "libSystem.B.dylib" + case "freebsd": + libName = "libc.so.7" + default: + b.Skipf("errno capture not benchmarked on %s", runtime.GOOS) + } + + handle, err := LoadLibrary(libName) + if err != nil { + b.Fatalf("LoadLibrary failed: %v", err) + } + defer FreeLibrary(handle) + + strlenFn, err := GetSymbol(handle, "strlen") + if err != nil { + b.Fatalf("GetSymbol(strlen) failed: %v", err) + } + + cif := &types.CallInterface{} + if err = PrepareCallInterface(cif, types.UnixCallingConvention, + types.UInt64TypeDescriptor, + []*types.TypeDescriptor{types.PointerTypeDescriptor}, + ); err != nil { + b.Fatalf("PrepareCallInterface failed: %v", err) + } + + input := "benchmark\x00" + ptr := unsafe.Pointer(unsafe.StringData(input)) + + var result uint64 + b.ResetTimer() + for b.Loop() { + _, _ = CallFunction(cif, strlenFn, + unsafe.Pointer(&result), + []unsafe.Pointer{unsafe.Pointer(&ptr)}, + ) + } +} diff --git a/ffi/ffi.go b/ffi/ffi.go index de488e1..33f6ab3 100644 --- a/ffi/ffi.go +++ b/ffi/ffi.go @@ -89,6 +89,7 @@ package ffi import ( "context" "errors" + "syscall" "unsafe" "github.com/go-webgpu/goffi/types" @@ -206,6 +207,11 @@ func PrepareVariadicCallInterface( // context before executing to prevent starting expensive operations when the // context is already cancelled or has exceeded its deadline. // +// errno is captured inside the assembly trampoline immediately after the C +// function returns, before the Go runtime can migrate the goroutine to a +// different OS thread. On Windows, errno is always 0 (use syscall.GetLastError() +// for Win32 error codes). +// // Parameters: // - ctx: Context for cancellation and timeout control (use context.Background() if not needed) // - cif: Prepared call interface (from PrepareCallInterface) @@ -214,10 +220,8 @@ func PrepareVariadicCallInterface( // - avalue: Slice of pointers to argument values (length must match argCount from PrepareCallInterface) // // Returns: -// - nil on success -// - ctx.Err() if context is cancelled or deadline exceeded before call starts -// - ErrInvalidCallInterface if cif or fn is nil -// - ErrFunctionCallFailed if the call execution fails +// - errno: C errno captured after the call (0 on success or Windows) +// - err: nil on success; ctx.Err() if context cancelled; ErrInvalidCallInterface if cif or fn is nil // // Example: // @@ -227,7 +231,7 @@ func PrepareVariadicCallInterface( // // var result float64 // arg := 16.0 -// err := ffi.CallFunctionContext( +// errno, err := ffi.CallFunctionContext( // ctx, // &cif, // sqrtPtr, @@ -255,28 +259,29 @@ func CallFunctionContext( fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { +) (syscall.Errno, error) { // Check context before expensive call - if err := ctx.Err(); err != nil { - return err + if ctxErr := ctx.Err(); ctxErr != nil { + return 0, ctxErr } if cif == nil { - return &InvalidCallInterfaceError{ + return 0, &InvalidCallInterfaceError{ Field: "cif", Reason: "must not be nil", Index: -1, } } if fn == nil { - return &InvalidCallInterfaceError{ + return 0, &InvalidCallInterfaceError{ Field: "fn", Reason: "function pointer must not be nil", Index: -1, } } - return executeFunction(cif, fn, rvalue, avalue) + cerrno, err := executeFunction(cif, fn, rvalue, avalue) + return syscall.Errno(cerrno), err } // CallFunction executes a C function call without context support. @@ -284,6 +289,10 @@ func CallFunctionContext( // This is equivalent to CallFunctionContext(context.Background(), cif, fn, rvalue, avalue). // For operations that need cancellation or timeout control, use CallFunctionContext instead. // +// errno is captured inside the assembly trampoline immediately after the C +// function returns, before the Go runtime can migrate the goroutine to a +// different OS thread. On Windows, errno is always 0. +// // Parameters: // - cif: Prepared call interface (from PrepareCallInterface) // - fn: Function pointer obtained from GetSymbol (must not be nil) @@ -291,21 +300,24 @@ func CallFunctionContext( // - avalue: Slice of pointers to argument values (length must match argCount from PrepareCallInterface) // // Returns: -// - nil on success -// - ErrInvalidCallInterface if cif or fn is nil -// - ErrFunctionCallFailed if the call execution fails +// - errno: C errno captured after the call (0 on success or Windows) +// - err: nil on success; ErrInvalidCallInterface if cif or fn is nil // // Example: // -// // Calling strlen(const char *str) -// var result uintptr -// str := "Hello" -// err := ffi.CallFunction( +// // Calling open(2) and checking errno on failure: +// errno, err := ffi.CallFunction( // &cif, -// strlenPtr, +// openFn, // unsafe.Pointer(&result), -// []unsafe.Pointer{unsafe.Pointer(&str)}, +// []unsafe.Pointer{unsafe.Pointer(&pathPtr), unsafe.Pointer(&flags)}, // ) +// if result == -1 { +// log.Printf("open failed: %v", errno) +// } +// +// // When errno is not needed: +// _, err := ffi.CallFunction(&cif, strlenFn, unsafe.Pointer(&result), avalue) // // For context-aware calls with timeout support, see CallFunctionContext. func CallFunction( @@ -313,6 +325,6 @@ func CallFunction( fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { +) (syscall.Errno, error) { return CallFunctionContext(context.Background(), cif, fn, rvalue, avalue) } diff --git a/ffi/ffi_test.go b/ffi/ffi_test.go index fb84ef2..cd92f37 100644 --- a/ffi/ffi_test.go +++ b/ffi/ffi_test.go @@ -106,7 +106,7 @@ func TestCallPrintf(t *testing.T) { avalue := []unsafe.Pointer{unsafe.Pointer(&arg)} var retVal int32 - err = CallFunction(cif, sym, unsafe.Pointer(&retVal), avalue) + _, err = CallFunction(cif, sym, unsafe.Pointer(&retVal), avalue) if err != nil { t.Fatalf("CallFunction failed: %v", err) } @@ -200,7 +200,7 @@ func TestPointerArgumentPassing(t *testing.T) { avalue := []unsafe.Pointer{unsafe.Pointer(&ptr)} var result uint64 - err := CallFunction(cif, sym, unsafe.Pointer(&result), avalue) + _, err := CallFunction(cif, sym, unsafe.Pointer(&result), avalue) if err != nil { t.Fatalf("CallFunction failed: %v", err) } @@ -276,7 +276,7 @@ func TestIntegerArgumentTypes(t *testing.T) { avalue := []unsafe.Pointer{unsafe.Pointer(&arg)} var result int32 - err := CallFunction(cif, sym, unsafe.Pointer(&result), avalue) + _, err := CallFunction(cif, sym, unsafe.Pointer(&result), avalue) if err != nil { t.Fatalf("CallFunction failed: %v", err) } @@ -371,7 +371,7 @@ func TestWindowsStackArguments(t *testing.T) { } var result uintptr - err = CallFunction(cif, sym, unsafe.Pointer(&result), avalue) + _, err = CallFunction(cif, sym, unsafe.Pointer(&result), avalue) if err != nil { t.Fatalf("CallFunction failed: %v", err) } @@ -470,7 +470,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { arg7 := uintptr(0) var fileHandle uintptr - err = CallFunction(cifCreate, createFileA, unsafe.Pointer(&fileHandle), []unsafe.Pointer{ + _, err = CallFunction(cifCreate, createFileA, unsafe.Pointer(&fileHandle), []unsafe.Pointer{ unsafe.Pointer(&arg1), unsafe.Pointer(&arg2), unsafe.Pointer(&arg3), @@ -512,7 +512,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { wArg5 := uintptr(0) // lpOverlapped - STACK ARGUMENT! var writeResult int32 - err = CallFunction(cifWrite, writeFile, unsafe.Pointer(&writeResult), []unsafe.Pointer{ + _, err = CallFunction(cifWrite, writeFile, unsafe.Pointer(&writeResult), []unsafe.Pointer{ unsafe.Pointer(&wArg1), unsafe.Pointer(&wArg2), unsafe.Pointer(&wArg3), @@ -544,7 +544,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { cArg1 := fileHandle var closeResult int32 - err = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ + _, err = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ unsafe.Pointer(&cArg1), }) if err != nil { @@ -557,7 +557,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { arg2 = uint32(GENERIC_READ) arg5 = uint32(OPEN_EXISTING) - err = CallFunction(cifCreate, createFileA, unsafe.Pointer(&fileHandle), []unsafe.Pointer{ + _, err = CallFunction(cifCreate, createFileA, unsafe.Pointer(&fileHandle), []unsafe.Pointer{ unsafe.Pointer(&arg1), unsafe.Pointer(&arg2), unsafe.Pointer(&arg3), @@ -598,7 +598,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { rArg5 := uintptr(0) // lpOverlapped - STACK ARGUMENT! var readResult int32 - err = CallFunction(cifRead, readFile, unsafe.Pointer(&readResult), []unsafe.Pointer{ + _, err = CallFunction(cifRead, readFile, unsafe.Pointer(&readResult), []unsafe.Pointer{ unsafe.Pointer(&rArg1), unsafe.Pointer(&rArg2), unsafe.Pointer(&rArg3), @@ -627,7 +627,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { t.Log("Step 7: Cleanup") cArg1 = fileHandle - err = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ + _, err = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ unsafe.Pointer(&cArg1), }) if err != nil { @@ -641,7 +641,7 @@ func TestWindowsStackArgumentsFileIO(t *testing.T) { if err == nil { dArg1 := tempFilePtr var deleteResult int32 - _ = CallFunction(cifDelete, deleteFileA, unsafe.Pointer(&deleteResult), []unsafe.Pointer{ + _, _ = CallFunction(cifDelete, deleteFileA, unsafe.Pointer(&deleteResult), []unsafe.Pointer{ unsafe.Pointer(&dArg1), }) } @@ -781,7 +781,7 @@ func TestWindowsStackArguments10Args(t *testing.T) { arg10 := unsafe.Pointer(&pi) // lpProcessInformation (STACK arg 10!) var createResult int32 - err = CallFunction(cifCreate, createProcessA, unsafe.Pointer(&createResult), []unsafe.Pointer{ + _, err = CallFunction(cifCreate, createProcessA, unsafe.Pointer(&createResult), []unsafe.Pointer{ unsafe.Pointer(&arg1), unsafe.Pointer(&arg2), unsafe.Pointer(&arg3), @@ -819,7 +819,7 @@ func TestWindowsStackArguments10Args(t *testing.T) { wArg1 := pi.hProcess wArg2 := uint32(INFINITE) var waitResult uint32 - err = CallFunction(cifWait, waitForSingleObject, unsafe.Pointer(&waitResult), []unsafe.Pointer{ + _, err = CallFunction(cifWait, waitForSingleObject, unsafe.Pointer(&waitResult), []unsafe.Pointer{ unsafe.Pointer(&wArg1), unsafe.Pointer(&wArg2), }) @@ -842,7 +842,7 @@ func TestWindowsStackArguments10Args(t *testing.T) { eArg1 := pi.hProcess eArg2 := unsafe.Pointer(&exitCode) var exitResult int32 - err = CallFunction(cifGetExit, getExitCodeProcess, unsafe.Pointer(&exitResult), []unsafe.Pointer{ + _, err = CallFunction(cifGetExit, getExitCodeProcess, unsafe.Pointer(&exitResult), []unsafe.Pointer{ unsafe.Pointer(&eArg1), unsafe.Pointer(&eArg2), }) @@ -860,12 +860,12 @@ func TestWindowsStackArguments10Args(t *testing.T) { cArg := pi.hProcess var closeResult int32 - _ = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ + _, _ = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ unsafe.Pointer(&cArg), }) cArg = pi.hThread - _ = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ + _, _ = CallFunction(cifClose, closeHandle, unsafe.Pointer(&closeResult), []unsafe.Pointer{ unsafe.Pointer(&cArg), }) @@ -954,7 +954,7 @@ func TestFloat32ArgEncoding(t *testing.T) { } var frac float32 - err := CallFunction(cif, sym, unsafe.Pointer(&frac), avalue) + _, err := CallFunction(cif, sym, unsafe.Pointer(&frac), avalue) if err != nil { t.Fatalf("CallFunction(modff) failed: %v", err) } @@ -1089,7 +1089,7 @@ func TestUnixStackSpill7Args(t *testing.T) { } var written int32 - err = CallFunction(cif, sym, unsafe.Pointer(&written), avalue) + _, err = CallFunction(cif, sym, unsafe.Pointer(&written), avalue) if err != nil { t.Fatalf("CallFunction(snprintf) failed: %v", err) } diff --git a/ffi/struct_e2e_test.go b/ffi/struct_e2e_test.go index 5fa885e..7dfa452 100644 --- a/ffi/struct_e2e_test.go +++ b/ffi/struct_e2e_test.go @@ -108,7 +108,7 @@ func TestStructArg8B_IntegerPair(t *testing.T) { s := Pair{A: 42, B: 19} args := []unsafe.Pointer{unsafe.Pointer(&s)} var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -153,7 +153,7 @@ func TestStructArg8B_FloatPair(t *testing.T) { s := PairF32{X: 2.5, Y: 3.5} args := []unsafe.Pointer{unsafe.Pointer(&s)} var result float32 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -194,7 +194,7 @@ func TestStructArg16B(t *testing.T) { s := PairI64{A: 1000000, B: 2000000} args := []unsafe.Pointer{unsafe.Pointer(&s)} var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -237,7 +237,7 @@ func TestStructArg24B_MemoryClass(t *testing.T) { s := TripleI64{A: 100, B: 200, C: 300} args := []unsafe.Pointer{unsafe.Pointer(&s)} var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -279,7 +279,7 @@ func TestStructArgWithScalar(t *testing.T) { extra := int64(1000) args := []unsafe.Pointer{unsafe.Pointer(&s), unsafe.Pointer(&extra)} var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -327,7 +327,7 @@ func TestCallbackStructArg8B_IntegerPair(t *testing.T) { unsafe.Pointer(&callback), } - if err := CallFunction(&cif, sym, nil, args); err != nil { + if _, err := CallFunction(&cif, sym, nil, args); err != nil { t.Fatal(err) } @@ -374,7 +374,7 @@ func TestCallbackStructArg8B_FloatPair(t *testing.T) { unsafe.Pointer(&callback), } - if err := CallFunction(&cif, sym, nil, args); err != nil { + if _, err := CallFunction(&cif, sym, nil, args); err != nil { t.Fatal(err) } @@ -421,7 +421,7 @@ func TestCallbackStructArg16B(t *testing.T) { unsafe.Pointer(&callback), } - if err := CallFunction(&cif, sym, nil, args); err != nil { + if _, err := CallFunction(&cif, sym, nil, args); err != nil { t.Fatal(err) } @@ -471,7 +471,7 @@ func TestCallbackStructArg24B_MemoryClass(t *testing.T) { unsafe.Pointer(&callback), } - if err := CallFunction(&cif, sym, nil, args); err != nil { + if _, err := CallFunction(&cif, sym, nil, args); err != nil { t.Fatal(err) } @@ -523,7 +523,7 @@ func TestCallbackStructArgWithScalar(t *testing.T) { unsafe.Pointer(&callback), } - if err := CallFunction(&cif, sym, nil, args); err != nil { + if _, err := CallFunction(&cif, sym, nil, args); err != nil { t.Fatal(err) } @@ -566,7 +566,7 @@ func TestStructReturn24B(t *testing.T) { type TripleI64 struct{ A, B, C int64 } var result TripleI64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), nil); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), nil); err != nil { t.Fatal(err) } diff --git a/ffi/struct_return_e2e_amd64_test.go b/ffi/struct_return_e2e_amd64_test.go index 41ed9e8..a3fb0ca 100644 --- a/ffi/struct_return_e2e_amd64_test.go +++ b/ffi/struct_return_e2e_amd64_test.go @@ -54,7 +54,7 @@ func TestStructReturn16B_TwoDoubles(t *testing.T) { 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 { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -107,7 +107,7 @@ func TestStructReturn16B_IntFloat(t *testing.T) { 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 { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -160,7 +160,7 @@ func TestStructReturn16B_FloatInt(t *testing.T) { 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 { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } @@ -210,7 +210,7 @@ func TestStructReturn16B_TwoInts(t *testing.T) { 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 { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), args); err != nil { t.Fatal(err) } diff --git a/ffi/variadic_e2e_test.go b/ffi/variadic_e2e_test.go index e930026..c1540f4 100644 --- a/ffi/variadic_e2e_test.go +++ b/ffi/variadic_e2e_test.go @@ -58,7 +58,7 @@ func TestVariadic_SumIntegers(t *testing.T) { } var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { t.Fatal(err) } @@ -109,7 +109,7 @@ func TestVariadic_TwoFixed(t *testing.T) { } var result int64 - if err := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { + if _, err := CallFunction(&cif, sym, unsafe.Pointer(&result), avalue); err != nil { t.Fatal(err) } diff --git a/internal/arch/amd64/call_unix.go b/internal/arch/amd64/call_unix.go index c9c1869..ab683f3 100644 --- a/internal/arch/amd64/call_unix.go +++ b/internal/arch/amd64/call_unix.go @@ -24,7 +24,8 @@ func (i *Implementation) Execute( fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { + errnoFn uintptr, +) (cerrno uintptr, err error) { // System V AMD64 ABI: // - GP registers: RDI, RSI, RDX, RCX, R8, R9 (6 registers, indices 0-5) // - SSE registers: XMM0-XMM7 (8 registers) @@ -196,7 +197,7 @@ func (i *Implementation) Execute( // Validate we haven't exceeded platform maximum if numStack > maxTotalArgs-6 { - return fmt.Errorf("goffi: %d stack arguments exceed platform limit of %d", numStack, maxTotalArgs-6) + return 0, fmt.Errorf("goffi: %d stack arguments exceed platform limit of %d", numStack, maxTotalArgs-6) } // Build GP register array (first 6 slots) @@ -213,15 +214,16 @@ func (i *Implementation) Execute( var stackArgs [9]uintptr copy(stackArgs[:], sysargs[6:]) - // Call via syscall - ret, r2, fret, fret2 := gosyscall.CallNFloat(uintptr(fn), gpr, sse, stackArgs, numStack) + // Call via syscall; errnoFn is non-zero on Unix, 0 on Windows. + // When errnoFn is 0, the assembly skips errno capture (TESTQ/JZ). + ret, r2, fret, fret2, capturedErrno := gosyscall.CallNFloatErrno(uintptr(fn), gpr, sse, stackArgs, numStack, errnoFn) runtime.KeepAlive(avalue) runtime.KeepAlive(rvalue) // If sret, the callee wrote directly into rvalue — no further copy needed. if sret { - return nil + return capturedErrno, nil } // Handle return value based on type @@ -232,5 +234,5 @@ func (i *Implementation) Execute( retVal = *(*uint64)(unsafe.Pointer(&fret)) } - return i.handleReturn(cif, rvalue, retVal, uint64(r2), fret, fret2) + return capturedErrno, i.handleReturn(cif, rvalue, retVal, uint64(r2), fret, fret2) } diff --git a/internal/arch/amd64/call_windows.go b/internal/arch/amd64/call_windows.go index 603e037..1951571 100644 --- a/internal/arch/amd64/call_windows.go +++ b/internal/arch/amd64/call_windows.go @@ -11,12 +11,15 @@ import ( "github.com/go-webgpu/goffi/types" ) +// Execute implements arch.FunctionCaller for Windows AMD64. +// errnoFn is always 0 on Windows (ErrnoFnAddr returns 0); cerrno is always 0. func (i *Implementation) Execute( cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { + errnoFn uintptr, +) (cerrno uintptr, err error) { // Win64 ABI: arguments are passed in numbered slots. // First 4 args: RCX, RDX, R8, R9 (integer) or XMM0-XMM3 (float). // Args 5+: on the stack. @@ -81,5 +84,8 @@ func (i *Implementation) Execute( // See: TASK-019, GAP-7. Workaround: use integer return type and reinterpret bits. // fret and fret2 are zero: Windows syscall.SyscallN does not capture XMM returns. // Float-returning functions on Windows require a custom assembly wrapper (known limitation). - return i.handleReturn(cif, rvalue, uint64(ret), 0, 0, 0) + // + // cerrno is always 0 on Windows: errno capture via __errno_location is not applicable. + // Windows Win32 errors use GetLastError(); CRT errno is rarely used for Win32 APIs. + return 0, i.handleReturn(cif, rvalue, uint64(ret), 0, 0, 0) } diff --git a/internal/arch/arm64/call_arm64.go b/internal/arch/arm64/call_arm64.go index 5a0a795..489c156 100644 --- a/internal/arch/arm64/call_arm64.go +++ b/internal/arch/arm64/call_arm64.go @@ -152,7 +152,8 @@ func (i *Implementation) Execute( fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { + errnoFn uintptr, +) (cerrno uintptr, err error) { // AAPCS64 ABI: // - X0-X7: 8 integer/pointer GP registers // - D0-D7: 8 floating-point registers @@ -307,14 +308,15 @@ func (i *Implementation) Execute( // Validate we haven't exceeded platform maximum if stackIdx > maxStackArgs { - return fmt.Errorf("goffi: %d stack arguments exceed platform limit of %d", stackIdx, maxStackArgs) + return 0, fmt.Errorf("goffi: %d stack arguments exceed platform limit of %d", stackIdx, maxStackArgs) } - // Call via our ARM64 syscall wrapper - ret1, ret2, fret := gosyscall.CallNFloat(uintptr(fn), gpr, fpr, stackArgs, stackIdx, r8) + // Call via our ARM64 syscall wrapper; errnoFn is non-zero on Unix, 0 on Windows. + // When errnoFn is 0, the assembly skips errno capture (CBZ). + ret1, ret2, fret, capturedErrno := gosyscall.CallNFloatErrno(uintptr(fn), gpr, fpr, stackArgs, stackIdx, r8, errnoFn) runtime.KeepAlive(avalue) // Handle return value based on type - return i.handleReturn(cif, rvalue, uint64(ret1), uint64(ret2), fret) + return capturedErrno, i.handleReturn(cif, rvalue, uint64(ret1), uint64(ret2), fret) } diff --git a/internal/arch/registry.go b/internal/arch/registry.go index 0651a94..82e803e 100644 --- a/internal/arch/registry.go +++ b/internal/arch/registry.go @@ -6,30 +6,33 @@ import ( "github.com/go-webgpu/goffi/types" ) -// FunctionCaller defines the contract for function execution +// FunctionCaller defines the contract for function execution. +// Execute always captures C errno inside the assembly trampoline immediately +// after the C function returns. On platforms without errno support (Windows), +// errnoFn is 0 and the returned cerrno is always 0. type FunctionCaller interface { - Execute(cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer) error + Execute(cif *types.CallInterface, fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, errnoFn uintptr) (cerrno uintptr, err error) } -// ArgumentClassifier defines the contract for argument classification +// ArgumentClassifier defines the contract for argument classification. type ArgumentClassifier interface { ClassifyReturn(t *types.TypeDescriptor, abi types.CallingConvention) int ClassifyArgument(t *types.TypeDescriptor, abi types.CallingConvention) ArgumentClassification } -// ArgumentClassification contains argument passing information +// ArgumentClassification contains argument passing information. type ArgumentClassification struct { GPRCount int SSECount int } -// Registry contains registered implementations +// Registry contains registered implementations. var Registry struct { Caller FunctionCaller Classifier ArgumentClassifier } -// Register registers implementations for the current architecture +// Register registers implementations for the current architecture. func Register(caller FunctionCaller, classifier ArgumentClassifier) { Registry.Caller = caller Registry.Classifier = classifier diff --git a/internal/arch/stubs/caller.go b/internal/arch/stubs/caller.go index 313acaa..b475166 100644 --- a/internal/arch/stubs/caller.go +++ b/internal/arch/stubs/caller.go @@ -20,8 +20,9 @@ func (c *unsupportedCaller) Execute( fn unsafe.Pointer, rvalue unsafe.Pointer, avalue []unsafe.Pointer, -) error { - return types.ErrUnsupportedArchitecture + errnoFn uintptr, +) (cerrno uintptr, err error) { + return 0, types.ErrUnsupportedArchitecture } func (c *unsupportedCaller) ClassifyReturn( diff --git a/internal/syscall/errno_darwin.go b/internal/syscall/errno_darwin.go new file mode 100644 index 0000000..a4e1cf9 --- /dev/null +++ b/internal/syscall/errno_darwin.go @@ -0,0 +1,9 @@ +//go:build darwin && (amd64 || arm64) + +package syscall + +// Link __error from libSystem.B.dylib (macOS equivalent of __errno_location). +// __error() returns a pointer to the per-thread errno variable. +// +//go:cgo_import_dynamic goffi_errno_location __error "/usr/lib/libSystem.B.dylib" +//go:cgo_import_dynamic _ _ "/usr/lib/libSystem.B.dylib" diff --git a/internal/syscall/errno_freebsd.go b/internal/syscall/errno_freebsd.go new file mode 100644 index 0000000..194b210 --- /dev/null +++ b/internal/syscall/errno_freebsd.go @@ -0,0 +1,9 @@ +//go:build freebsd && (amd64 || arm64) + +package syscall + +// Link __error from libc.so.7 (FreeBSD equivalent of __errno_location). +// __error() returns a pointer to the per-thread errno variable. +// +//go:cgo_import_dynamic goffi_errno_location __error "libc.so.7" +//go:cgo_import_dynamic _ _ "libc.so.7" diff --git a/internal/syscall/errno_linux.go b/internal/syscall/errno_linux.go new file mode 100644 index 0000000..87f969e --- /dev/null +++ b/internal/syscall/errno_linux.go @@ -0,0 +1,10 @@ +//go:build linux && (amd64 || arm64) + +package syscall + +// Link __errno_location from libc.so.6 (glibc and musl both export it). +// On glibc >= 2.34, libc.so.6 is the real library; libdl.so.2 is a stub. +// On musl, libc.so.6 is a symlink. Either way, __errno_location is available. +// +//go:cgo_import_dynamic goffi_errno_location __errno_location "libc.so.6" +//go:cgo_import_dynamic _ _ "libc.so.6" diff --git a/internal/syscall/errno_stubs_amd64.s b/internal/syscall/errno_stubs_amd64.s new file mode 100644 index 0000000..04c4632 --- /dev/null +++ b/internal/syscall/errno_stubs_amd64.s @@ -0,0 +1,10 @@ +//go:build (linux || darwin || freebsd) && amd64 + +#include "textflag.h" + +// goffi_errno_location_stub: JMP to the dynamically linked errno function. +// On Linux/FreeBSD: __errno_location (from libc.so.6 / libc.so.7) +// On macOS: __error (from libSystem.B.dylib) +// In all cases the dynamic symbol is imported as goffi_errno_location. +TEXT goffi_errno_location_stub(SB), NOSPLIT|NOFRAME, $0-0 + JMP goffi_errno_location(SB) diff --git a/internal/syscall/errno_stubs_arm64.s b/internal/syscall/errno_stubs_arm64.s new file mode 100644 index 0000000..4e4eacc --- /dev/null +++ b/internal/syscall/errno_stubs_arm64.s @@ -0,0 +1,10 @@ +//go:build (linux || darwin || freebsd) && arm64 + +#include "textflag.h" + +// goffi_errno_location_stub: B to the dynamically linked errno function. +// On Linux/FreeBSD: __errno_location (from libc.so.6 / libc.so.7) +// On macOS: __error (from libSystem.B.dylib) +// In all cases the dynamic symbol is imported as goffi_errno_location. +TEXT goffi_errno_location_stub(SB), NOSPLIT|NOFRAME, $0-0 + B goffi_errno_location(SB) diff --git a/internal/syscall/errno_unix.go b/internal/syscall/errno_unix.go new file mode 100644 index 0000000..cde7b61 --- /dev/null +++ b/internal/syscall/errno_unix.go @@ -0,0 +1,29 @@ +//go:build (linux || darwin || freebsd) && (amd64 || arm64) + +package syscall + +import "unsafe" + +// goffi_errno_location_stub is the JMP trampoline that forwards calls to the +// dynamically linked __errno_location (Linux/FreeBSD) or __error (macOS). +// The symbol is defined in errno_stubs_amd64.s / errno_stubs_arm64.s and +// jumps to the goffi_errno_location dynamic symbol imported via +// //go:cgo_import_dynamic in errno_linux.go / errno_darwin.go / errno_freebsd.go. +// +//go:linkname goffi_errno_location_stub goffi_errno_location_stub +var goffi_errno_location_stub byte + +// errnoFnABI0 holds the ABI0 address of the errno function stub, set at init time. +// unsafe.Pointer is required here: we are computing the address of an assembly +// trampoline (not a Go heap object) and storing it as a uintptr for later use +// as a C function pointer inside the assembly trampoline in syscallN. +var errnoFnABI0 = uintptr(unsafe.Pointer(&goffi_errno_location_stub)) //nolint:govet // unsafe.Pointer-to-uintptr is intentional: this is an assembly stub address, not a GC-managed pointer. + +// ErrnoFnAddr returns the address of the platform's errno-location function +// (__errno_location on Linux/FreeBSD, __error on macOS). This address is +// passed to CallNFloatErrno to enable in-trampoline errno capture. +// +// Returns 0 if errno capture is not supported on the current platform. +func ErrnoFnAddr() uintptr { + return errnoFnABI0 +} diff --git a/internal/syscall/errno_windows.go b/internal/syscall/errno_windows.go new file mode 100644 index 0000000..48408ef --- /dev/null +++ b/internal/syscall/errno_windows.go @@ -0,0 +1,12 @@ +//go:build windows + +// Package syscall provides low-level FFI syscall infrastructure. +package syscall + +// ErrnoFnAddr returns 0 on Windows because errno capture via the Unix +// __errno_location mechanism is not applicable. Windows uses GetLastError() +// for Win32 errors, which is already captured by syscall.SyscallN as the +// third return value. CRT errno is rarely used for Win32 APIs. +func ErrnoFnAddr() uintptr { + return 0 +} diff --git a/internal/syscall/syscall_arm64.go b/internal/syscall/syscall_arm64.go index c80cdcb..a20c2f6 100644 --- a/internal/syscall/syscall_arm64.go +++ b/internal/syscall/syscall_arm64.go @@ -32,6 +32,8 @@ var syscallArgsPool = sync.Pool{New: func() any { return new(syscallArgs) }} // r2: 200 (X1 integer return, 9-16 byte struct returns) // fr1-fr4: 208-232 (D0-D3 float returns for HFA) // r8: 240 (X8 - large struct return pointer) +// errno: 248 (captured C errno value; 0 if errnoFn == 0) +// errnoFn: 256 (address of __errno_location/__error; 0 = skip errno capture) // // 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). @@ -44,6 +46,8 @@ type syscallArgs struct { r1, r2 uintptr // X0-X1 integer returns (offsets 192-200) fr1, fr2, fr3, fr4 uintptr // D0-D3 float returns for HFA (offsets 208-232) r8 uintptr // X8 - large struct return pointer (offset 240) + errno uintptr // captured C errno (offset 248) + errnoFn uintptr // address of __errno_location/__error (offset 256) } // syscallN is implemented in syscall_unix_arm64.s @@ -114,3 +118,51 @@ func callNFloat(fn uintptr, gpr [8]uintptr, fpr [8]uint64, stackArgs [7]uintptr, fret[3] = uint64(args.fr4) return } + +// CallNFloatErrno is like CallNFloat but also captures the C errno value set +// by the called function. The errno function address (errnoFn) must be the +// address of __errno_location (Linux/FreeBSD) or __error (macOS), obtained +// from ErrnoFnAddr(). When errnoFn is 0, errno capture is skipped and the +// returned errno value is always 0. +// +// The errno is read inside the assembly trampoline immediately after the C +// function returns, before the Go runtime can migrate the goroutine to a +// different OS thread. This is the only safe window for errno capture. +func CallNFloatErrno(fn uintptr, gpr [8]uintptr, fpr [8]uint64, stackArgs [7]uintptr, numStack int, r8 uintptr, errnoFn uintptr) (r1 uintptr, r2 uintptr, fret [4]uint64, cerrno uintptr) { + args := syscallArgsPool.Get().(*syscallArgs) + defer syscallArgsPool.Put(args) + *args = syscallArgs{ + fn: fn, + a1: gpr[0], a2: gpr[1], a3: gpr[2], a4: gpr[3], + a5: gpr[4], a6: gpr[5], a7: gpr[6], a8: gpr[7], + a9: stackArgs[0], + a10: stackArgs[1], + a11: stackArgs[2], + a12: stackArgs[3], + a13: stackArgs[4], + a14: stackArgs[5], + a15: stackArgs[6], + f1: uintptr(fpr[0]), + f2: uintptr(fpr[1]), + f3: uintptr(fpr[2]), + f4: uintptr(fpr[3]), + f5: uintptr(fpr[4]), + f6: uintptr(fpr[5]), + f7: uintptr(fpr[6]), + f8: uintptr(fpr[7]), + r8: r8, + errnoFn: errnoFn, + } + _ = numStack + + runtime_cgocall(syscallNABI0, unsafe.Pointer(args)) + + r1 = args.r1 + r2 = args.r2 + fret[0] = uint64(args.fr1) + fret[1] = uint64(args.fr2) + fret[2] = uint64(args.fr3) + fret[3] = uint64(args.fr4) + cerrno = args.errno + return +} diff --git a/internal/syscall/syscall_arm64.s b/internal/syscall/syscall_arm64.s index 35e526d..3024670 100644 --- a/internal/syscall/syscall_arm64.s +++ b/internal/syscall/syscall_arm64.s @@ -9,37 +9,39 @@ // // syscallN takes a pointer to syscallArgs struct (offsets verified by check_arm64.go): // struct { -// fn uintptr // offset 0 -// a1 uintptr // offset 8 (X0) -// a2 uintptr // offset 16 (X1) -// a3 uintptr // offset 24 (X2) -// a4 uintptr // offset 32 (X3) -// a5 uintptr // offset 40 (X4) -// a6 uintptr // offset 48 (X5) -// a7 uintptr // offset 56 (X6) -// a8 uintptr // offset 64 (X7) -// a9 uintptr // offset 72 (stack[0]) -// a10 uintptr // offset 80 (stack[1]) -// a11 uintptr // offset 88 (stack[2]) -// a12 uintptr // offset 96 (stack[3]) -// a13 uintptr // offset 104 (stack[4]) -// a14 uintptr // offset 112 (stack[5]) -// a15 uintptr // offset 120 (stack[6]) -// f1 uintptr // offset 128 (D0 input) -// f2 uintptr // offset 136 (D1 input) -// f3 uintptr // offset 144 (D2 input) -// f4 uintptr // offset 152 (D3 input) -// f5 uintptr // offset 160 (D4 input) -// f6 uintptr // offset 168 (D5 input) -// f7 uintptr // offset 176 (D6 input) -// f8 uintptr // offset 184 (D7 input) -// r1 uintptr // offset 192 (return X0) -// r2 uintptr // offset 200 (return X1) -// fr1 uintptr // offset 208 (return D0 for HFA or float) -// fr2 uintptr // offset 216 (return D1 for HFA) -// fr3 uintptr // offset 224 (return D2 for HFA) -// fr4 uintptr // offset 232 (return D3 for HFA) -// r8 uintptr // offset 240 (X8 - large struct return pointer) +// fn uintptr // offset 0 +// a1 uintptr // offset 8 (X0) +// a2 uintptr // offset 16 (X1) +// a3 uintptr // offset 24 (X2) +// a4 uintptr // offset 32 (X3) +// a5 uintptr // offset 40 (X4) +// a6 uintptr // offset 48 (X5) +// a7 uintptr // offset 56 (X6) +// a8 uintptr // offset 64 (X7) +// a9 uintptr // offset 72 (stack[0]) +// a10 uintptr // offset 80 (stack[1]) +// a11 uintptr // offset 88 (stack[2]) +// a12 uintptr // offset 96 (stack[3]) +// a13 uintptr // offset 104 (stack[4]) +// a14 uintptr // offset 112 (stack[5]) +// a15 uintptr // offset 120 (stack[6]) +// f1 uintptr // offset 128 (D0 input) +// f2 uintptr // offset 136 (D1 input) +// f3 uintptr // offset 144 (D2 input) +// f4 uintptr // offset 152 (D3 input) +// f5 uintptr // offset 160 (D4 input) +// f6 uintptr // offset 168 (D5 input) +// f7 uintptr // offset 176 (D6 input) +// f8 uintptr // offset 184 (D7 input) +// r1 uintptr // offset 192 (return X0) +// r2 uintptr // offset 200 (return X1) +// fr1 uintptr // offset 208 (return D0 for HFA or float) +// fr2 uintptr // offset 216 (return D1 for HFA) +// fr3 uintptr // offset 224 (return D2 for HFA) +// fr4 uintptr // offset 232 (return D3 for HFA) +// r8 uintptr // offset 240 (X8 - large struct return pointer) +// errno uintptr // offset 248 (captured C errno; 0 if errnoFn == 0) +// errnoFn uintptr // offset 256 (address of __errno_location/__error; 0 = skip) // } // // Stack frame layout (total STACK_SIZE = 96 bytes, 16-byte aligned): @@ -111,7 +113,7 @@ TEXT syscallN(SB), NOSPLIT|NOFRAME, $0 MOVD 0(R9), R10 // fn BL (R10) - // Get the args pointer back + // Get the args pointer back (R9 was clobbered by the BL call) MOVD 80(RSP), R9 // PTR_ADDRESS = 80 // Save return values (offsets verified by check_arm64.go) @@ -122,6 +124,17 @@ TEXT syscallN(SB), NOSPLIT|NOFRAME, $0 FMOVD F2, 224(R9) // fr3: D2 return for HFA FMOVD F3, 232(R9) // fr4: D3 return for HFA + // Errno capture (conditional): only when errnoFn (offset 256) is non-zero. + // Safe window: we are still on g0, same OS thread as the C call. + // R19 and R20 are callee-saved under AAPCS64, so they survive BL (R20). + MOVD 256(R9), R20 // R20 = errnoFn address + CBZ R20, errno_done + MOVD R9, R19 // R19 = save args pointer across the errno call + BL (R20) // __errno_location()/__error() → R0 = &errno (int*) + MOVW (R0), R0 // R0 = *(&errno) as uint32, zero-extended to 64 bits + MOVD R0, 248(R19) // args->errno = captured errno value + +errno_done: // Restore frame and return MOVD 72(RSP), R30 // Restore LR MOVD 64(RSP), R29 // Restore FP diff --git a/internal/syscall/syscall_unix_amd64.go b/internal/syscall/syscall_unix_amd64.go index 5b9d9f7..fc651c0 100644 --- a/internal/syscall/syscall_unix_amd64.go +++ b/internal/syscall/syscall_unix_amd64.go @@ -24,17 +24,21 @@ var syscallArgsPool = sync.Pool{New: func() any { return new(syscallArgs) }} // // Layout (offsets in bytes): // -// fn: 0 -// a1-a15: 8-128 (6 GP registers + 9 stack slots) -// f1-f8: 128-192 (XMM0-XMM7 as bit patterns) -// r1: 192 (RAX return) -// r2: 200 (RDX return, used for 9-16 byte struct returns) +// fn: 0 +// a1-a15: 8-128 (6 GP registers + 9 stack slots) +// f1-f8: 128-192 (XMM0-XMM7 as bit patterns) +// r1: 192 (RAX return) +// r2: 200 (RDX return, used for 9-16 byte struct returns) +// errno: 208 (captured C errno value; 0 if errnoFn == 0) +// errnoFn: 216 (address of __errno_location/__error; 0 = skip errno capture) 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 r1, r2 uintptr + errno uintptr + errnoFn uintptr } // syscallN is implemented in syscall_unix_amd64.s @@ -104,3 +108,50 @@ func CallNFloat(fn uintptr, gpr [6]uintptr, sse [8]float64, stackArgs [9]uintptr f2 = *(*float64)(unsafe.Pointer(&args.f2)) return } + +// CallNFloatErrno is like CallNFloat but also captures the C errno value set +// by the called function. The errno function address (errnoFn) must be the +// address of __errno_location (Linux/FreeBSD) or __error (macOS), obtained +// from ErrnoFnAddr(). When errnoFn is 0, errno capture is skipped and the +// returned errno value is always 0. +// +// The errno is read inside the assembly trampoline immediately after the C +// function returns, before the Go runtime can migrate the goroutine to a +// different OS thread. This is the only safe window for errno capture. +func CallNFloatErrno(fn uintptr, gpr [6]uintptr, sse [8]float64, stackArgs [9]uintptr, numStack int, errnoFn uintptr) (r1 uintptr, r2 uintptr, f1 float64, f2 float64, cerrno uintptr) { + args := syscallArgsPool.Get().(*syscallArgs) + defer syscallArgsPool.Put(args) + *args = syscallArgs{ + fn: fn, + a1: gpr[0], a2: gpr[1], a3: gpr[2], + a4: gpr[3], a5: gpr[4], a6: gpr[5], + a7: stackArgs[0], + a8: stackArgs[1], + a9: stackArgs[2], + a10: stackArgs[3], + a11: stackArgs[4], + a12: stackArgs[5], + a13: stackArgs[6], + a14: stackArgs[7], + a15: stackArgs[8], + f1: *(*uintptr)(unsafe.Pointer(&sse[0])), + f2: *(*uintptr)(unsafe.Pointer(&sse[1])), + f3: *(*uintptr)(unsafe.Pointer(&sse[2])), + f4: *(*uintptr)(unsafe.Pointer(&sse[3])), + f5: *(*uintptr)(unsafe.Pointer(&sse[4])), + f6: *(*uintptr)(unsafe.Pointer(&sse[5])), + f7: *(*uintptr)(unsafe.Pointer(&sse[6])), + f8: *(*uintptr)(unsafe.Pointer(&sse[7])), + errnoFn: errnoFn, + } + _ = numStack + + runtime_cgocall(syscallNABI0, unsafe.Pointer(args)) + + r1 = args.r1 + r2 = args.r2 + f1 = *(*float64)(unsafe.Pointer(&args.f1)) + f2 = *(*float64)(unsafe.Pointer(&args.f2)) + cerrno = args.errno + return +} diff --git a/internal/syscall/syscall_unix_amd64.s b/internal/syscall/syscall_unix_amd64.s index fc69361..ba617cb 100644 --- a/internal/syscall/syscall_unix_amd64.s +++ b/internal/syscall/syscall_unix_amd64.s @@ -9,32 +9,34 @@ // // syscallN takes a pointer to syscallArgs struct: // struct { -// fn uintptr // offset 0 -// a1 uintptr // offset 8 (RDI) -// a2 uintptr // offset 16 (RSI) -// a3 uintptr // offset 24 (RDX) -// a4 uintptr // offset 32 (RCX) -// a5 uintptr // offset 40 (R8) -// a6 uintptr // offset 48 (R9) -// a7 uintptr // offset 56 (stack[0]) -// a8 uintptr // offset 64 (stack[1]) -// a9 uintptr // offset 72 (stack[2]) -// a10 uintptr // offset 80 (stack[3]) -// a11 uintptr // offset 88 (stack[4]) -// a12 uintptr // offset 96 (stack[5]) -// a13 uintptr // offset 104 (stack[6]) -// a14 uintptr // offset 112 (stack[7]) -// a15 uintptr // offset 120 (stack[8]) -// f1 uintptr // offset 128 (XMM0 bit pattern) -// f2 uintptr // offset 136 (XMM1) -// f3 uintptr // offset 144 (XMM2) -// f4 uintptr // offset 152 (XMM3) -// f5 uintptr // offset 160 (XMM4) -// f6 uintptr // offset 168 (XMM5) -// f7 uintptr // offset 176 (XMM6) -// f8 uintptr // offset 184 (XMM7) -// r1 uintptr // offset 192 (RAX return) -// r2 uintptr // offset 200 (RDX return, 9-16 byte struct) +// fn uintptr // offset 0 +// a1 uintptr // offset 8 (RDI) +// a2 uintptr // offset 16 (RSI) +// a3 uintptr // offset 24 (RDX) +// a4 uintptr // offset 32 (RCX) +// a5 uintptr // offset 40 (R8) +// a6 uintptr // offset 48 (R9) +// a7 uintptr // offset 56 (stack[0]) +// a8 uintptr // offset 64 (stack[1]) +// a9 uintptr // offset 72 (stack[2]) +// a10 uintptr // offset 80 (stack[3]) +// a11 uintptr // offset 88 (stack[4]) +// a12 uintptr // offset 96 (stack[5]) +// a13 uintptr // offset 104 (stack[6]) +// a14 uintptr // offset 112 (stack[7]) +// a15 uintptr // offset 120 (stack[8]) +// f1 uintptr // offset 128 (XMM0 bit pattern) +// f2 uintptr // offset 136 (XMM1) +// f3 uintptr // offset 144 (XMM2) +// f4 uintptr // offset 152 (XMM3) +// f5 uintptr // offset 160 (XMM4) +// f6 uintptr // offset 168 (XMM5) +// f7 uintptr // offset 176 (XMM6) +// f8 uintptr // offset 184 (XMM7) +// r1 uintptr // offset 192 (RAX return) +// r2 uintptr // offset 200 (RDX return, 9-16 byte struct) +// errno uintptr // offset 208 (captured C errno; 0 if errnoFn == 0) +// errnoFn uintptr // offset 216 (address of __errno_location/__error; 0 = skip) // } // // syscallN must be called on the g0 stack with runtime.cgocall. @@ -101,13 +103,27 @@ TEXT syscallN(SB), NOSPLIT|NOFRAME, $0 MOVQ 0(R11), R10 CALL R10 - // Restore pointer and save return values + // Restore args pointer and save C function return values. + // DI was clobbered by the CALL (it held a1/RDI before the call). MOVQ PTR_ADDRESS(BP), DI MOVQ AX, 192(DI) // r1: integer return in RAX MOVQ DX, 200(DI) // r2: second integer return in RDX (9-16 byte structs) MOVQ X0, 128(DI) // f1: float return in XMM0 MOVQ X1, 136(DI) // f2: XMM1 — second SSE return for 9-16B all-float struct returns + // Errno capture (conditional): only when errnoFn (offset 216) is non-zero. + // Safe window: we are still on g0, same OS thread as the C call. + // R12 and R13 are callee-saved under the System V AMD64 ABI, so they + // survive the CALL R13 to __errno_location/__error below. + MOVQ 216(DI), R13 // R13 = errnoFn address + TESTQ R13, R13 + JZ errno_done + MOVQ DI, R12 // R12 = save args pointer across the errno call + CALL R13 // __errno_location()/__error() → RAX = &errno (int*) + MOVL (AX), AX // AX = *(&errno) as int32, zero-extended to 64 bits + MOVQ AX, 208(R12) // args->errno = captured errno value + +errno_done: // Restore stack and return XORL AX, AX // no error (ignored by runtime.cgocall) ADDQ $STACK_SIZE, SP