detached 8d8a729

Merge branch 'fix/abi-correctness'

commit
8d8a729c8d4f3df5df6dc0f03f256cdd7fadf56b
author
brice · · 2026-04-13 12:30 UTC
merge of
7850e38 + 5aa9aab · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
16 files · +436 -88
A struct_abi_helpers.go +38 -0
@@ -0,0 +1,38 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "math"
8+ "reflect"
9+ "unsafe"
10+)
11+
12+func packFloat32Pair(low, high uintptr) uintptr {
13+ return uintptr((uint64(high) << 32) | (uint64(low) & math.MaxUint32))
14+}
15+
16+func highFloat32Bits(word uintptr) uint32 {
17+ return uint32(uint64(word) >> 32)
18+}
19+
20+func flushPackedWord(val *uint64, shift *byte, flushed *bool, isFloat bool, addFloat, addInt func(uintptr)) {
21+ *flushed = true
22+ if isFloat {
23+ addFloat(uintptr(*val))
24+ } else {
25+ addInt(uintptr(*val))
26+ }
27+ *val = 0
28+ *shift = 0
29+}
30+
31+func stableValuePointer(v reflect.Value) (unsafe.Pointer, any) {
32+ if v.CanAddr() {
33+ return v.Addr().UnsafePointer(), nil
34+ }
35+ tmp := reflect.New(v.Type())
36+ tmp.Elem().Set(v)
37+ return tmp.UnsafePointer(), tmp.Interface()
38+}
@@ -0,0 +1,38 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "math"
8+ "reflect"
9+ "unsafe"
10+)
11+
12+func packFloat32Pair(low, high uintptr) uintptr {
13+ return uintptr((uint64(high) << 32) | (uint64(low) & math.MaxUint32))
14+}
15+
16+func highFloat32Bits(word uintptr) uint32 {
17+ return uint32(uint64(word) >> 32)
18+}
19+
20+func flushPackedWord(val *uint64, shift *byte, flushed *bool, isFloat bool, addFloat, addInt func(uintptr)) {
21+ *flushed = true
22+ if isFloat {
23+ addFloat(uintptr(*val))
24+ } else {
25+ addInt(uintptr(*val))
26+ }
27+ *val = 0
28+ *shift = 0
29+}
30+
31+func stableValuePointer(v reflect.Value) (unsafe.Pointer, any) {
32+ if v.CanAddr() {
33+ return v.Addr().UnsafePointer(), nil
34+ }
35+ tmp := reflect.New(v.Type())
36+ tmp.Elem().Set(v)
37+ return tmp.UnsafePointer(), tmp.Interface()
38+}
A struct_abi_helpers_test.go +105 -0
@@ -0,0 +1,105 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "math"
8+ "reflect"
9+ "testing"
10+)
11+
12+func TestPackFloat32PairMasksLowWord(t *testing.T) {
13+ low := uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5)))
14+ high := uintptr(math.Float32bits(2.5))
15+ want := uintptr(uint64(math.Float32bits(2.5))<<32 | uint64(math.Float32bits(1.5)))
16+ if got := packFloat32Pair(low, high); got != want {
17+ t.Fatalf("packFloat32Pair() = %#x, want %#x", got, want)
18+ }
19+}
20+
21+func TestHighFloat32Bits(t *testing.T) {
22+ word := uintptr(uint64(math.Float32bits(3.25)) << 32)
23+ if got, want := highFloat32Bits(word), math.Float32bits(3.25); got != want {
24+ t.Fatalf("highFloat32Bits() = %#x, want %#x", got, want)
25+ }
26+}
27+
28+func TestFlushPackedWordResetsState(t *testing.T) {
29+ var val uint64 = 0x11223344
30+ shift := byte(32)
31+ flushed := false
32+ var gotFloat uintptr
33+ flushPackedWord(&val, &shift, &flushed, true, func(v uintptr) { gotFloat = v }, func(uintptr) {
34+ t.Fatal("unexpected integer flush")
35+ })
36+ if !flushed || val != 0 || shift != 0 {
37+ t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
38+ }
39+ if gotFloat != 0x11223344 {
40+ t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotFloat, uintptr(0x11223344))
41+ }
42+}
43+
44+func TestFlushPackedWordResetsStateForIntegerBranch(t *testing.T) {
45+ var val uint64 = 0x55667788
46+ shift := byte(16)
47+ flushed := false
48+ var gotInt uintptr
49+ flushPackedWord(&val, &shift, &flushed, false, func(uintptr) {
50+ t.Fatal("unexpected float flush")
51+ }, func(v uintptr) { gotInt = v })
52+ if !flushed || val != 0 || shift != 0 {
53+ t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
54+ }
55+ if gotInt != 0x55667788 {
56+ t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotInt, uintptr(0x55667788))
57+ }
58+}
59+
60+func TestStableValuePointerCopiesUnaddressableValue(t *testing.T) {
61+ type sample struct{ X uint32 }
62+ ptr, keepAlive := stableValuePointer(reflect.ValueOf(sample{X: 7}))
63+ if keepAlive == nil {
64+ t.Fatal("expected keepAlive copy for unaddressable value")
65+ }
66+ if got := *(*uint32)(ptr); got != 7 {
67+ t.Fatalf("stableValuePointer() copied %#x, want %#x", got, uint32(7))
68+ }
69+}
70+
71+func TestStableValuePointerUsesAddressableValue(t *testing.T) {
72+ type sample struct{ X uint32 }
73+ value := sample{X: 9}
74+ ptr, keepAlive := stableValuePointer(reflect.ValueOf(&value).Elem())
75+ if keepAlive != nil {
76+ t.Fatal("expected nil keepAlive for addressable value")
77+ }
78+ if got := *(*uint32)(ptr); got != 9 {
79+ t.Fatalf("stableValuePointer() pointed to %#x, want %#x", got, uint32(9))
80+ }
81+}
82+
83+func TestIsAllSameFloatRejectsNestedMixedStruct(t *testing.T) {
84+ type inner struct{ X, Y float32 }
85+ type mixed struct {
86+ Inner inner
87+ Tail int32
88+ }
89+ allFloats, fields := isAllSameFloat(reflect.TypeOf(mixed{}))
90+ if allFloats {
91+ t.Fatalf("isAllSameFloat() = true for mixed nested struct with %d fields", fields)
92+ }
93+}
94+
95+func TestIsAllSameFloatAcceptsNestedFloatStruct(t *testing.T) {
96+ type inner struct{ X, Y float32 }
97+ type outer struct {
98+ Left inner
99+ Right inner
100+ }
101+ allFloats, fields := isAllSameFloat(reflect.TypeOf(outer{}))
102+ if !allFloats || fields != 4 {
103+ t.Fatalf("isAllSameFloat() = (%v, %d), want (true, 4)", allFloats, fields)
104+ }
105+}
@@ -0,0 +1,105 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package purego
5+
6+import (
7+ "math"
8+ "reflect"
9+ "testing"
10+)
11+
12+func TestPackFloat32PairMasksLowWord(t *testing.T) {
13+ low := uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5)))
14+ high := uintptr(math.Float32bits(2.5))
15+ want := uintptr(uint64(math.Float32bits(2.5))<<32 | uint64(math.Float32bits(1.5)))
16+ if got := packFloat32Pair(low, high); got != want {
17+ t.Fatalf("packFloat32Pair() = %#x, want %#x", got, want)
18+ }
19+}
20+
21+func TestHighFloat32Bits(t *testing.T) {
22+ word := uintptr(uint64(math.Float32bits(3.25)) << 32)
23+ if got, want := highFloat32Bits(word), math.Float32bits(3.25); got != want {
24+ t.Fatalf("highFloat32Bits() = %#x, want %#x", got, want)
25+ }
26+}
27+
28+func TestFlushPackedWordResetsState(t *testing.T) {
29+ var val uint64 = 0x11223344
30+ shift := byte(32)
31+ flushed := false
32+ var gotFloat uintptr
33+ flushPackedWord(&val, &shift, &flushed, true, func(v uintptr) { gotFloat = v }, func(uintptr) {
34+ t.Fatal("unexpected integer flush")
35+ })
36+ if !flushed || val != 0 || shift != 0 {
37+ t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
38+ }
39+ if gotFloat != 0x11223344 {
40+ t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotFloat, uintptr(0x11223344))
41+ }
42+}
43+
44+func TestFlushPackedWordResetsStateForIntegerBranch(t *testing.T) {
45+ var val uint64 = 0x55667788
46+ shift := byte(16)
47+ flushed := false
48+ var gotInt uintptr
49+ flushPackedWord(&val, &shift, &flushed, false, func(uintptr) {
50+ t.Fatal("unexpected float flush")
51+ }, func(v uintptr) { gotInt = v })
52+ if !flushed || val != 0 || shift != 0 {
53+ t.Fatalf("flushPackedWord() left state dirty: flushed=%v val=%#x shift=%d", flushed, val, shift)
54+ }
55+ if gotInt != 0x55667788 {
56+ t.Fatalf("flushPackedWord() flushed %#x, want %#x", gotInt, uintptr(0x55667788))
57+ }
58+}
59+
60+func TestStableValuePointerCopiesUnaddressableValue(t *testing.T) {
61+ type sample struct{ X uint32 }
62+ ptr, keepAlive := stableValuePointer(reflect.ValueOf(sample{X: 7}))
63+ if keepAlive == nil {
64+ t.Fatal("expected keepAlive copy for unaddressable value")
65+ }
66+ if got := *(*uint32)(ptr); got != 7 {
67+ t.Fatalf("stableValuePointer() copied %#x, want %#x", got, uint32(7))
68+ }
69+}
70+
71+func TestStableValuePointerUsesAddressableValue(t *testing.T) {
72+ type sample struct{ X uint32 }
73+ value := sample{X: 9}
74+ ptr, keepAlive := stableValuePointer(reflect.ValueOf(&value).Elem())
75+ if keepAlive != nil {
76+ t.Fatal("expected nil keepAlive for addressable value")
77+ }
78+ if got := *(*uint32)(ptr); got != 9 {
79+ t.Fatalf("stableValuePointer() pointed to %#x, want %#x", got, uint32(9))
80+ }
81+}
82+
83+func TestIsAllSameFloatRejectsNestedMixedStruct(t *testing.T) {
84+ type inner struct{ X, Y float32 }
85+ type mixed struct {
86+ Inner inner
87+ Tail int32
88+ }
89+ allFloats, fields := isAllSameFloat(reflect.TypeOf(mixed{}))
90+ if allFloats {
91+ t.Fatalf("isAllSameFloat() = true for mixed nested struct with %d fields", fields)
92+ }
93+}
94+
95+func TestIsAllSameFloatAcceptsNestedFloatStruct(t *testing.T) {
96+ type inner struct{ X, Y float32 }
97+ type outer struct {
98+ Left inner
99+ Right inner
100+ }
101+ allFloats, fields := isAllSameFloat(reflect.TypeOf(outer{}))
102+ if !allFloats || fields != 4 {
103+ t.Fatalf("isAllSameFloat() = (%v, %d), want (true, 4)", allFloats, fields)
104+ }
105+}
M struct_amd64.go +22 -27
@@ -23,35 +23,30 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
2323 // up to 8 bytes is returned in RAX
2424 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
2525 case outSize <= 16:
26- r1, r2 := syscall.a1, syscall.a2
27- if isAllFloats(outType) {
28- r1 = syscall.f1
29- r2 = syscall.f2
30- } else {
31- // check first 8 bytes if it's floats
32- hasFirstFloat := false
33- f1 := outType.Field(0).Type
34- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && outType.Field(1).Type.Kind() == reflect.Float32 {
35- r1 = syscall.f1
36- hasFirstFloat = true
37- }
38-
39- // find index of the field that starts the second 8 bytes
40- var i int
41- for i = 0; i < outType.NumField(); i++ {
42- if outType.Field(i).Offset == 8 {
43- break
26+ var r1, r2 uintptr
27+ var intRegs, floatRegs int
28+ for i := 0; i < int((outSize+7)/8); i++ {
29+ class := classifyEightbyte(outType, uintptr(i)*8, uintptr(i)*8+8)
30+ var reg uintptr
31+ if class == _SSE {
32+ if floatRegs == 0 {
33+ reg = syscall.f1
34+ } else {
35+ reg = syscall.f2
4436 }
37+ floatRegs++
38+ } else {
39+ if intRegs == 0 {
40+ reg = syscall.a1
41+ } else {
42+ reg = syscall.a2
43+ }
44+ intRegs++
4545 }
46-
47- // check last 8 bytes if they are floats
48- f1 = outType.Field(i).Type
49- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == outType.NumField() {
50- r2 = syscall.f1
51- } else if hasFirstFloat {
52- // if the first field was a float then that means the second integer field
53- // comes from the first integer register
54- r2 = syscall.a1
46+ if i == 0 {
47+ r1 = reg
48+ } else {
49+ r2 = reg
5550 }
5651 }
5752 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
@@ -23,35 +23,30 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
23 // up to 8 bytes is returned in RAX23 // up to 8 bytes is returned in RAX
24 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()24 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{syscall.a1})).Elem()
25 case outSize <= 16:25 case outSize <= 16:
26- r1, r2 := syscall.a1, syscall.a226+ var r1, r2 uintptr
27- if isAllFloats(outType) {27+ var intRegs, floatRegs int
28- r1 = syscall.f128+ for i := 0; i < int((outSize+7)/8); i++ {
29- r2 = syscall.f229+ class := classifyEightbyte(outType, uintptr(i)*8, uintptr(i)*8+8)
30- } else {30+ var reg uintptr
31- // check first 8 bytes if it's floats31+ if class == _SSE {
32- hasFirstFloat := false32+ if floatRegs == 0 {
33- f1 := outType.Field(0).Type33+ reg = syscall.f1
34- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && outType.Field(1).Type.Kind() == reflect.Float32 {34+ } else {
35- r1 = syscall.f135+ reg = syscall.f2
36- hasFirstFloat = true
37- }
38-
39- // find index of the field that starts the second 8 bytes
40- var i int
41- for i = 0; i < outType.NumField(); i++ {
42- if outType.Field(i).Offset == 8 {
43- break
44 }36 }
37+ floatRegs++
38+ } else {
39+ if intRegs == 0 {
40+ reg = syscall.a1
41+ } else {
42+ reg = syscall.a2
43+ }
44+ intRegs++
45 }45 }
46-46+ if i == 0 {
47- // check last 8 bytes if they are floats47+ r1 = reg
48- f1 = outType.Field(i).Type48+ } else {
49- if f1.Kind() == reflect.Float64 || f1.Kind() == reflect.Float32 && i+1 == outType.NumField() {49+ r2 = reg
50- r2 = syscall.f1
51- } else if hasFirstFloat {
52- // if the first field was a float then that means the second integer field
53- // comes from the first integer register
54- r2 = syscall.a1
55 }50 }
56 }51 }
57 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()52 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
A struct_amd64_regression_test.go +21 -0
@@ -0,0 +1,21 @@
1+//go:build amd64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestGetStructHandlesSecondEightbyteWithoutOffset8Field(t *testing.T) {
11+ type awkward struct {
12+ A [3]uint32
13+ B uint32
14+ }
15+ sys := syscall15Args{a1: 0x0000000200000001, a2: 0x0000000400000003}
16+ got := getStruct(reflect.TypeOf(awkward{}), sys).Interface().(awkward)
17+ want := awkward{A: [3]uint32{1, 2, 3}, B: 4}
18+ if got != want {
19+ t.Fatalf("got %+v, want %+v", got, want)
20+ }
21+}
@@ -0,0 +1,21 @@
1+//go:build amd64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestGetStructHandlesSecondEightbyteWithoutOffset8Field(t *testing.T) {
11+ type awkward struct {
12+ A [3]uint32
13+ B uint32
14+ }
15+ sys := syscall15Args{a1: 0x0000000200000001, a2: 0x0000000400000003}
16+ got := getStruct(reflect.TypeOf(awkward{}), sys).Interface().(awkward)
17+ want := awkward{A: [3]uint32{1, 2, 3}, B: 4}
18+ if got != want {
19+ t.Fatalf("got %+v, want %+v", got, want)
20+ }
21+}
M struct_arm.go +8 -3
@@ -5,6 +5,7 @@ package purego
55
66 import (
77 "reflect"
8+ "runtime"
89 "unsafe"
910 )
1011
@@ -16,8 +17,10 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
1617
1718 // TODO: ARM EABI: small structs are passed in registers or on stack
1819 // For simplicity, pass by pointer for now
19- ptr := v.Addr().UnsafePointer()
20- keepAlive = append(keepAlive, ptr)
20+ ptr, live := stableValuePointer(v)
21+ if live != nil {
22+ keepAlive = append(keepAlive, live)
23+ }
2124 if *numInts < 4 {
2225 addInt(uintptr(ptr))
2326 *numInts++
@@ -25,6 +28,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
2528 addStack(uintptr(ptr))
2629 *numStack++
2730 }
31+ runtime.KeepAlive(live)
2832 return keepAlive
2933 }
3034
@@ -52,13 +56,14 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
5256 if size == 0 {
5357 return
5458 }
55- ptr := unsafe.Pointer(v.UnsafeAddr())
59+ ptr, live := stableValuePointer(v)
5660 if size <= 4 {
5761 addInt(*(*uintptr)(ptr))
5862 } else if size <= 8 {
5963 addInt(*(*uintptr)(ptr))
6064 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
6165 }
66+ runtime.KeepAlive(live)
6267 }
6368
6469 // shouldBundleStackArgs always returns false on arm
@@ -5,6 +5,7 @@ package purego
5 5
6 import (6 import (
7 "reflect"7 "reflect"
8+ "runtime"
8 "unsafe"9 "unsafe"
9 )10 )
10 11
@@ -16,8 +17,10 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
16 17
17 // TODO: ARM EABI: small structs are passed in registers or on stack18 // TODO: ARM EABI: small structs are passed in registers or on stack
18 // For simplicity, pass by pointer for now19 // For simplicity, pass by pointer for now
19- ptr := v.Addr().UnsafePointer()20+ ptr, live := stableValuePointer(v)
20- keepAlive = append(keepAlive, ptr)21+ if live != nil {
22+ keepAlive = append(keepAlive, live)
23+ }
21 if *numInts < 4 {24 if *numInts < 4 {
22 addInt(uintptr(ptr))25 addInt(uintptr(ptr))
23 *numInts++26 *numInts++
@@ -25,6 +28,7 @@ func addStruct(v reflect.Value, numInts, numFloats, numStack *int, addInt, addFl
25 addStack(uintptr(ptr))28 addStack(uintptr(ptr))
26 *numStack++29 *numStack++
27 }30 }
31+ runtime.KeepAlive(live)
28 return keepAlive32 return keepAlive
29 }33 }
30 34
@@ -52,13 +56,14 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
52 if size == 0 {56 if size == 0 {
53 return57 return
54 }58 }
55- ptr := unsafe.Pointer(v.UnsafeAddr())59+ ptr, live := stableValuePointer(v)
56 if size <= 4 {60 if size <= 4 {
57 addInt(*(*uintptr)(ptr))61 addInt(*(*uintptr)(ptr))
58 } else if size <= 8 {62 } else if size <= 8 {
59 addInt(*(*uintptr)(ptr))63 addInt(*(*uintptr)(ptr))
60 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))64 addInt(*(*uintptr)(unsafe.Add(ptr, 4)))
61 }65 }
66+ runtime.KeepAlive(live)
62 }67 }
63 68
64 // shouldBundleStackArgs always returns false on arm69 // shouldBundleStackArgs always returns false on arm
M struct_arm64.go +37 -23
@@ -229,38 +229,52 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
229229 //
230230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231231 func isHFA(t reflect.Type) bool {
232- // round up struct size to nearest 8 see section B.4
233232 structSize := roundUpTo8(t.Size())
234- if structSize == 0 || t.NumField() > 4 {
233+ if structSize == 0 {
235234 return false
236235 }
237- first := t.Field(0)
238- switch first.Type.Kind() {
239- case reflect.Float32, reflect.Float64:
240- firstKind := first.Type.Kind()
236+ var kind reflect.Kind
237+ var members int
238+ var walk func(reflect.Type) bool
239+ walk = func(t reflect.Type) bool {
241240 for i := 0; i < t.NumField(); i++ {
242- if t.Field(i).Type.Kind() != firstKind {
243- return false
244- }
245- }
246- return true
247- case reflect.Array:
248- switch first.Type.Elem().Kind() {
249- case reflect.Float32, reflect.Float64:
250- return true
251- default:
252- return false
253- }
254- case reflect.Struct:
255- for i := 0; i < first.Type.NumField(); i++ {
256- if !isHFA(first.Type) {
241+ ft := t.Field(i).Type
242+ switch ft.Kind() {
243+ case reflect.Float32, reflect.Float64:
244+ if kind == 0 {
245+ kind = ft.Kind()
246+ } else if ft.Kind() != kind {
247+ return false
248+ }
249+ members++
250+ if members > 4 {
251+ return false
252+ }
253+ case reflect.Struct:
254+ if !walk(ft) {
255+ return false
256+ }
257+ case reflect.Array:
258+ elemKind := ft.Elem().Kind()
259+ if elemKind != reflect.Float32 && elemKind != reflect.Float64 {
260+ return false
261+ }
262+ if kind == 0 {
263+ kind = elemKind
264+ } else if elemKind != kind {
265+ return false
266+ }
267+ members += ft.Len()
268+ if members > 4 {
269+ return false
270+ }
271+ default:
257272 return false
258273 }
259274 }
260275 return true
261- default:
262- return false
263276 }
277+ return walk(t) && members > 0
264278 }
265279
266280 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
@@ -229,38 +229,52 @@ func placeStack(v reflect.Value, keepAlive []any, addInt func(uintptr)) []any {
229 //229 //
230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst230 // [Arm64 Calling Convention]: https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst
231 func isHFA(t reflect.Type) bool {231 func isHFA(t reflect.Type) bool {
232- // round up struct size to nearest 8 see section B.4
233 structSize := roundUpTo8(t.Size())232 structSize := roundUpTo8(t.Size())
234- if structSize == 0 || t.NumField() > 4 {233+ if structSize == 0 {
235 return false234 return false
236 }235 }
237- first := t.Field(0)236+ var kind reflect.Kind
238- switch first.Type.Kind() {237+ var members int
239- case reflect.Float32, reflect.Float64:238+ var walk func(reflect.Type) bool
240- firstKind := first.Type.Kind()239+ walk = func(t reflect.Type) bool {
241 for i := 0; i < t.NumField(); i++ {240 for i := 0; i < t.NumField(); i++ {
242- if t.Field(i).Type.Kind() != firstKind {241+ ft := t.Field(i).Type
243- return false242+ switch ft.Kind() {
244- }243+ case reflect.Float32, reflect.Float64:
245- }244+ if kind == 0 {
246- return true245+ kind = ft.Kind()
247- case reflect.Array:246+ } else if ft.Kind() != kind {
248- switch first.Type.Elem().Kind() {247+ return false
249- case reflect.Float32, reflect.Float64:248+ }
250- return true249+ members++
251- default:250+ if members > 4 {
252- return false251+ return false
253- }252+ }
254- case reflect.Struct:253+ case reflect.Struct:
255- for i := 0; i < first.Type.NumField(); i++ {254+ if !walk(ft) {
256- if !isHFA(first.Type) {255+ return false
256+ }
257+ case reflect.Array:
258+ elemKind := ft.Elem().Kind()
259+ if elemKind != reflect.Float32 && elemKind != reflect.Float64 {
260+ return false
261+ }
262+ if kind == 0 {
263+ kind = elemKind
264+ } else if elemKind != kind {
265+ return false
266+ }
267+ members += ft.Len()
268+ if members > 4 {
269+ return false
270+ }
271+ default:
257 return false272 return false
258 }273 }
259 }274 }
260 return true275 return true
261- default:
262- return false
263 }276 }
277+ return walk(t) && members > 0
264 }278 }
265 279
266 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type280 // isHVA reports a Homogeneous Aggregate with a Fundamental Data Type that is a Short-Vector type
A struct_arm64_regression_test.go +37 -0
@@ -0,0 +1,37 @@
1+//go:build arm64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestIsHFARejectsNestedMixedStruct(t *testing.T) {
11+ type inner struct{ X, Y float32 }
12+ type mixed struct {
13+ Inner inner
14+ Tail int32
15+ }
16+ if isHFA(reflect.TypeOf(mixed{})) {
17+ t.Fatal("expected mixed nested struct to be non-HFA")
18+ }
19+}
20+
21+func TestIsHFAAcceptsNestedFloatStructs(t *testing.T) {
22+ type inner struct{ X, Y float32 }
23+ type outer struct {
24+ Left inner
25+ Right inner
26+ }
27+ if !isHFA(reflect.TypeOf(outer{})) {
28+ t.Fatal("expected nested float-only struct to be HFA")
29+ }
30+}
31+
32+func TestIsHFAAcceptsFloatArrayStruct(t *testing.T) {
33+ type arrayed struct{ A [2]float64 }
34+ if !isHFA(reflect.TypeOf(arrayed{})) {
35+ t.Fatal("expected float array struct to be HFA")
36+ }
37+}
@@ -0,0 +1,37 @@
1+//go:build arm64 && (darwin || linux)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+)
9+
10+func TestIsHFARejectsNestedMixedStruct(t *testing.T) {
11+ type inner struct{ X, Y float32 }
12+ type mixed struct {
13+ Inner inner
14+ Tail int32
15+ }
16+ if isHFA(reflect.TypeOf(mixed{})) {
17+ t.Fatal("expected mixed nested struct to be non-HFA")
18+ }
19+}
20+
21+func TestIsHFAAcceptsNestedFloatStructs(t *testing.T) {
22+ type inner struct{ X, Y float32 }
23+ type outer struct {
24+ Left inner
25+ Right inner
26+ }
27+ if !isHFA(reflect.TypeOf(outer{})) {
28+ t.Fatal("expected nested float-only struct to be HFA")
29+ }
30+}
31+
32+func TestIsHFAAcceptsFloatArrayStruct(t *testing.T) {
33+ type arrayed struct{ A [2]float64 }
34+ if !isHFA(reflect.TypeOf(arrayed{})) {
35+ t.Fatal("expected float array struct to be HFA")
36+ }
37+}
A struct_arm_regression_test.go +41 -0
@@ -0,0 +1,41 @@
1+//go:build arm && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+ "unsafe"
9+)
10+
11+func TestARMAddStructCopiesUnaddressableValue(t *testing.T) {
12+ type sample struct{ X uint32 }
13+ var numInts, numFloats, numStack int
14+ var ptr uintptr
15+ keepAlive := addStruct(reflect.ValueOf(sample{X: 7}), &numInts, &numFloats, &numStack, func(v uintptr) {
16+ ptr = v
17+ }, func(uintptr) {
18+ t.Fatal("unexpected float register")
19+ }, func(uintptr) {
20+ t.Fatal("unexpected stack slot")
21+ }, nil)
22+ if len(keepAlive) == 0 {
23+ t.Fatal("expected copied keepAlive value")
24+ }
25+ if got := *(*uint32)(unsafe.Pointer(ptr)); got != 7 {
26+ t.Fatalf("copied value = %#x, want %#x", got, uint32(7))
27+ }
28+}
29+
30+func TestARMPlaceRegistersReadsUnaddressableValue(t *testing.T) {
31+ type sample struct{ X uint32 }
32+ var ints []uintptr
33+ placeRegisters(reflect.ValueOf(sample{X: 7}), func(uintptr) {
34+ t.Fatal("unexpected float register")
35+ }, func(v uintptr) {
36+ ints = append(ints, v)
37+ })
38+ if len(ints) != 1 || ints[0] != 7 {
39+ t.Fatalf("placeRegisters() = %#v, want [7]", ints)
40+ }
41+}
@@ -0,0 +1,41 @@
1+//go:build arm && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import (
6+ "reflect"
7+ "testing"
8+ "unsafe"
9+)
10+
11+func TestARMAddStructCopiesUnaddressableValue(t *testing.T) {
12+ type sample struct{ X uint32 }
13+ var numInts, numFloats, numStack int
14+ var ptr uintptr
15+ keepAlive := addStruct(reflect.ValueOf(sample{X: 7}), &numInts, &numFloats, &numStack, func(v uintptr) {
16+ ptr = v
17+ }, func(uintptr) {
18+ t.Fatal("unexpected float register")
19+ }, func(uintptr) {
20+ t.Fatal("unexpected stack slot")
21+ }, nil)
22+ if len(keepAlive) == 0 {
23+ t.Fatal("expected copied keepAlive value")
24+ }
25+ if got := *(*uint32)(unsafe.Pointer(ptr)); got != 7 {
26+ t.Fatalf("copied value = %#x, want %#x", got, uint32(7))
27+ }
28+}
29+
30+func TestARMPlaceRegistersReadsUnaddressableValue(t *testing.T) {
31+ type sample struct{ X uint32 }
32+ var ints []uintptr
33+ placeRegisters(reflect.ValueOf(sample{X: 7}), func(uintptr) {
34+ t.Fatal("unexpected float register")
35+ }, func(v uintptr) {
36+ ints = append(ints, v)
37+ })
38+ if len(ints) != 1 || ints[0] != 7 {
39+ t.Fatalf("placeRegisters() = %#v, want [7]", ints)
40+ }
41+}
M struct_loong64.go +6 -11
@@ -19,7 +19,7 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
1919 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
2020 r1 = syscall.f1
2121 if numFields == 2 {
22- r1 = syscall.f2<<32 | syscall.f1
22+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
2323 }
2424 }
2525 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()
@@ -28,10 +28,10 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
2828 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
2929 switch numFields {
3030 case 4:
31- r1 = syscall.f2<<32 | syscall.f1
32- r2 = syscall.f4<<32 | syscall.f3
31+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
32+ r2 = packFloat32Pair(syscall.f3, syscall.f4)
3333 case 3:
34- r1 = syscall.f2<<32 | syscall.f1
34+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
3535 r2 = syscall.f3
3636 case 2:
3737 r1 = syscall.f1
@@ -91,13 +91,8 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
9191 align := byte(f.Type().Align()*8 - 1)
9292 shift = (shift + align) &^ align
9393 if shift >= 64 {
94- shift = 0
95- flushed = true
96- if class == _FLOAT {
97- addFloat(uintptr(val))
98- } else {
99- addInt(uintptr(val))
100- }
94+ flushPackedWord(&val, &shift, &flushed, class == _FLOAT, addFloat, addInt)
95+ class = _NO_CLASS
10196 }
10297 switch f.Type().Kind() {
10398 case reflect.Struct:
@@ -19,7 +19,7 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
19 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {19 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
20 r1 = syscall.f120 r1 = syscall.f1
21 if numFields == 2 {21 if numFields == 2 {
22- r1 = syscall.f2<<32 | syscall.f122+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
23 }23 }
24 }24 }
25 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()25 return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()
@@ -28,10 +28,10 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
28 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {28 if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
29 switch numFields {29 switch numFields {
30 case 4:30 case 4:
31- r1 = syscall.f2<<32 | syscall.f131+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
32- r2 = syscall.f4<<32 | syscall.f332+ r2 = packFloat32Pair(syscall.f3, syscall.f4)
33 case 3:33 case 3:
34- r1 = syscall.f2<<32 | syscall.f134+ r1 = packFloat32Pair(syscall.f1, syscall.f2)
35 r2 = syscall.f335 r2 = syscall.f3
36 case 2:36 case 2:
37 r1 = syscall.f137 r1 = syscall.f1
@@ -91,13 +91,8 @@ func placeRegisters(v reflect.Value, addFloat func(uintptr), addInt func(uintptr
91 align := byte(f.Type().Align()*8 - 1)91 align := byte(f.Type().Align()*8 - 1)
92 shift = (shift + align) &^ align92 shift = (shift + align) &^ align
93 if shift >= 64 {93 if shift >= 64 {
94- shift = 094+ flushPackedWord(&val, &shift, &flushed, class == _FLOAT, addFloat, addInt)
95- flushed = true95+ class = _NO_CLASS
96- if class == _FLOAT {
97- addFloat(uintptr(val))
98- } else {
99- addInt(uintptr(val))
100- }
101 }96 }
102 switch f.Type().Kind() {97 switch f.Type().Kind() {
103 case reflect.Struct:98 case reflect.Struct:
A struct_loong64_regression_test.go +35 -0
@@ -0,0 +1,35 @@
1+//go:build loong64 && linux
2+
3+package purego
4+
5+import (
6+ "math"
7+ "reflect"
8+ "testing"
9+)
10+
11+func TestLoong64GetStructMasksFloat32Pairs(t *testing.T) {
12+ type pair struct{ X, Y float32 }
13+ sys := syscall15Args{
14+ f1: uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5))),
15+ f2: uintptr(math.Float32bits(2.5)),
16+ }
17+ got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18+ if got.X != 1.5 || got.Y != 2.5 {
19+ t.Fatalf("got %+v, want {1.5 2.5}", got)
20+ }
21+}
22+
23+func TestLoong64PlaceRegistersResetsAfterFlush(t *testing.T) {
24+ type triple struct{ A, B, C uint32 }
25+ var ints []uintptr
26+ placeRegisters(reflect.ValueOf(triple{A: 1, B: 2, C: 3}), func(uintptr) {
27+ t.Fatal("unexpected float register")
28+ }, func(v uintptr) {
29+ ints = append(ints, v)
30+ })
31+ want := []uintptr{0x0000000200000001, 0x0000000000000003}
32+ if len(ints) != len(want) || ints[0] != want[0] || ints[1] != want[1] {
33+ t.Fatalf("placeRegisters() = %#v, want %#v", ints, want)
34+ }
35+}
@@ -0,0 +1,35 @@
1+//go:build loong64 && linux
2+
3+package purego
4+
5+import (
6+ "math"
7+ "reflect"
8+ "testing"
9+)
10+
11+func TestLoong64GetStructMasksFloat32Pairs(t *testing.T) {
12+ type pair struct{ X, Y float32 }
13+ sys := syscall15Args{
14+ f1: uintptr(0xDEADBEEF00000000 | uint64(math.Float32bits(1.5))),
15+ f2: uintptr(math.Float32bits(2.5)),
16+ }
17+ got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18+ if got.X != 1.5 || got.Y != 2.5 {
19+ t.Fatalf("got %+v, want {1.5 2.5}", got)
20+ }
21+}
22+
23+func TestLoong64PlaceRegistersResetsAfterFlush(t *testing.T) {
24+ type triple struct{ A, B, C uint32 }
25+ var ints []uintptr
26+ placeRegisters(reflect.ValueOf(triple{A: 1, B: 2, C: 3}), func(uintptr) {
27+ t.Fatal("unexpected float register")
28+ }, func(v uintptr) {
29+ ints = append(ints, v)
30+ })
31+ want := []uintptr{0x0000000200000001, 0x0000000000000003}
32+ if len(ints) != len(want) || ints[0] != want[0] || ints[1] != want[1] {
33+ t.Fatalf("placeRegisters() = %#v, want %#v", ints, want)
34+ }
35+}
M struct_s390x.go +1 -1
@@ -31,7 +31,7 @@ func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
3131 // float32 values in FP regs
3232 f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
3333 for i := 0; i < numFields; i++ {
34- *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])
34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = highFloat32Bits(f[i])
3535 }
3636 } else {
3737 // float64: whole register value is valid
@@ -31,7 +31,7 @@ func getStruct(outType reflect.Type, syscall syscall15Args) reflect.Value {
31 // float32 values in FP regs31 // float32 values in FP regs
32 f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}32 f := []uintptr{syscall.f1, syscall.f2, syscall.f3, syscall.f4}
33 for i := 0; i < numFields; i++ {33 for i := 0; i < numFields; i++ {
34- *(*uint32)(unsafe.Pointer(&buf[i*4])) = uint32(f[i])34+ *(*uint32)(unsafe.Pointer(&buf[i*4])) = highFloat32Bits(f[i])
35 }35 }
36 } else {36 } else {
37 // float64: whole register value is valid37 // float64: whole register value is valid
A struct_s390x_regression_test.go +21 -0
@@ -0,0 +1,21 @@
1+//go:build s390x && linux
2+
3+package purego
4+
5+import (
6+ "math"
7+ "reflect"
8+ "testing"
9+)
10+
11+func TestS390xGetStructUsesHighFloat32Bits(t *testing.T) {
12+ type pair struct{ X, Y float32 }
13+ sys := syscall15Args{
14+ f1: uintptr(uint64(math.Float32bits(1.5)) << 32),
15+ f2: uintptr(uint64(math.Float32bits(2.5)) << 32),
16+ }
17+ got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18+ if got.X != 1.5 || got.Y != 2.5 {
19+ t.Fatalf("got %+v, want {1.5 2.5}", got)
20+ }
21+}
@@ -0,0 +1,21 @@
1+//go:build s390x && linux
2+
3+package purego
4+
5+import (
6+ "math"
7+ "reflect"
8+ "testing"
9+)
10+
11+func TestS390xGetStructUsesHighFloat32Bits(t *testing.T) {
12+ type pair struct{ X, Y float32 }
13+ sys := syscall15Args{
14+ f1: uintptr(uint64(math.Float32bits(1.5)) << 32),
15+ f2: uintptr(uint64(math.Float32bits(2.5)) << 32),
16+ }
17+ got := getStruct(reflect.TypeOf(pair{}), sys).Interface().(pair)
18+ if got.X != 1.5 || got.Y != 2.5 {
19+ t.Fatalf("got %+v, want {1.5 2.5}", got)
20+ }
21+}
M syscall.go +6 -2
@@ -52,13 +52,15 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
5252 }
5353
5454 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
55-// There is an internal maximum number of arguments that SyscallN can take. It panics
56-// when the maximum is exceeded. It returns the result and the libc error code if there is one.
55+// There is an internal maximum number of arguments that SyscallN can take.
56+// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
57+// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
5758 //
5859 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
5960 // especially point 4.
6061 //
6162 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
63+// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
6264 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
6365 // for an explanation of why that is.
6466 //
@@ -85,6 +87,8 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
8587
8688 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
8789 // It is designed for method-like calls where the first argument is always a receiver pointer.
90+// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
91+// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
8892 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
8993 //
9094 //go:uintptrescapes
@@ -52,13 +52,15 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
52 }52 }
53 53
54 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.54 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
55-// There is an internal maximum number of arguments that SyscallN can take. It panics55+// There is an internal maximum number of arguments that SyscallN can take.
56-// when the maximum is exceeded. It returns the result and the libc error code if there is one.56+// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
57+// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
57 //58 //
58 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]59 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
59 // especially point 4.60 // especially point 4.
60 //61 //
61 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.62 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
63+// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
62 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-112805760764 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
63 // for an explanation of why that is.65 // for an explanation of why that is.
64 //66 //
@@ -85,6 +87,8 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
85 87
86 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.88 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
87 // It is designed for method-like calls where the first argument is always a receiver pointer.89 // It is designed for method-like calls where the first argument is always a receiver pointer.
90+// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
91+// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
88 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).92 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
89 //93 //
90 //go:uintptrescapes94 //go:uintptrescapes
M syscall_32bit.go +6 -21
@@ -13,7 +13,7 @@ package purego
1313 type CDecl struct{}
1414
1515 const (
16- maxArgs = 32
16+ maxArgs = 15
1717 )
1818
1919 type syscall15Args struct {
@@ -41,23 +41,6 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
4141 s.a13 = ints[12]
4242 s.a14 = ints[13]
4343 s.a15 = ints[14]
44- s.a16 = ints[15]
45- s.a17 = ints[16]
46- s.a18 = ints[17]
47- s.a19 = ints[18]
48- s.a20 = ints[19]
49- s.a21 = ints[20]
50- s.a22 = ints[21]
51- s.a23 = ints[22]
52- s.a24 = ints[23]
53- s.a25 = ints[24]
54- s.a26 = ints[25]
55- s.a27 = ints[26]
56- s.a28 = ints[27]
57- s.a29 = ints[28]
58- s.a30 = ints[29]
59- s.a31 = ints[30]
60- s.a32 = ints[31]
6144 s.f1 = floats[0]
6245 s.f2 = floats[1]
6346 s.f3 = floats[2]
@@ -73,18 +56,18 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
7356 s.f13 = floats[12]
7457 s.f14 = floats[13]
7558 s.f15 = floats[14]
76- s.f16 = floats[15]
7759 s.arm64_r8 = r8
7860 }
7961
8062 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
81-// There is an internal maximum number of arguments that SyscallN can take. It panics
82-// when the maximum is exceeded. It returns the result and the libc error code if there is one.
63+// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
64+// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
8365 //
8466 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
8567 // especially point 4.
8668 //
8769 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
70+// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
8871 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
8972 // for an explanation of why that is.
9073 //
@@ -111,6 +94,8 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
11194
11295 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
11396 // It is designed for method-like calls where the first argument is always a receiver pointer.
97+// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
98+// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
11499 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
115100 //
116101 //go:uintptrescapes
@@ -13,7 +13,7 @@ package purego
13 type CDecl struct{}13 type CDecl struct{}
14 14
15 const (15 const (
16- maxArgs = 3216+ maxArgs = 15
17 )17 )
18 18
19 type syscall15Args struct {19 type syscall15Args struct {
@@ -41,23 +41,6 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
41 s.a13 = ints[12]41 s.a13 = ints[12]
42 s.a14 = ints[13]42 s.a14 = ints[13]
43 s.a15 = ints[14]43 s.a15 = ints[14]
44- s.a16 = ints[15]
45- s.a17 = ints[16]
46- s.a18 = ints[17]
47- s.a19 = ints[18]
48- s.a20 = ints[19]
49- s.a21 = ints[20]
50- s.a22 = ints[21]
51- s.a23 = ints[22]
52- s.a24 = ints[23]
53- s.a25 = ints[24]
54- s.a26 = ints[25]
55- s.a27 = ints[26]
56- s.a28 = ints[27]
57- s.a29 = ints[28]
58- s.a30 = ints[29]
59- s.a31 = ints[30]
60- s.a32 = ints[31]
61 s.f1 = floats[0]44 s.f1 = floats[0]
62 s.f2 = floats[1]45 s.f2 = floats[1]
63 s.f3 = floats[2]46 s.f3 = floats[2]
@@ -73,18 +56,18 @@ func (s *syscall15Args) Set(fn uintptr, ints []uintptr, floats []uintptr, r8 uin
73 s.f13 = floats[12]56 s.f13 = floats[12]
74 s.f14 = floats[13]57 s.f14 = floats[13]
75 s.f15 = floats[14]58 s.f15 = floats[14]
76- s.f16 = floats[15]
77 s.arm64_r8 = r859 s.arm64_r8 = r8
78 }60 }
79 61
80 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.62 // SyscallN takes fn, a C function pointer and a list of arguments as uintptr.
81-// There is an internal maximum number of arguments that SyscallN can take. It panics63+// On 386 and arm, SyscallN supports at most 15 uintptr arguments.
82-// when the maximum is exceeded. It returns the result and the libc error code if there is one.64+// It panics when the maximum is exceeded. It returns the result and the libc error code if there is one.
83 //65 //
84 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]66 // In order to call this function properly make sure to follow all the rules specified in [unsafe.Pointer]
85 // especially point 4.67 // especially point 4.
86 //68 //
87 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.69 // NOTE: SyscallN does not properly call functions that have both integer and float parameters.
70+// Use RegisterFunc instead when the target function mixes integer and floating-point arguments.
88 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-112805760771 // See discussion comment https://github.com/ebiten/purego/pull/1#issuecomment-1128057607
89 // for an explanation of why that is.72 // for an explanation of why that is.
90 //73 //
@@ -111,6 +94,8 @@ func SyscallN(fn uintptr, args ...uintptr) (r1, r2, err uintptr) {
111 94
112 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.95 // SyscallSelf is like SyscallN but efficiently prepends self to args without allocation.
113 // It is designed for method-like calls where the first argument is always a receiver pointer.96 // It is designed for method-like calls where the first argument is always a receiver pointer.
97+// It shares SyscallN's argument-placement limits and mixed integer/float restriction.
98+// On 386 and arm, the total count including self is limited to 15 uintptr arguments.
114 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).99 // This avoids the heap allocation that would occur with SyscallN(fn, append([]uintptr{self}, args...)...).
115 //100 //
116 //go:uintptrescapes101 //go:uintptrescapes
A syscall_32bit_internal_test.go +16 -0
@@ -0,0 +1,16 @@
1+//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import "testing"
6+
7+func TestSyscall15ArgsSetDoesNotPanicWithMaxArgs(t *testing.T) {
8+ defer func() {
9+ if r := recover(); r != nil {
10+ t.Fatalf("Set panicked: %v", r)
11+ }
12+ }()
13+
14+ var s syscall15Args
15+ s.Set(1, make([]uintptr, maxArgs), make([]uintptr, maxArgs), 8)
16+}
@@ -0,0 +1,16 @@
1+//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2+
3+package purego
4+
5+import "testing"
6+
7+func TestSyscall15ArgsSetDoesNotPanicWithMaxArgs(t *testing.T) {
8+ defer func() {
9+ if r := recover(); r != nil {
10+ t.Fatalf("Set panicked: %v", r)
11+ }
12+ }()
13+
14+ var s syscall15Args
15+ s.Set(1, make([]uintptr, maxArgs), make([]uintptr, maxArgs), 8)
16+}
A syscall_32bit_test.go +36 -0
@@ -0,0 +1,36 @@
1+//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2+
3+package purego_test
4+
5+import (
6+ "fmt"
7+ "testing"
8+
9+ "github.com/ebitengine/purego"
10+)
11+
12+func TestSyscallN32BitPanicsAt16Args(t *testing.T) {
13+ defer func() {
14+ r := recover()
15+ if r == nil {
16+ t.Fatal("expected panic")
17+ }
18+ if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallN" {
19+ t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallN")
20+ }
21+ }()
22+ purego.SyscallN(uintptr(1), make([]uintptr, 16)...)
23+}
24+
25+func TestSyscallSelf32BitPanicsAt16Args(t *testing.T) {
26+ defer func() {
27+ r := recover()
28+ if r == nil {
29+ t.Fatal("expected panic")
30+ }
31+ if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallSelf" {
32+ t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallSelf")
33+ }
34+ }()
35+ purego.SyscallSelf(uintptr(1), 1, make([]uintptr, 15)...)
36+}
@@ -0,0 +1,36 @@
1+//go:build (386 || arm) && (freebsd || linux || netbsd || windows)
2+
3+package purego_test
4+
5+import (
6+ "fmt"
7+ "testing"
8+
9+ "github.com/ebitengine/purego"
10+)
11+
12+func TestSyscallN32BitPanicsAt16Args(t *testing.T) {
13+ defer func() {
14+ r := recover()
15+ if r == nil {
16+ t.Fatal("expected panic")
17+ }
18+ if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallN" {
19+ t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallN")
20+ }
21+ }()
22+ purego.SyscallN(uintptr(1), make([]uintptr, 16)...)
23+}
24+
25+func TestSyscallSelf32BitPanicsAt16Args(t *testing.T) {
26+ defer func() {
27+ r := recover()
28+ if r == nil {
29+ t.Fatal("expected panic")
30+ }
31+ if got := fmt.Sprint(r); got != "purego: too many arguments to SyscallSelf" {
32+ t.Fatalf("panic = %q, want %q", got, "purego: too many arguments to SyscallSelf")
33+ }
34+ }()
35+ purego.SyscallSelf(uintptr(1), 1, make([]uintptr, 15)...)
36+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close