From 58b8151f71f923b9e95a6338ef25b2701ea91a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 13 Jul 2026 16:33:38 +0800 Subject: [PATCH 1/4] Add complete Windows 386 ABI support --- .github/workflows/test.yml | 15 +- README.md | 2 +- callback_windows_386.go | 179 ++++++++++++ callback_windows_other.go | 15 + func.go | 95 +++++-- func_test.go | 8 +- struct_other.go | 5 +- struct_test.go | 47 ++-- struct_windows_386.go | 107 ++++++++ sys_386.s | 41 ++- sys_arm.s | 2 +- sys_windows_386.s | 67 +++++ syscall_32bit.go | 6 +- syscall_windows.go | 18 +- testdata/abitest/abi_test.c | 201 ++++++++++++++ testdata/structtest/struct_test.c | 3 +- wincallback.go | 2 +- windows_386_test.go | 443 ++++++++++++++++++++++++++++++ zcallback_386.s | 2 +- 19 files changed, 1172 insertions(+), 86 deletions(-) create mode 100644 callback_windows_386.go create mode 100644 callback_windows_other.go create mode 100644 struct_windows_386.go create mode 100644 sys_windows_386.s create mode 100644 windows_386_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5c334cac..d4b419b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,6 +34,9 @@ jobs: - name: Set up Msys2 if: runner.os == 'Windows' && runner.arch != 'ARM64' uses: msys2/setup-msys2@v2 + with: + install: mingw-w64-i686-gcc + path-type: inherit - name: go vet run: | @@ -124,14 +127,20 @@ jobs: env CGO_ENABLED=1 go test "-gcflags=all=-N -l" -v ./... - name: go test (Windows 386) - if: runner.os == 'Windows' + if: runner.os == 'Windows' && runner.arch != 'ARM64' + shell: msys2 {0} run: | - env CGO_ENABLED=0 GOARCH=386 go test -shuffle=on -v -count=10 ./... + CC="$(cygpath -w /mingw32/bin/gcc.exe)" + env CGO_ENABLED=0 GOARCH=386 CC="$CC" go test -shuffle=on -v -count=10 ./... + env CGO_ENABLED=0 GOARCH=386 CC="$CC" go test "-gcflags=all=-N -l" -v ./... - name: go test (Windows 386, Cgo) if: runner.os == 'Windows' && runner.arch != 'ARM64' + shell: msys2 {0} run: | - env CGO_ENABLED=1 GOARCH=386 go test -shuffle=on -v -count=10 ./... + CC="$(cygpath -w /mingw32/bin/gcc.exe)" + env CGO_ENABLED=1 GOARCH=386 CC="$CC" go test -shuffle=on -v -count=10 ./... + env CGO_ENABLED=1 GOARCH=386 CC="$CC" go test "-gcflags=all=-N -l" -v ./... - name: go test (Linux 386) if: ${{ runner.os == 'Linux' && runner.arch != 'ARM64' }} diff --git a/README.md b/README.md index 6ca453a1..6e7dbf29 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Tier 2 platforms are supported by PureGo on a best-effort basis. Critical bugs o - **FreeBSD**: amd643,4, arm643,4 - **Linux**: 3863, arm3, loong642, ppc64le2, riscv643, s390x3, 5 - **NetBSD**: amd643,4, arm643,4 -- **Windows**: 3863,6, arm3,6,7 +- **Windows**: 3862, arm3,6,7 #### Support Notes diff --git a/callback_windows_386.go b/callback_windows_386.go new file mode 100644 index 00000000..32554143 --- /dev/null +++ b/callback_windows_386.go @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build windows && 386 + +package purego + +import ( + "math" + "reflect" + "sync" + "syscall" + "unsafe" +) + +const ( + win386CallbackResultInteger = iota + win386CallbackResultFloat32 + win386CallbackResultFloat64 + win386MaxCallbacks = 2000 +) + +// win386CallbackResult is filled by win386CallbackDispatch on the C stack. +// callbackasm1 then moves integer results to EDX:EAX or floating-point results +// to x87 ST(0), as required by the 32-bit Windows ABI. +type win386CallbackResult struct { + low uintptr + high uintptr + float64 uint64 + kind uintptr + stackPop uintptr +} + +type win386Callback struct { + fn reflect.Value + argSlots int + isCDecl bool +} + +var win386Callbacks struct { + sync.RWMutex + funcs [win386MaxCallbacks]win386Callback + n int +} + +var ( + win386CallbackBridgeOnce sync.Once + win386CallbackBridge uintptr +) + +// callbackasm is implemented by zcallback_386.s. +// +//go:linkname win386Callbackasm callbackasm +var win386Callbackasm byte + +func win386CallbackasmAddr(index int) uintptr { + // Each entry is MOVL $index, CX followed by JMP callbackasm1. + return uintptr(unsafe.Pointer(&win386Callbackasm)) + uintptr(index*10) +} + +func newCallback(fn any, isCDecl bool) uintptr { + value := reflect.ValueOf(fn) + if value.Kind() != reflect.Func { + panic("purego: the type must be a function") + } + if value.IsNil() { + panic("purego: function must not be nil") + } + + typeOfFn := value.Type() + argSlots := 0 + for i := 0; i < typeOfFn.NumIn(); i++ { + in := typeOfFn.In(i) + if i == 0 && in.AssignableTo(reflect.TypeFor[CDecl]()) { + continue + } + switch in.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Pointer, reflect.UnsafePointer: + default: + panic("purego: unsupported argument type: " + in.Kind().String()) + } + argSlots += int((in.Size() + unsafe.Sizeof(uintptr(0)) - 1) / unsafe.Sizeof(uintptr(0))) + } + if argSlots > maxArgs { + panic("purego: too many callback argument slots") + } + + if typeOfFn.NumOut() > 1 { + panic("purego: callbacks can only have one return") + } + if typeOfFn.NumOut() == 1 { + switch out := typeOfFn.Out(0); out.Kind() { + case reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Pointer, reflect.UnsafePointer: + default: + panic("purego: unsupported return type: " + out.String()) + } + } + + win386CallbackBridgeOnce.Do(func() { + // The native trampoline always cleans these three private arguments. + // The original callback's cdecl/stdcall cleanup is handled separately. + win386CallbackBridge = syscall.NewCallback(win386CallbackDispatch) + }) + + win386Callbacks.Lock() + defer win386Callbacks.Unlock() + if win386Callbacks.n >= len(win386Callbacks.funcs) { + panic("purego: the maximum number of callbacks has been reached") + } + index := win386Callbacks.n + win386Callbacks.funcs[index] = win386Callback{fn: value, argSlots: argSlots, isCDecl: isCDecl} + win386Callbacks.n++ + return win386CallbackasmAddr(index) +} + +// win386CallbackDispatch has only pointer-sized arguments and return value, so +// it can use the standard library's restricted Windows/386 callback adapter. +// The original callback's wider typed ABI is decoded here by purego. +func win386CallbackDispatch(index uintptr, stack unsafe.Pointer, result *win386CallbackResult) uintptr { + *result = win386CallbackResult{} + win386Callbacks.RLock() + callback := win386Callbacks.funcs[index] + win386Callbacks.RUnlock() + + fnType := callback.fn.Type() + values := make([]reflect.Value, fnType.NumIn()) + slots := (*[maxArgs]uintptr)(stack) + stackSlot := 0 + for i := range values { + in := fnType.In(i) + if i == 0 && in.AssignableTo(reflect.TypeFor[CDecl]()) { + values[i] = reflect.Zero(in) + continue + } + values[i] = reflect.NewAt(in, unsafe.Pointer(&slots[stackSlot])).Elem() + stackSlot += int((in.Size() + unsafe.Sizeof(uintptr(0)) - 1) / unsafe.Sizeof(uintptr(0))) + } + + if !callback.isCDecl { + result.stackPop = uintptr(callback.argSlots) * unsafe.Sizeof(uintptr(0)) + } + returned := callback.fn.Call(values) + if len(returned) == 0 { + return 0 + } + + value := returned[0] + switch value.Kind() { + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + bits := value.Uint() + result.low = uintptr(bits) + result.high = uintptr(bits >> 32) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + bits := uint64(value.Int()) + result.low = uintptr(bits) + result.high = uintptr(bits >> 32) + case reflect.Bool: + if value.Bool() { + result.low = 1 + } + case reflect.Pointer, reflect.UnsafePointer: + result.low = value.Pointer() + case reflect.Float32: + result.float64 = uint64(math.Float32bits(float32(value.Float()))) + result.kind = win386CallbackResultFloat32 + case reflect.Float64: + result.float64 = math.Float64bits(value.Float()) + result.kind = win386CallbackResultFloat64 + default: + panic("purego: unsupported callback return kind: " + value.Kind().String()) + } + return 0 +} diff --git a/callback_windows_other.go b/callback_windows_other.go new file mode 100644 index 00000000..c193040d --- /dev/null +++ b/callback_windows_other.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build windows && !386 + +package purego + +import "syscall" + +func newCallback(fn any, isCDecl bool) uintptr { + if isCDecl { + return syscall.NewCallbackCDecl(fn) + } + return syscall.NewCallback(fn) +} diff --git a/func.go b/func.go index 6ddf3ce3..1573753f 100644 --- a/func.go +++ b/func.go @@ -63,7 +63,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { // int64 <=> int64_t // float32 <=> float // float64 <=> double -// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64) +// struct <=> struct (android, darwin, ios, linux, and windows on amd64/arm64/386) // func <=> C function // unsafe.Pointer, *T <=> void* // []T => void* @@ -101,7 +101,7 @@ func RegisterLibFunc(fptr any, handle uintptr, name string) { // On Apple ARM64 platforms (macOS and iOS), purego handles proper alignment of struct arguments // when passing them on the stack, following the C ABI's byte-level packing rules. // -// On Windows, struct arguments and returns are supported on amd64 and arm64 when calling C functions. +// On Windows, struct arguments and returns are supported on amd64, arm64, and 386 when calling C functions. // Passing or returning structs in callbacks created with [NewCallback] is not supported on Windows. // // # Example @@ -164,21 +164,34 @@ func RegisterFunc(fptr any, cfn uintptr) { case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Pointer, reflect.UnsafePointer, reflect.Slice, reflect.Bool: - if ints < numOfIntegerRegisters() { - ints++ - } else { - stack++ + slots := 1 + if runtime.GOOS == "windows" && runtime.GOARCH == "386" && arg.Size() == 8 { + slots = 2 + } + for range slots { + if ints < numOfIntegerRegisters() { + ints++ + } else { + stack++ + } } case reflect.Float32, reflect.Float64: - if floats < floatArgRegs { - floats++ - } else { - stack++ + slots := 1 + if runtime.GOOS == "windows" && runtime.GOARCH == "386" && arg.Size() == 8 { + slots = 2 + } + for range slots { + if floats < floatArgRegs { + floats++ + } else { + stack++ + } } case reflect.Struct: ensureStructSupported() - if arg.Size() == 0 && runtime.GOOS != "windows" { - // On Windows an empty struct still consumes one argument slot. + if arg.Size() == 0 && (runtime.GOOS != "windows" || runtime.GOARCH == "386") { + // The System V ABI and the Windows/386 GNU ABI do not + // consume an argument slot for an empty struct. continue } addInt := func(u uintptr) { @@ -290,8 +303,21 @@ func RegisterFunc(fptr any, cfn uintptr) { }() var arm64_r8 uintptr + if runtime.GOARCH == "386" && ty.NumOut() == 1 && + (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) { + // The 386 bridge uses this to avoid popping x87 ST(0) after + // non-floating calls. + arm64_r8 = 1 + } if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { outType := ty.Out(0) + if runtime.GOOS == "windows" && runtime.GOARCH == "386" && isSingleFloatStruct(outType) { + // MinGW follows the GNU i386 convention for a struct whose + // only field is a float: it returns the value in x87 ST(0). + // MSVC returns the same struct in EAX/EDX, so mode 2 asks the + // assembly bridge to detect which form the callee used. + arm64_r8 = 2 + } if structReturnInMemory(outType) { // The caller allocates the return value and passes its pointer // as a hidden first integer argument. @@ -332,11 +358,11 @@ func RegisterFunc(fptr any, cfn uintptr) { } var syscall *syscallArgs - if runtime.GOOS == "windows" && runtime.GOARCH != "arm64" { - // Windows amd64, 386, and arm use syscall.SyscallN. + if runtime.GOOS == "windows" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" { + // Windows amd64 and arm use syscall.SyscallN. syscall = thePool.Get().(*syscallArgs) syscall.a1, syscall.a2, _ = syscall_syscallN(cfn, sysargs[:numStack]...) - syscall.f1 = syscall.a2 // on amd64 a2 stores the float return. On 32bit platforms floats aren't support + syscall.f1 = syscall.a2 // on amd64 a2 stores the float return } else { syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], arm64_r8) } @@ -348,9 +374,17 @@ func RegisterFunc(fptr any, cfn uintptr) { v := reflect.New(outType).Elem() switch outType.Kind() { case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - v.SetUint(uint64(syscall.a1)) + bits := uint64(syscall.a1) + if is32bit && outType.Size() == 8 { + bits |= uint64(syscall.a2) << 32 + } + v.SetUint(bits) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - v.SetInt(int64(syscall.a1)) + bits := uint64(syscall.a1) + if is32bit && outType.Size() == 8 { + bits |= uint64(syscall.a2) << 32 + } + v.SetInt(int64(bits)) case reflect.Bool: v.SetBool(byte(syscall.a1) != 0) case reflect.UnsafePointer: @@ -380,7 +414,7 @@ func RegisterFunc(fptr any, cfn uintptr) { v.SetFloat(math.Float64frombits(uint64(syscall.f1))) case "s390x": // S390X is big-endian: float32 in upper 32 bits of 64-bit register - v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1 >> 32)))) + v.SetFloat(float64(math.Float32frombits(uint32(uint64(syscall.f1) >> 32)))) default: v.SetFloat(float64(math.Float32frombits(uint32(syscall.f1)))) } @@ -408,6 +442,14 @@ func RegisterFunc(fptr any, cfn uintptr) { fn.Set(v) } +func isSingleFloatStruct(ty reflect.Type) bool { + if ty.Kind() != reflect.Struct || ty.NumField() != 1 { + return false + } + kind := ty.Field(0).Type.Kind() + return kind == reflect.Float32 || kind == reflect.Float64 +} + func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat func(x uintptr), addStack func(x uintptr), numInts *int, numFloats *int, numStack *int) []any { const is32bit = unsafe.Sizeof(uintptr(0)) == 4 switch v.Kind() { @@ -416,9 +458,17 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat keepAlive = append(keepAlive, ptr) addInt(uintptr(unsafe.Pointer(ptr))) case reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - addInt(uintptr(v.Uint())) + bits := v.Uint() + addInt(uintptr(bits)) + if runtime.GOOS == "windows" && runtime.GOARCH == "386" && v.Type().Size() == 8 { + addInt(uintptr(bits >> 32)) + } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - addInt(uintptr(v.Int())) + bits := uint64(v.Int()) + addInt(uintptr(bits)) + if runtime.GOOS == "windows" && runtime.GOARCH == "386" && v.Type().Size() == 8 { + addInt(uintptr(bits >> 32)) + } case reflect.Pointer, reflect.UnsafePointer, reflect.Slice: // There is no need to keepAlive this pointer separately because it is kept alive in the args variable addInt(v.Pointer()) @@ -513,10 +563,13 @@ func checkStructFieldsSupported(ty reflect.Type) { // ensureStructSupported panics if passing or returning structs through a call to // a C function is unsupported on the current platform. func ensureStructSupported() { + if runtime.GOOS == "windows" && runtime.GOARCH == "386" { + return + } switch runtime.GOARCH { case "amd64", "arm64", "loong64", "ppc64le": default: - panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, and ppc64le") + panic("purego: struct arguments/returns are only supported on amd64, arm64, loong64, ppc64le, and windows/386") } switch runtime.GOOS { case "android", "darwin", "ios", "linux", "windows": diff --git a/func_test.go b/func_test.go index 2c0cae4d..7eb68fcb 100644 --- a/func_test.go +++ b/func_test.go @@ -122,9 +122,6 @@ func TestRegisterFunc_Floats(t *testing.T) { t.Skip("Platform doesn't support Floats") return } - if runtime.GOOS == "windows" && runtime.GOARCH == "386" { - t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test - } library, err := getSystemLibrary() if err != nil { t.Fatalf("couldn't get system library: %s", err) @@ -651,12 +648,9 @@ func buildSharedLib(tb testing.TB, compilerEnv, libFile string, sources ...strin } // Compiling the library needs a C toolchain targeting GOARCH. CI has none - // for Windows on 386 or arm64, so skip those (the prebuilt path above - // avoids the toolchain). + // for Windows on arm64 (the prebuilt path above avoids the toolchain). if runtime.GOOS == "windows" { switch runtime.GOARCH { - case "386": - tb.Skip("need a 386 C toolchain to run this test") // TODO: find a 386 C toolchain for test case "arm64": tb.Skip("need an arm64 C toolchain to run this test") } diff --git a/struct_other.go b/struct_other.go index c50daf38..7903f7e4 100644 --- a/struct_other.go +++ b/struct_other.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors -//go:build !(amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x) +//go:build !(amd64 || arm || arm64 || loong64 || ppc64le || riscv64 || s390x || (windows && 386)) package purego @@ -11,8 +11,7 @@ import ( ) // This file provides the struct-handling helpers for architectures that do -// not support struct arguments or returns: 386, and any architecture reachable -// only through the integer-only cgo fallback. Every struct operation panics. +// not support struct arguments or returns. Every struct operation panics. func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { panic("purego: struct arguments are not supported on this architecture") diff --git a/struct_test.go b/struct_test.go index a3f6e178..9424ed26 100644 --- a/struct_test.go +++ b/struct_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2024 The Ebitengine Authors -//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64 || ppc64le) +//go:build (darwin || linux || windows) && (amd64 || arm64 || loong64 || ppc64le || (windows && 386)) package purego_test @@ -79,8 +79,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { } for _, imp := range implementations { if imp.usesCallbacks && runtime.GOOS == "windows" { - // Callbacks on Windows use the stdlib syscall.NewCallback, which does - // not support struct arguments or returns. + // Windows callbacks do not support struct arguments or returns. continue } if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { @@ -95,7 +94,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return 0xdeadbeef }) if ret := NoStruct(Empty{}); ret != expectedUnsigned { - t.Fatalf("NoStruct returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("NoStruct returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -105,14 +104,14 @@ func TestRegisterFunc_structArgs(t *testing.T) { return 0xdeadbeef }) if ret := EmptyEmptyFn(EmptyEmpty{}); ret != expectedUnsigned { - t.Fatalf("EmptyEmpty returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("EmptyEmpty returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } var EmptyEmptyWithReg func(uint32, EmptyEmpty, uint32) int64 register(&EmptyEmptyWithReg, lib, "EmptyEmptyWithReg", func(x uint32, _ EmptyEmpty, y uint32) int64 { return int64(x)<<16 | int64(y) }) if ret := EmptyEmptyWithReg(0xdead, EmptyEmpty{}, 0xbeef); ret != expectedUnsigned { - t.Fatalf("EmptyEmptyWithReg returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("EmptyEmptyWithReg returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -125,7 +124,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return *g.x + *g.y + *g.z }) if ret := GreaterThan16BytesFn(GreaterThan16Bytes{x: &x, y: &y, z: &z}); ret != expectedUnsigned { - t.Fatalf("GreaterThan16Bytes returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("GreaterThan16Bytes returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -140,7 +139,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return *g.a.x + *g.a.y + *g.a.z }) if ret := GreaterThan16BytesStructFn(GreaterThan16BytesStruct{a: struct{ x, y, z *int64 }{x: &x, y: &y, z: &z}}); ret != expectedUnsigned { - t.Fatalf("GreaterThan16BytesStructFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("GreaterThan16BytesStructFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -160,8 +159,9 @@ func TestRegisterFunc_structArgs(t *testing.T) { } return stack }) - if ret := AfterRegisters(0xD0000000, 0xE000000, 0xA00000, 0xD0000, 0xB000, 0xE00, 0xE0, 0xF, GreaterThan16Bytes{x: &x, y: &y, z: &z}); ret != expectedUnsigned { - t.Fatalf("AfterRegisters returned %#x wanted %#x", ret, expectedUnsigned) + firstRegisterBits := uint32(0xD0000000) + if ret := AfterRegisters(int(firstRegisterBits), 0xE000000, 0xA00000, 0xD0000, 0xB000, 0xE00, 0xE0, 0xF, GreaterThan16Bytes{x: &x, y: &y, z: &z}); ret != expectedUnsigned { + t.Fatalf("AfterRegisters returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } var BeforeRegisters func(bytes GreaterThan16Bytes, a, b int64) uint64 z -= 0xFF @@ -169,7 +169,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return uint64(*bytes.x + *bytes.y + *bytes.z + a + b) }) if ret := BeforeRegisters(GreaterThan16Bytes{&x, &y, &z}, 0x0F, 0xF0); ret != expectedUnsigned { - t.Fatalf("BeforeRegisters returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("BeforeRegisters returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -181,7 +181,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return l.x + l.y }) if ret := IntLessThan16BytesFn(IntLessThan16Bytes{0xDEAD0000, 0xBEEF}); ret != expectedUnsigned { - t.Fatalf("IntLessThan16BytesFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("IntLessThan16BytesFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -345,7 +345,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return uint32(b.a)<<24 | uint32(b.b)<<16 | uint32(b.c)<<8 | uint32(b.d) }) if ret := UnsignedChar4BytesFn(UnsignedChar4Bytes{a: 0xDE, b: 0xAD, c: 0xBE, d: 0xEF}); ret != expectedUnsigned { - t.Fatalf("UnsignedChar4BytesFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("UnsignedChar4BytesFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -373,7 +373,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { z: struct{ c byte }{c: 0xBE}, w: struct{ d byte }{d: 0xEF}, }); ret != expectedUnsigned { - t.Fatalf("UnsignedChar4BytesStructFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("UnsignedChar4BytesStructFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -471,7 +471,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return uint32(a.a[0])<<24 | uint32(a.a[1])<<16 | uint32(a.a[2])<<8 | uint32(a.a[3]) }) if ret := Array4UnsignedCharsFn(Array4UnsignedChars{a: [...]uint8{0xDE, 0xAD, 0xBE, 0xEF}}); ret != expectedUnsigned { - t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -483,7 +483,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return uint32(a.a[0])<<24 | uint32(a.a[1])<<16 | uint32(a.a[2])<<8 | 0xef }) if ret := Array3UnsignedChars(Array3UnsignedChar{a: [...]uint8{0xDE, 0xAD, 0xBE}}); ret != expectedUnsigned { - t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -495,7 +495,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { return uint32(a.a[0])<<16 | uint32(a.a[1]) }) if ret := Array2UnsignedShorts(Array2UnsignedShort{a: [...]uint16{0xDEAD, 0xBEEF}}); ret != expectedUnsigned { - t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, expectedUnsigned) + t.Fatalf("Array4UnsignedCharsFn returned %#x wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -591,7 +591,7 @@ func TestRegisterFunc_structArgs(t *testing.T) { // These numbers are created so that when added together and then divided by 11 it produces 0xdeadbeef Content{point{x: 41_000_000_000, y: 95_000_000}, size{width: 214_000, height: 149}}, 15, 4, true); ret != expectedUnsigned { - t.Fatalf("InitWithContentRect returned %d wanted %#x", ret, expectedUnsigned) + t.Fatalf("InitWithContentRect returned %d wanted %#x", ret, uint64(expectedUnsigned)) } } { @@ -622,11 +622,11 @@ func TestRegisterFunc_structArgs(t *testing.T) { } { type OneLong struct{ a uintptr } - var TakeGoUintAndReturn func(a OneLong) uint64 - register(&TakeGoUintAndReturn, lib, "TakeGoUintAndReturn", func(a OneLong) uint64 { - return uint64(a.a) + var TakeGoUintAndReturn func(a OneLong) uintptr + register(&TakeGoUintAndReturn, lib, "TakeGoUintAndReturn", func(a OneLong) uintptr { + return a.a }) - expected := uint64(7) + expected := uintptr(7) if ret := TakeGoUintAndReturn(OneLong{7}); ret != expected { t.Fatalf("TakeGoUintAndReturn returned %+v wanted %+v", ret, expected) } @@ -917,8 +917,7 @@ func TestRegisterFunc_structReturns(t *testing.T) { } for _, imp := range implementations { if imp.usesCallbacks && runtime.GOOS == "windows" { - // Callbacks on Windows use the stdlib syscall.NewCallback, which does - // not support struct arguments or returns. + // Windows callbacks do not support struct arguments or returns. continue } if imp.usesCallbacks && runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" { diff --git a/struct_windows_386.go b/struct_windows_386.go new file mode 100644 index 00000000..14251063 --- /dev/null +++ b/struct_windows_386.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build windows && 386 + +package purego + +import ( + "math" + "reflect" + "unsafe" +) + +// Win32 passes struct arguments inline on the stack. Each argument consumes +// enough four-byte slots to hold its bytes; padding in the final slot is not +// part of the value and is cleared here for deterministic calls. +func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFloat, addStack func(uintptr), keepAlive []any) []any { + size := v.Type().Size() + if size == 0 { + // Empty structs are a GNU C extension. The Windows GNU ABI does not + // allocate a stack slot for them. + return keepAlive + } + + copyOfValue := reflect.New(v.Type()) + copyOfValue.Elem().Set(v) + base := unsafe.Pointer(copyOfValue.Pointer()) + for offset := uintptr(0); offset < size; offset += unsafe.Sizeof(uintptr(0)) { + var slot uintptr + bytesLeft := size - offset + if bytesLeft > unsafe.Sizeof(slot) { + bytesLeft = unsafe.Sizeof(slot) + } + dst := unsafe.Slice((*byte)(unsafe.Pointer(&slot)), int(bytesLeft)) + src := unsafe.Slice((*byte)(unsafe.Add(base, offset)), int(bytesLeft)) + copy(dst, src) + addInt(slot) + } + return keepAlive +} + +// structReturnInMemory reports whether Win32 returns the struct through a +// caller-allocated hidden pointer passed as the first stack argument. One-, +// two-, and four-byte structs are returned in EAX and eight-byte structs in +// EDX:EAX. +func structReturnInMemory(outType reflect.Type) bool { + switch outType.Size() { + case 0, 1, 2, 4, 8: + return false + default: + return true + } +} + +func getStruct(outType reflect.Type, syscall syscallArgs) reflect.Value { + if isSingleFloatStruct(outType) && syscall.floatReturn != 0 { + bits := uint64(syscall.f1) | uint64(syscall.f2)<<32 + if outType.Field(0).Type.Kind() == reflect.Float32 { + result := uintptr(math.Float32bits(float32(math.Float64frombits(bits)))) + return reflect.NewAt(outType, unsafe.Pointer(&result)).Elem() + } + result := struct { + low uintptr + high uintptr + }{uintptr(bits), uintptr(bits >> 32)} + return reflect.NewAt(outType, unsafe.Pointer(&result)).Elem() + } + switch size := outType.Size(); { + case size == 0: + return reflect.New(outType).Elem() + case structReturnInMemory(outType): + // Win32 aggregate-return functions return the hidden result pointer in + // EAX after filling it. + return reflect.NewAt(outType, *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))).Elem() + case size <= unsafe.Sizeof(uintptr(0)): + result := syscall.a1 + return reflect.NewAt(outType, unsafe.Pointer(&result)).Elem() + default: + result := struct { + low uintptr + high uintptr + }{syscall.a1, syscall.a2} + return reflect.NewAt(outType, unsafe.Pointer(&result)).Elem() + } +} + +func shouldBundleStackArgs(v reflect.Value, numInts, numFloats int) bool { + return false +} + +func collectStackArgs(args []reflect.Value, startIdx int, numInts, numFloats int, + keepAlive []any, addInt, addFloat, addStack func(uintptr), + pNumInts, pNumFloats, pNumStack *int) ([]reflect.Value, []any) { + panic("purego: collectStackArgs should not be called on windows/386") +} + +func bundleStackArgs(stackArgs []reflect.Value, addStack func(uintptr)) { + panic("purego: bundleStackArgs should not be called on windows/386") +} + +func getCallbackStruct(inType reflect.Type, frame unsafe.Pointer, floatsN *int, intsN *int, stackSlot *int, stackByteOffset *uintptr) reflect.Value { + panic("purego: struct callback arguments are not supported on windows/386") +} + +func setStruct(a *callbackArgs, ret reflect.Value) { + panic("purego: struct callback returns are not supported on windows/386") +} diff --git a/sys_386.s b/sys_386.s index 476f5631..f979d190 100644 --- a/sys_386.s +++ b/sys_386.s @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors -//go:build linux +//go:build linux || windows #include "textflag.h" #include "go_asm.h" @@ -20,12 +20,12 @@ // f1 uintptr // ... // f16 uintptr -// arm64_r8 uintptr +// floatReturn uintptr // } // syscallX must be called on the g0 stack with the // C calling convention (use libcCall). // -// On i386 System V ABI, all arguments are passed on the stack. +// On the i386 System V and Windows ABIs, all arguments are passed on the stack. // Return value is in EAX (and EDX for 64-bit values). GLOBL ·syscallXABI0(SB), NOPTR|RODATA, $4 DATA ·syscallXABI0(SB)/4, $syscallX(SB) @@ -44,17 +44,17 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0 MOVL AX, BX // save args pointer in BX // Allocate stack space for C function arguments - // i386 SysV: all 32 args on stack = 32 * 4 = 128 bytes + // i386: all 32 args on stack = 32 * 4 = 128 bytes // Plus 16 bytes for alignment and local storage SUBL $STACK_SIZE, SP - MOVL BX, PTR_ADDRESS(SP) // save args pointer + MOVL SP, DI // save the stack pointer; the target may use cdecl or stdcall // Load function pointer MOVL syscallArgs_fn(BX), AX MOVL AX, (PTR_ADDRESS-4)(SP) // save fn pointer // Push all integer arguments onto the stack (a1-a32) - // i386 SysV ABI: arguments pushed right-to-left, but we're + // i386 ABIs pass arguments right-to-left, but we're // setting up the stack from low to high addresses MOVL syscallArgs_a1(BX), AX MOVL AX, 0(SP) @@ -123,19 +123,38 @@ TEXT syscallX(SB), NOSPLIT|NOFRAME, $0-0 // Call the C function MOVL (PTR_ADDRESS-4)(SP), AX + CLD CALL AX - // Get args pointer back and save results - MOVL PTR_ADDRESS(SP), BX + // BX and DI are callee-saved in both the i386 SysV and Windows ABIs. + // Restore SP explicitly because a stdcall target may have popped its args. MOVL AX, syscallArgs_a1(BX) // return value r1 MOVL DX, syscallArgs_a2(BX) // return value r2 (for 64-bit returns) - // Save x87 FPU return value (ST0) to f1 field - // On i386 System V ABI, float/double returns are in ST(0) - // We save as float64 (8 bytes) to preserve precision + // Save an x87 return only when RegisterFunc requested one. Mode 1 is a + // scalar float result and therefore requires ST(0). Mode 2 is a one-field + // float struct: MinGW returns it in ST(0), while MSVC uses EAX/EDX, so first + // use FXAM to distinguish an x87 result from an empty register stack. + CMPL syscallArgs_floatReturn(BX), $0 + JE no_float_return + CMPL syscallArgs_floatReturn(BX), $2 + JNE save_float_return + FXAM + FSTSW AX + ANDL $0x4500, AX // C3, C2, C0 + CMPL AX, $0x4100 // empty x87 register + JNE save_float_return + MOVL $0, syscallArgs_floatReturn(BX) + JMP no_float_return + +save_float_return: FMOVDP F0, syscallArgs_f1(BX) + MOVL $1, syscallArgs_floatReturn(BX) + +no_float_return: // Clean up stack + MOVL DI, SP ADDL $STACK_SIZE, SP // Restore callee-saved registers diff --git a/sys_arm.s b/sys_arm.s index f1ea44a2..3154a258 100644 --- a/sys_arm.s +++ b/sys_arm.s @@ -20,7 +20,7 @@ // f1 uintptr // ... // f16 uintptr -// arm64_r8 uintptr +// floatReturn uintptr // } // syscallX must be called on the g0 stack with the // C calling convention (use libcCall). diff --git a/sys_windows_386.s b/sys_windows_386.s new file mode 100644 index 00000000..866cad85 --- /dev/null +++ b/sys_windows_386.s @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build windows && 386 + +#include "textflag.h" + +// callbackasm1 receives an index in CX from the callbackasm table. It calls a +// pointer-sized standard-library callback which decodes the original typed +// stack frame, then restores the native Windows/386 result registers. +TEXT callbackasm1(SB), NOSPLIT|NOFRAME, $0 + // Preserve the non-volatile Windows x86 registers. + PUSHL BP + PUSHL BX + PUSHL SI + PUSHL DI + + // Layout after this allocation: + // 0..11 private bridge arguments + // 12..35 win386CallbackResult + // 36..51 saved DI, SI, BX, BP + // 52 original return address + // 56.. original callback arguments + SUBL $36, SP + MOVL CX, 0(SP) + LEAL 56(SP), AX + MOVL AX, 4(SP) + LEAL 12(SP), AX + MOVL AX, 8(SP) + + MOVL ·win386CallbackBridge(SB), AX + CALL AX // stdcall bridge pops its three private arguments + SUBL $12, SP // return to the base of our local frame + + // Move the original return address to the post-cleanup stack position. + // This supports a variable stdcall pop while leaving EDX available for a + // 64-bit result. For cdecl, stackPop is zero and this rewrites the same slot. + MOVL 32(SP), CX + MOVL 52(SP), DI + LEAL 52(SP), SI + ADDL CX, SI + MOVL DI, 0(SI) + + MOVL 12(SP), AX + MOVL 16(SP), DX + MOVL 28(SP), DI + CMPL DI, $1 + JE callback_float32 + CMPL DI, $2 + JE callback_float64 + JMP callback_result_ready + +callback_float32: + FMOVF 20(SP), F0 + JMP callback_result_ready + +callback_float64: + FMOVD 20(SP), F0 + +callback_result_ready: + ADDL $36, SP + POPL DI + POPL SI + POPL BX + POPL BP + ADDL CX, SP + RET diff --git a/syscall_32bit.go b/syscall_32bit.go index c0641110..68f2dbd6 100644 --- a/syscall_32bit.go +++ b/syscall_32bit.go @@ -18,10 +18,10 @@ type syscallArgs struct { fn, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15 uintptr a16, a17, a18, a19, a20, a21, a22, a23, a24, a25, a26, a27, a28, a29, a30, a31, a32 uintptr f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, f11, f12, f13, f14, f15, f16 uintptr - arm64_r8 uintptr + floatReturn uintptr } -func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintptr) *syscallArgs { +func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, floatReturn uintptr) *syscallArgs { s := thePool.Get().(*syscallArgs) *s = syscallArgs{ fn: fn, @@ -37,7 +37,7 @@ func syscall_SyscallN(fn uintptr, sysargs []uintptr, floats []uintptr, r8 uintpt f5: floats[4], f6: floats[5], f7: floats[6], f8: floats[7], f9: floats[8], f10: floats[9], f11: floats[10], f12: floats[11], f13: floats[12], f14: floats[13], f15: floats[14], f16: floats[15], - arm64_r8: r8, + floatReturn: floatReturn, } runtime_cgocall(syscallXABI0, unsafe.Pointer(s)) return s diff --git a/syscall_windows.go b/syscall_windows.go index 1e2ba8cb..29e557a1 100644 --- a/syscall_windows.go +++ b/syscall_windows.go @@ -17,15 +17,18 @@ func syscall_syscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) { } // NewCallback converts a Go function to a function pointer conforming to the stdcall calling convention. -// This is useful when interoperating with Windows code requiring callbacks. The argument is expected to be a -// function with one uintptr-sized result. The function must not have arguments with size larger than the -// size of uintptr. Only a limited number of callbacks may be created in a single Go process, and any memory -// allocated for these callbacks is never released. Between NewCallback and NewCallbackCDecl, at least 1024 -// callbacks can always be created. Although this function is similiar to the darwin version it may act +// This is useful when interoperating with Windows code requiring callbacks. On 386 it supports scalar +// integer, pointer, bool, float32, and float64 arguments and results, including 64-bit values occupying two +// stack slots. On other Windows architectures, the standard library's callback restrictions apply. +// Only a limited number of callbacks may be created in a single Go process, and any memory allocated for +// these callbacks is never released. Although this function is similar to the Unix version it may act // differently. func NewCallback(fn any) uintptr { isCDecl := false ty := reflect.TypeOf(fn) + if ty == nil || ty.Kind() != reflect.Func { + panic("purego: the type must be a function") + } for i := 0; i < ty.NumIn(); i++ { in := ty.In(i) if !in.AssignableTo(reflect.TypeOf(CDecl{})) { @@ -36,10 +39,7 @@ func NewCallback(fn any) uintptr { } isCDecl = true } - if isCDecl { - return syscall.NewCallbackCDecl(fn) - } - return syscall.NewCallback(fn) + return newCallback(fn, isCDecl) } func loadSymbol(handle uintptr, name string) (uintptr, error) { diff --git a/testdata/abitest/abi_test.c b/testdata/abitest/abi_test.c index 824e453d..ae8dbed2 100644 --- a/testdata/abitest/abi_test.c +++ b/testdata/abitest/abi_test.c @@ -167,3 +167,204 @@ double stack_32_mixed_int_float( f9 * 25 + f10 * 26 + f11 * 27 + f12 * 28 + f13 * 29 + f14 * 30 + f15 * 31 + f16 * 32; } + +#if defined(_WIN32) && defined(__i386__) + +#include + +#define STDCALL __attribute__((stdcall)) + +static uint32_t win386_pointer_value = UINT32_C(0x89abcdef); + +uint64_t win386_direct_mixed( + int8_t i8, uint16_t u16, int32_t i32, uint64_t u64, + float f32, double f64, uintptr_t pointer, bool boolean, const char *string +) { + if (i8 != -101 || u16 != 60000 || i32 != INT32_C(-2000000000) || + u64 != UINT64_C(0xfedcba9876543210) || f32 != 1.25f || f64 != -2.5 || + pointer != (uintptr_t)&win386_pointer_value || !boolean || + strcmp(string, "purego-win386") != 0) { + return 0; + } + return u64; +} + +int64_t win386_return_int64(void) { + return INT64_C(-0x123456789abcdef); +} + +uint64_t win386_return_uint64(void) { + return UINT64_C(0xfedcba9876543210); +} + +float win386_return_float32(float value) { + return value * 1.5f; +} + +double win386_return_float64(double value) { + return value * -2.25; +} + +float win386_identity_float32(float value) { + return value; +} + +double win386_identity_float64(double value) { + return value; +} + +typedef struct { + float value; +} win386_one_float32; + +typedef struct { + double value; +} win386_one_float64; + +win386_one_float32 win386_identity_struct_float32(win386_one_float32 value) { + return value; +} + +win386_one_float64 win386_identity_struct_float64(win386_one_float64 value) { + return value; +} + +uint64_t win386_16_uint64( + uint64_t a1, uint64_t a2, uint64_t a3, uint64_t a4, + uint64_t a5, uint64_t a6, uint64_t a7, uint64_t a8, + uint64_t a9, uint64_t a10, uint64_t a11, uint64_t a12, + uint64_t a13, uint64_t a14, uint64_t a15, uint64_t a16 +) { + return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16; +} + +double win386_16_float64( + double a1, double a2, double a3, double a4, + double a5, double a6, double a7, double a8, + double a9, double a10, double a11, double a12, + double a13, double a14, double a15, double a16 +) { + return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16; +} + +uintptr_t win386_pointer(void) { + return (uintptr_t)&win386_pointer_value; +} + +typedef uint64_t (STDCALL *win386_stdcall_u64_callback)( + int8_t, uint16_t, int32_t, uint64_t, float, double, uint32_t *, bool +); +typedef uint64_t (*win386_cdecl_u64_callback)( + int8_t, uint16_t, int32_t, uint64_t, float, double, uint32_t *, bool +); +typedef int64_t (STDCALL *win386_stdcall_i64_callback)(int64_t, double); +typedef float (STDCALL *win386_stdcall_f32_callback)(uint64_t, float, double); +typedef double (*win386_cdecl_f64_callback)(uint64_t, float, double); +typedef uint64_t (STDCALL *win386_stdcall_16_u64_callback)( + uint64_t, uint64_t, uint64_t, uint64_t, + uint64_t, uint64_t, uint64_t, uint64_t, + uint64_t, uint64_t, uint64_t, uint64_t, + uint64_t, uint64_t, uint64_t, uint64_t +); +typedef float (STDCALL *win386_stdcall_identity_f32_callback)(float); +typedef double (*win386_cdecl_identity_f64_callback)(double); +typedef uint32_t (STDCALL *win386_stdcall_u32_callback)(uint32_t); +typedef uint64_t (STDCALL *win386_stdcall_reentrant_callback)(uint32_t); +typedef uint64_t (*win386_cdecl_reentrant_callback)(uint32_t); +typedef uint64_t (STDCALL *win386_stdcall_thread_callback)(uint64_t, float, double); + +uint64_t win386_call_stdcall_u64(uintptr_t callback) { + return ((win386_stdcall_u64_callback)callback)( + -101, 60000, INT32_C(-2000000000), UINT64_C(0xfedcba9876543210), + 1.25f, -2.5, &win386_pointer_value, true + ); +} + +uint64_t win386_call_cdecl_u64(uintptr_t callback) { + return ((win386_cdecl_u64_callback)callback)( + -101, 60000, INT32_C(-2000000000), UINT64_C(0xfedcba9876543210), + 1.25f, -2.5, &win386_pointer_value, true + ); +} + +int64_t win386_call_stdcall_i64(uintptr_t callback) { + return ((win386_stdcall_i64_callback)callback)(INT64_C(-0x123456789abcdef), 3.5); +} + +float win386_call_stdcall_f32(uintptr_t callback) { + return ((win386_stdcall_f32_callback)callback)(UINT64_C(0x123456789abcdef0), 1.25f, -2.5); +} + +double win386_call_cdecl_f64(uintptr_t callback) { + return ((win386_cdecl_f64_callback)callback)(UINT64_C(0x123456789abcdef0), 1.25f, -2.5); +} + +uint64_t win386_call_stdcall_16_u64(uintptr_t callback) { + return ((win386_stdcall_16_u64_callback)callback)( + UINT64_C(0x100000001), UINT64_C(0x100000002), UINT64_C(0x100000003), UINT64_C(0x100000004), + UINT64_C(0x100000005), UINT64_C(0x100000006), UINT64_C(0x100000007), UINT64_C(0x100000008), + UINT64_C(0x100000009), UINT64_C(0x10000000a), UINT64_C(0x10000000b), UINT64_C(0x10000000c), + UINT64_C(0x10000000d), UINT64_C(0x10000000e), UINT64_C(0x10000000f), UINT64_C(0x100000010) + ); +} + +float win386_call_stdcall_identity_f32(uintptr_t callback, float value) { + return ((win386_stdcall_identity_f32_callback)callback)(value); +} + +double win386_call_cdecl_identity_f64(uintptr_t callback, double value) { + return ((win386_cdecl_identity_f64_callback)callback)(value); +} + +uint32_t win386_call_stdcall_u32(uintptr_t callback, uint32_t value) { + return ((win386_stdcall_u32_callback)callback)(value); +} + +uint64_t win386_call_stdcall_reentrant(uintptr_t callback, uint32_t depth) { + return ((win386_stdcall_reentrant_callback)callback)(depth); +} + +uint64_t win386_call_cdecl_reentrant(uintptr_t callback, uint32_t depth) { + return ((win386_cdecl_reentrant_callback)callback)(depth); +} + +typedef struct { + win386_stdcall_thread_callback callback; + uint64_t result; +} win386_thread_call; + +static DWORD WINAPI win386_thread_entry(void *opaque) { + win386_thread_call *call = (win386_thread_call *)opaque; + call->result = call->callback(UINT64_C(0xfedcba9876543210), 1.25f, -2.5); + return 0; +} + +uint64_t win386_call_stdcall_on_thread(uintptr_t callback) { + win386_thread_call call = {(win386_stdcall_thread_callback)callback, 0}; + HANDLE thread = CreateThread(NULL, 0, win386_thread_entry, &call, 0, NULL); + if (thread == NULL) { + return 0; + } + if (WaitForSingleObject(thread, INFINITE) != WAIT_OBJECT_0) { + CloseHandle(thread); + return 0; + } + CloseHandle(thread); + return call.result; +} + +typedef struct { + uint32_t slots[32]; +} win386_struct_32_slots; + +uint32_t win386_take_struct_32_slots(win386_struct_32_slots value) { + uint32_t sum = 0; + for (size_t i = 0; i < 32; i++) { + sum += value.slots[i]; + } + return sum; +} + +#endif diff --git a/testdata/structtest/struct_test.c b/testdata/structtest/struct_test.c index b513b816..1de12d16 100644 --- a/testdata/structtest/struct_test.c +++ b/testdata/structtest/struct_test.c @@ -37,7 +37,8 @@ uint64_t GreaterThan16Bytes(struct GreaterThan16Bytes g) { // AfterRegisters tests to make sure that structs placed on the stack work properly uint64_t AfterRegisters(intptr_t a, intptr_t b, intptr_t c, intptr_t d, intptr_t e, intptr_t f, intptr_t g, intptr_t h, struct GreaterThan16Bytes bytes) { - intptr_t registers = a + b + c + d + e + f + g + h; + uint64_t registers = (uintptr_t)a + (uintptr_t)b + (uintptr_t)c + (uintptr_t)d + + (uintptr_t)e + (uintptr_t)f + (uintptr_t)g + (uintptr_t)h; int64_t stack = *bytes.x + *bytes.y + *bytes.z; if (registers != stack) { return 0xbadbad; diff --git a/wincallback.go b/wincallback.go index 86fb2a4d..f25bdabd 100644 --- a/wincallback.go +++ b/wincallback.go @@ -22,7 +22,7 @@ func genasm386() { buf.WriteString(`// Code generated by wincallback.go using 'go generate'. DO NOT EDIT. -//go:build linux +//go:build linux || windows // External code calls into callbackasm at an offset corresponding // to the callback index. Callbackasm is a table of MOVL and JMP instructions. diff --git a/windows_386_test.go b/windows_386_test.go new file mode 100644 index 00000000..65e9be50 --- /dev/null +++ b/windows_386_test.go @@ -0,0 +1,443 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +//go:build windows && 386 + +package purego_test + +import ( + "fmt" + "math" + "path/filepath" + "strings" + "sync" + "sync/atomic" + "testing" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/internal/load" +) + +const ( + win386Uint64 = uint64(0xfedcba9876543210) + win386Int64 = int64(-0x123456789abcdef) +) + +func openWin386ABILibrary(t *testing.T) uintptr { + t.Helper() + name := filepath.Join(t.TempDir(), "abitest.dll") + if err := buildSharedLib(t, "CC", name, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil { + t.Fatal(err) + } + library, err := load.OpenLibrary(name) + if err != nil { + t.Fatalf("OpenLibrary(%q): %v", name, err) + } + t.Cleanup(func() { + if err := load.CloseLibrary(library); err != nil { + t.Errorf("CloseLibrary(%q): %v", name, err) + } + }) + return library +} + +func TestWindows386RegisterFuncABI(t *testing.T) { + library := openWin386ABILibrary(t) + + var pointer func() uintptr + purego.RegisterLibFunc(&pointer, library, "win386_pointer") + + var mixed func(int8, uint16, int32, uint64, float32, float64, uintptr, bool, string) uint64 + purego.RegisterLibFunc(&mixed, library, "win386_direct_mixed") + if got := mixed(-101, 60000, -2000000000, win386Uint64, 1.25, -2.5, pointer(), true, "purego-win386"); got != win386Uint64 { + t.Fatalf("mixed arguments: got %#x, want %#x", got, win386Uint64) + } + + var returnInt64 func() int64 + var returnUint64 func() uint64 + var returnFloat32 func(float32) float32 + var returnFloat64 func(float64) float64 + purego.RegisterLibFunc(&returnInt64, library, "win386_return_int64") + purego.RegisterLibFunc(&returnUint64, library, "win386_return_uint64") + purego.RegisterLibFunc(&returnFloat32, library, "win386_return_float32") + purego.RegisterLibFunc(&returnFloat64, library, "win386_return_float64") + + if got := returnInt64(); got != win386Int64 { + t.Errorf("int64 return: got %#x, want %#x", got, win386Int64) + } + if got := returnUint64(); got != win386Uint64 { + t.Errorf("uint64 return: got %#x, want %#x", got, win386Uint64) + } + if got := returnFloat32(2.5); got != 3.75 { + t.Errorf("float32 return: got %v, want 3.75", got) + } + if got := returnFloat64(2.5); got != -5.625 { + t.Errorf("float64 return: got %v, want -5.625", got) + } + + var sixteenUint64 func( + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + ) uint64 + var sixteenFloat64 func( + float64, float64, float64, float64, float64, float64, float64, float64, + float64, float64, float64, float64, float64, float64, float64, float64, + ) float64 + purego.RegisterLibFunc(&sixteenUint64, library, "win386_16_uint64") + purego.RegisterLibFunc(&sixteenFloat64, library, "win386_16_float64") + if got := sixteenUint64( + 0x100000001, 0x100000002, 0x100000003, 0x100000004, + 0x100000005, 0x100000006, 0x100000007, 0x100000008, + 0x100000009, 0x10000000a, 0x10000000b, 0x10000000c, + 0x10000000d, 0x10000000e, 0x10000000f, 0x100000010, + ); got != 0x1000000088 { + t.Errorf("16 uint64 arguments: got %#x, want %#x", got, uint64(0x1000000088)) + } + if got := sixteenFloat64(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16); got != 136 { + t.Errorf("16 float64 arguments: got %v, want 136", got) + } +} + +func TestWindows386Callbacks(t *testing.T) { + library := openWin386ABILibrary(t) + + checkArgs := func(i8 int8, u16 uint16, i32 int32, u64 uint64, f32 float32, f64 float64, pointer *uint32, boolean bool) { + t.Helper() + if i8 != -101 || u16 != 60000 || i32 != -2000000000 || u64 != win386Uint64 || + f32 != 1.25 || f64 != -2.5 || pointer == nil || *pointer != 0x89abcdef || !boolean { + t.Errorf("callback arguments were decoded incorrectly: %d %d %d %#x %v %v %p %v", i8, u16, i32, u64, f32, f64, pointer, boolean) + } + } + + stdcall := purego.NewCallback(func(i8 int8, u16 uint16, i32 int32, u64 uint64, f32 float32, f64 float64, pointer *uint32, boolean bool) uint64 { + checkArgs(i8, u16, i32, u64, f32, f64, pointer, boolean) + return win386Uint64 + }) + cdecl := purego.NewCallback(func(_ purego.CDecl, i8 int8, u16 uint16, i32 int32, u64 uint64, f32 float32, f64 float64, pointer *uint32, boolean bool) uint64 { + checkArgs(i8, u16, i32, u64, f32, f64, pointer, boolean) + return win386Uint64 + }) + + var callStdcall func(uintptr) uint64 + var callCdecl func(uintptr) uint64 + purego.RegisterLibFunc(&callStdcall, library, "win386_call_stdcall_u64") + purego.RegisterLibFunc(&callCdecl, library, "win386_call_cdecl_u64") + for i := 0; i < 100; i++ { + if got := callStdcall(stdcall); got != win386Uint64 { + t.Fatalf("stdcall uint64 return %d: got %#x, want %#x", i, got, win386Uint64) + } + if got := callCdecl(cdecl); got != win386Uint64 { + t.Fatalf("cdecl uint64 return %d: got %#x, want %#x", i, got, win386Uint64) + } + } + + stdcallInt64 := purego.NewCallback(func(value int64, f64 float64) int64 { + if value != win386Int64 || f64 != 3.5 { + t.Errorf("signed callback arguments: got %#x, %v", value, f64) + } + return value + }) + stdcallFloat32 := purego.NewCallback(func(u64 uint64, f32 float32, f64 float64) float32 { + if u64 != 0x123456789abcdef0 || f32 != 1.25 || f64 != -2.5 { + t.Errorf("float32 callback arguments: got %#x, %v, %v", u64, f32, f64) + } + return 6.25 + }) + cdeclFloat64 := purego.NewCallback(func(_ purego.CDecl, u64 uint64, f32 float32, f64 float64) float64 { + if u64 != 0x123456789abcdef0 || f32 != 1.25 || f64 != -2.5 { + t.Errorf("float64 callback arguments: got %#x, %v, %v", u64, f32, f64) + } + return -9.5 + }) + + var callInt64 func(uintptr) int64 + var callFloat32 func(uintptr) float32 + var callFloat64 func(uintptr) float64 + purego.RegisterLibFunc(&callInt64, library, "win386_call_stdcall_i64") + purego.RegisterLibFunc(&callFloat32, library, "win386_call_stdcall_f32") + purego.RegisterLibFunc(&callFloat64, library, "win386_call_cdecl_f64") + if got := callInt64(stdcallInt64); got != win386Int64 { + t.Errorf("callback int64 return: got %#x, want %#x", got, win386Int64) + } + if got := callFloat32(stdcallFloat32); got != 6.25 { + t.Errorf("callback float32 return: got %v, want 6.25", got) + } + if got := callFloat64(cdeclFloat64); got != -9.5 { + t.Errorf("callback float64 return: got %v, want -9.5", got) + } + + stdcall16Uint64 := purego.NewCallback(func( + a1, a2, a3, a4, a5, a6, a7, a8, + a9, a10, a11, a12, a13, a14, a15, a16 uint64, + ) uint64 { + return a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + }) + var call16Uint64 func(uintptr) uint64 + purego.RegisterLibFunc(&call16Uint64, library, "win386_call_stdcall_16_u64") + if got := call16Uint64(stdcall16Uint64); got != 0x1000000088 { + t.Errorf("callback 16 uint64 arguments: got %#x, want %#x", got, uint64(0x1000000088)) + } +} + +func TestWindows386SlotLimit(t *testing.T) { + t.Run("direct", func(t *testing.T) { + var sixteenInt64 func( + int64, int64, int64, int64, int64, int64, int64, int64, + int64, int64, int64, int64, int64, int64, int64, int64, + ) + purego.RegisterFunc(&sixteenInt64, 1) + + mustPanicWin386(t, "too many stack arguments", func() { + var seventeenInt64 func( + int64, int64, int64, int64, int64, int64, int64, int64, + int64, int64, int64, int64, int64, int64, int64, int64, + int64, + ) + purego.RegisterFunc(&seventeenInt64, 1) + }) + }) + + t.Run("callback", func(t *testing.T) { + purego.NewCallback(func( + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + ) { + }) + + mustPanicWin386(t, "too many callback argument slots", func() { + purego.NewCallback(func( + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + uint64, uint64, uint64, uint64, uint64, uint64, uint64, uint64, + uint64, + ) { + }) + }) + mustPanicWin386(t, "unsupported argument type: struct", func() { + purego.NewCallback(func(struct{ Value uint32 }) {}) + }) + mustPanicWin386(t, "function must not be nil", func() { + var nilCallback func(uint32) uint32 + purego.NewCallback(nilCallback) + }) + }) +} + +func mustPanicWin386(t *testing.T, contains string, fn func()) { + t.Helper() + defer func() { + panicValue := recover() + if panicValue == nil || !strings.Contains(fmt.Sprint(panicValue), contains) { + t.Fatalf("got panic %v, want text containing %q", panicValue, contains) + } + }() + fn() +} + +func TestWindows386FullSlotCalls(t *testing.T) { + library := openWin386ABILibrary(t) + + var thirtyTwo func( + uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, + uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, + uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, + uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, + ) uintptr + purego.RegisterLibFunc(&thirtyTwo, library, "stack_32_uintptr") + if got := thirtyTwo( + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + ); got != 528 { + t.Fatalf("32 scalar slots: got %d, want 528", got) + } + + type slots32 struct{ Slots [32]uint32 } + var takeStruct func(slots32) uint32 + purego.RegisterLibFunc(&takeStruct, library, "win386_take_struct_32_slots") + var value slots32 + for i := range value.Slots { + value.Slots[i] = uint32(i + 1) + } + if got := takeStruct(value); got != 528 { + t.Fatalf("32-slot struct: got %d, want 528", got) + } + + mustPanicWin386(t, "too many stack arguments", func() { + type slots33 struct{ Slots [33]uint32 } + var tooLarge func(slots33) + purego.RegisterFunc(&tooLarge, 1) + }) +} + +func TestWindows386X87SpecialValues(t *testing.T) { + library := openWin386ABILibrary(t) + + var identity32 func(float32) float32 + var identity64 func(float64) float64 + var integer func() uint64 + purego.RegisterLibFunc(&identity32, library, "win386_identity_float32") + purego.RegisterLibFunc(&identity64, library, "win386_identity_float64") + purego.RegisterLibFunc(&integer, library, "win386_return_uint64") + + callback32 := purego.NewCallback(func(value float32) float32 { return value }) + callback64 := purego.NewCallback(func(_ purego.CDecl, value float64) float64 { return value }) + var callCallback32 func(uintptr, float32) float32 + var callCallback64 func(uintptr, float64) float64 + purego.RegisterLibFunc(&callCallback32, library, "win386_call_stdcall_identity_f32") + purego.RegisterLibFunc(&callCallback64, library, "win386_call_cdecl_identity_f64") + + float32Values := []float32{ + float32(math.Copysign(0, -1)), + float32(math.Inf(1)), + float32(math.Inf(-1)), + math.SmallestNonzeroFloat32, + math.Float32frombits(0x7fc12345), + } + float64Values := []float64{ + math.Copysign(0, -1), + math.Inf(1), + math.Inf(-1), + math.SmallestNonzeroFloat64, + math.Float64frombits(0x7ff8123456789abc), + } + for i := range 2000 { + value32 := float32Values[i%len(float32Values)] + checkFloat32Win386(t, "direct", value32, identity32(value32)) + checkFloat32Win386(t, "callback", value32, callCallback32(callback32, value32)) + + value64 := float64Values[i%len(float64Values)] + checkFloat64Win386(t, "direct", value64, identity64(value64)) + checkFloat64Win386(t, "callback", value64, callCallback64(callback64, value64)) + if got := integer(); got != win386Uint64 { + t.Fatalf("integer call after x87 call %d: got %#x", i, got) + } + } + + type oneFloat32 struct{ Value float32 } + type oneFloat64 struct{ Value float64 } + var struct32 func(oneFloat32) oneFloat32 + var struct64 func(oneFloat64) oneFloat64 + purego.RegisterLibFunc(&struct32, library, "win386_identity_struct_float32") + purego.RegisterLibFunc(&struct64, library, "win386_identity_struct_float64") + for _, value := range float32Values { + checkFloat32Win386(t, "struct", value, struct32(oneFloat32{value}).Value) + } + for _, value := range float64Values { + checkFloat64Win386(t, "struct", value, struct64(oneFloat64{value}).Value) + } +} + +func checkFloat32Win386(t *testing.T, path string, want, got float32) { + t.Helper() + if math.IsNaN(float64(want)) { + if !math.IsNaN(float64(got)) { + t.Fatalf("%s float32: got %v, want NaN", path, got) + } + return + } + if math.Float32bits(got) != math.Float32bits(want) { + t.Fatalf("%s float32: got %#08x, want %#08x", path, math.Float32bits(got), math.Float32bits(want)) + } +} + +func checkFloat64Win386(t *testing.T, path string, want, got float64) { + t.Helper() + if math.IsNaN(want) { + if !math.IsNaN(got) { + t.Fatalf("%s float64: got %v, want NaN", path, got) + } + return + } + if math.Float64bits(got) != math.Float64bits(want) { + t.Fatalf("%s float64: got %#016x, want %#016x", path, math.Float64bits(got), math.Float64bits(want)) + } +} + +func TestWindows386CallbackConcurrencyAndReentrancy(t *testing.T) { + library := openWin386ABILibrary(t) + + var callOne func(uintptr, uint32) uint32 + purego.RegisterLibFunc(&callOne, library, "win386_call_stdcall_u32") + var calls atomic.Uint64 + callback := purego.NewCallback(func(value uint32) uint32 { + calls.Add(1) + return value ^ 0xa5a5a5a5 + }) + var failed atomic.Bool + var wg sync.WaitGroup + for goroutine := range 8 { + wg.Add(1) + go func() { + defer wg.Done() + for iteration := range 250 { + value := uint32(goroutine<<16 | iteration) + if got := callOne(callback, value); got != value^0xa5a5a5a5 { + failed.Store(true) + return + } + } + }() + } + wg.Wait() + if failed.Load() || calls.Load() != 2000 { + t.Fatalf("concurrent callback calls: failed=%v calls=%d, want 2000", failed.Load(), calls.Load()) + } + + var callStdcall func(uintptr, uint32) uint64 + var stdcall uintptr + purego.RegisterLibFunc(&callStdcall, library, "win386_call_stdcall_reentrant") + stdcall = purego.NewCallback(func(depth uint32) uint64 { + if depth == 0 { + return 1 + } + return uint64(depth) + callStdcall(stdcall, depth-1) + }) + if got, want := callStdcall(stdcall, 12), uint64(79); got != want { + t.Fatalf("stdcall reentrant callback: got %d, want %d", got, want) + } + + var callCdecl func(uintptr, uint32) uint64 + var cdecl uintptr + purego.RegisterLibFunc(&callCdecl, library, "win386_call_cdecl_reentrant") + cdecl = purego.NewCallback(func(_ purego.CDecl, depth uint32) uint64 { + if depth == 0 { + return 1 + } + return uint64(depth) + callCdecl(cdecl, depth-1) + }) + if got, want := callCdecl(cdecl, 12), uint64(79); got != want { + t.Fatalf("cdecl reentrant callback: got %d, want %d", got, want) + } +} + +func TestWindows386CallbackFromNativeThread(t *testing.T) { + library := openWin386ABILibrary(t) + + var badArgs atomic.Bool + callback := purego.NewCallback(func(value uint64, f32 float32, f64 float64) uint64 { + if value != win386Uint64 || f32 != 1.25 || f64 != -2.5 { + badArgs.Store(true) + return 0 + } + return value + }) + var callOnThread func(uintptr) uint64 + purego.RegisterLibFunc(&callOnThread, library, "win386_call_stdcall_on_thread") + if got := callOnThread(callback); got != win386Uint64 || badArgs.Load() { + t.Fatalf("native-thread callback: got %#x badArgs=%v", got, badArgs.Load()) + } +} + +func TestWindows386CallbackTableBeyond32(t *testing.T) { + library := openWin386ABILibrary(t) + var last uintptr + for i := range 40 { + add := uint32(i) + last = purego.NewCallback(func(value uint32) uint32 { return value + add }) + } + var call func(uintptr, uint32) uint32 + purego.RegisterLibFunc(&call, library, "win386_call_stdcall_u32") + if got := call(last, 100); got != 139 { + t.Fatalf("callback table entry beyond 32: got %d, want 139", got) + } +} diff --git a/zcallback_386.s b/zcallback_386.s index bd2d9c85..d2849169 100644 --- a/zcallback_386.s +++ b/zcallback_386.s @@ -1,6 +1,6 @@ // Code generated by wincallback.go using 'go generate'. DO NOT EDIT. -//go:build linux +//go:build linux || windows // External code calls into callbackasm at an offset corresponding // to the callback index. Callbackasm is a table of MOVL and JMP instructions. From e84a25fefaeabaa71037eb1218bf6a1aacdb2ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 13 Jul 2026 16:46:13 +0800 Subject: [PATCH 2/4] Fix Windows 386 CI compiler environment --- .github/workflows/test.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4b419b9..3e9a0836 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,9 +35,18 @@ jobs: if: runner.os == 'Windows' && runner.arch != 'ARM64' uses: msys2/setup-msys2@v2 with: + msystem: MINGW32 install: mingw-w64-i686-gcc path-type: inherit + - name: Verify Windows 386 compiler + if: runner.os == 'Windows' && runner.arch != 'ARM64' + shell: msys2 {0} + run: | + gcc --version + printf 'int purego_ci_probe(void) { return 0; }\n' | + gcc -shared -Wall -Werror -fPIC -m32 -x c - -o /tmp/purego-ci-probe.dll + - name: go vet run: | env CGO_ENABLED=0 go vet -v ./... @@ -130,7 +139,7 @@ jobs: if: runner.os == 'Windows' && runner.arch != 'ARM64' shell: msys2 {0} run: | - CC="$(cygpath -w /mingw32/bin/gcc.exe)" + CC="$(cygpath -w "$(command -v gcc)")" env CGO_ENABLED=0 GOARCH=386 CC="$CC" go test -shuffle=on -v -count=10 ./... env CGO_ENABLED=0 GOARCH=386 CC="$CC" go test "-gcflags=all=-N -l" -v ./... @@ -138,7 +147,7 @@ jobs: if: runner.os == 'Windows' && runner.arch != 'ARM64' shell: msys2 {0} run: | - CC="$(cygpath -w /mingw32/bin/gcc.exe)" + CC="$(cygpath -w "$(command -v gcc)")" env CGO_ENABLED=1 GOARCH=386 CC="$CC" go test -shuffle=on -v -count=10 ./... env CGO_ENABLED=1 GOARCH=386 CC="$CC" go test "-gcflags=all=-N -l" -v ./... From 2b122b9e7618c309a7d344cc073a53a6f5b5729f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 15 Jul 2026 14:02:31 +0800 Subject: [PATCH 3/4] Remove redundant build constraints from Windows 386 files --- callback_windows_386.go | 2 -- struct_windows_386.go | 2 -- sys_windows_386.s | 2 -- 3 files changed, 6 deletions(-) diff --git a/callback_windows_386.go b/callback_windows_386.go index 32554143..8b0e8d11 100644 --- a/callback_windows_386.go +++ b/callback_windows_386.go @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors -//go:build windows && 386 - package purego import ( diff --git a/struct_windows_386.go b/struct_windows_386.go index 14251063..d62d85aa 100644 --- a/struct_windows_386.go +++ b/struct_windows_386.go @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors -//go:build windows && 386 - package purego import ( diff --git a/sys_windows_386.s b/sys_windows_386.s index 866cad85..fea0f84b 100644 --- a/sys_windows_386.s +++ b/sys_windows_386.s @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2026 The Ebitengine Authors -//go:build windows && 386 - #include "textflag.h" // callbackasm1 receives an index in CX from the callbackasm table. It calls a From c7fac17bec3f9a34f4656a0609ab36fe2896b28f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 21 Jul 2026 19:02:39 +0800 Subject: [PATCH 4/4] Rename overloaded arm64_r8 local to arch-neutral aux --- func.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/func.go b/func.go index 1573753f..77955e1a 100644 --- a/func.go +++ b/func.go @@ -302,12 +302,15 @@ func RegisterFunc(fptr any, cfn uintptr) { runtime.KeepAlive(args) }() - var arm64_r8 uintptr + // aux is passed out-of-band to the assembly bridge: on arm64 it is the + // hidden struct-return pointer placed in R8, and on 386 it selects the + // x87 float-return mode. + var aux uintptr if runtime.GOARCH == "386" && ty.NumOut() == 1 && (ty.Out(0).Kind() == reflect.Float32 || ty.Out(0).Kind() == reflect.Float64) { // The 386 bridge uses this to avoid popping x87 ST(0) after // non-floating calls. - arm64_r8 = 1 + aux = 1 } if ty.NumOut() == 1 && ty.Out(0).Kind() == reflect.Struct { outType := ty.Out(0) @@ -316,7 +319,7 @@ func RegisterFunc(fptr any, cfn uintptr) { // only field is a float: it returns the value in x87 ST(0). // MSVC returns the same struct in EAX/EDX, so mode 2 asks the // assembly bridge to detect which form the callee used. - arm64_r8 = 2 + aux = 2 } if structReturnInMemory(outType) { // The caller allocates the return value and passes its pointer @@ -329,7 +332,7 @@ func RegisterFunc(fptr any, cfn uintptr) { if !isAllFloats || numFields > 4 { val := reflect.New(outType) keepAlive = append(keepAlive, val) - arm64_r8 = val.Pointer() + aux = val.Pointer() } } } @@ -364,7 +367,7 @@ func RegisterFunc(fptr any, cfn uintptr) { syscall.a1, syscall.a2, _ = syscall_syscallN(cfn, sysargs[:numStack]...) syscall.f1 = syscall.a2 // on amd64 a2 stores the float return } else { - syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], arm64_r8) + syscall = syscall_SyscallN(cfn, sysargs[:], floats[:], aux) } defer thePool.Put(syscall) if ty.NumOut() == 0 {