From fd26f942f907d8d88fd0a9bf1b3eff051943d1a7 Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 22 Jul 2026 19:25:20 +0300 Subject: [PATCH] fix(windows): capture scalar float returns --- CHANGELOG.md | 3 ++ README.md | 3 -- ffi/float_return_windows_amd64_test.go | 70 ++++++++++++++++++++++++++ ffi/testdata/structtest.c | 9 ++++ internal/arch/amd64/call_windows.go | 21 ++++---- 5 files changed, 91 insertions(+), 15 deletions(-) create mode 100644 ffi/float_return_windows_amd64_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index dcdac99..3bfdd1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **Windows AMD64 scalar float returns** — recover `float32` and `float64` values from the XMM0 bits exposed as the second result of `syscall.SyscallN` + ## [0.6.1] - 2026-07-21 ### Added diff --git a/README.md b/README.md index 7ac1299..0b091b5 100644 --- a/README.md +++ b/README.md @@ -300,9 +300,6 @@ if err != nil { - Go runtime limitation, not goffi-specific. Go 1.22+ added partial SEH support ([#58542](https://github.com/golang/go/issues/58542)), but edge cases remain. - Workaround: build native libraries with `panic=abort`. -**Windows: float return values not captured from XMM0** -- `syscall.SyscallN` returns RAX only. Go `syscall` package limitation. - **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. diff --git a/ffi/float_return_windows_amd64_test.go b/ffi/float_return_windows_amd64_test.go new file mode 100644 index 0000000..303f478 --- /dev/null +++ b/ffi/float_return_windows_amd64_test.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 Andrey Kolkov and GoGPU Contributors + +//go:build windows && amd64 + +package ffi + +import ( + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +func TestWindowsAMD64ScalarFloatReturns(t *testing.T) { + requireStructLib(t) + + token := byte(1) + pointerArg := unsafe.Pointer(&token) + arguments := []unsafe.Pointer{unsafe.Pointer(&pointerArg)} + + t.Run("float32", func(t *testing.T) { + symbol := requireTestSymbol(t, "return_float32") + cif := prepareWindowsFloatCall(t, types.FloatTypeDescriptor) + + var result float32 + if _, err := CallFunction(cif, symbol, unsafe.Pointer(&result), arguments); err != nil { + t.Fatal(err) + } + if result != 0.125 { + t.Fatalf("return_float32() = %v, want 0.125", result) + } + }) + + t.Run("float64", func(t *testing.T) { + symbol := requireTestSymbol(t, "return_float64") + cif := prepareWindowsFloatCall(t, types.DoubleTypeDescriptor) + + var result float64 + if _, err := CallFunction(cif, symbol, unsafe.Pointer(&result), arguments); err != nil { + t.Fatal(err) + } + if result != 0.625 { + t.Fatalf("return_float64() = %v, want 0.625", result) + } + }) +} + +func requireTestSymbol(t *testing.T, name string) unsafe.Pointer { + t.Helper() + symbol, err := GetSymbol(structTestLib, name) + if err != nil { + t.Fatal(err) + } + return symbol +} + +func prepareWindowsFloatCall(t *testing.T, returnType *types.TypeDescriptor) *types.CallInterface { + t.Helper() + cif := &types.CallInterface{} + if err := PrepareCallInterface( + cif, + types.WindowsCallingConvention, + returnType, + []*types.TypeDescriptor{types.PointerTypeDescriptor}, + ); err != nil { + t.Fatal(err) + } + return cif +} diff --git a/ffi/testdata/structtest.c b/ffi/testdata/structtest.c index 63511bf..9739f82 100644 --- a/ffi/testdata/structtest.c +++ b/ffi/testdata/structtest.c @@ -1,6 +1,15 @@ #include #include +// Scalar float returns use XMM0 on Windows AMD64. Keep a pointer argument so +// the end-to-end tests exercise the same call shape as pointer-based native APIs. +float return_float32(const void *value) { + return value ? 0.125f : 0.0f; +} +double return_float64(const void *value) { + return value ? 0.625 : 0.0; +} + // ≤ 8 bytes: {int32, uint32} — INTEGER class, single GP register struct pair_i32_u32 { int32_t a; uint32_t b; }; int64_t take_struct_8(struct pair_i32_u32 s) { diff --git a/internal/arch/amd64/call_windows.go b/internal/arch/amd64/call_windows.go index 1951571..1342871 100644 --- a/internal/arch/amd64/call_windows.go +++ b/internal/arch/amd64/call_windows.go @@ -71,21 +71,18 @@ func (i *Implementation) Execute( } // Call via syscall.SyscallN — handles all args including stack args (5+). - ret, _, _ := syscall.SyscallN(uintptr(fn), args...) + // On windows/amd64, its second result contains the raw XMM0 bits so callers + // can recover scalar floating-point return values. + ret, floatRet, _ := syscall.SyscallN(uintptr(fn), args...) runtime.KeepAlive(avalue) - // Handle return value. - // Note: float return values in XMM0 are not captured by syscall.SyscallN on Windows. - // This is a known limitation: Go's syscall package on Windows only exposes RAX (ret). - // Float-returning C functions on Windows require a custom assembly wrapper to capture - // XMM0. Since this requires significant additional infrastructure and matches purego's - // documented limitation, it is recorded as a known limitation for v0.4.1. - // 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). - // + returnBits := ret + if cif.ReturnType.Kind == types.FloatType || cif.ReturnType.Kind == types.DoubleType { + returnBits = floatRet + } + // 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) + return 0, i.handleReturn(cif, rvalue, uint64(returnBits), 0, 0, 0) }