Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
70 changes: 70 additions & 0 deletions ffi/float_return_windows_amd64_test.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions ffi/testdata/structtest.c
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#include <stdint.h>
#include <stdarg.h>

// 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) {
Expand Down
21 changes: 9 additions & 12 deletions internal/arch/amd64/call_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading