1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Ebitengine Authors
3
4package purego_test
5
6import (
7 "bytes"
8 "errors"
9 "fmt"
10 "math"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "reflect"
15 "runtime"
16 "strconv"
17 "strings"
18 "sync"
19 "testing"
20 "unsafe"
21
22 "github.com/ebitengine/purego"
23 "github.com/ebitengine/purego/internal/load"
24)
25
26func getSystemLibrary() (string, error) {
27 switch runtime.GOOS {
28 case "android":
29 return "libc.so", nil
30 case "darwin", "ios":
31 return "/usr/lib/libSystem.B.dylib", nil
32 case "freebsd":
33 return "libc.so.7", nil
34 case "linux":
35 return "libc.so.6", nil
36 case "netbsd":
37 return "libc.so", nil
38 case "windows":
39 return "ucrtbase.dll", nil
40 default:
41 return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)
42 }
43}
44
45func TestRegisterFunc_ConcurrentPointerReturn(t *testing.T) {
46 library, err := getSystemLibrary()
47 if err != nil {
48 t.Fatalf("couldn't get system library: %s", err)
49 }
50 libc, err := load.OpenLibrary(library)
51 if err != nil {
52 t.Fatalf("failed to dlopen: %s", err)
53 }
54
55 var alloc func(uint64) *byte
56 var free func(*byte)
57 purego.RegisterLibFunc(&alloc, libc, "malloc")
58 purego.RegisterLibFunc(&free, libc, "free")
59
60 var wg sync.WaitGroup
61
62 for i := 0; i < runtime.NumCPU(); i++ {
63 wg.Add(1)
64 go func(id int) {
65 defer wg.Done()
66 for range 400_000 {
67 ptr := alloc(5)
68 if ptr == nil {
69 continue
70 }
71 free(ptr)
72 }
73 }(i)
74 }
75
76 wg.Wait()
77}
78
79func TestRegisterFunc(t *testing.T) {
80 library, err := getSystemLibrary()
81 if err != nil {
82 t.Fatalf("couldn't get system library: %s", err)
83 }
84 libc, err := load.OpenLibrary(library)
85 if err != nil {
86 t.Fatalf("failed to dlopen: %s", err)
87 }
88 var puts func(string)
89 purego.RegisterLibFunc(&puts, libc, "puts")
90 puts("Calling C from from Go without Cgo!")
91}
92
93func Test_qsort(t *testing.T) {
94 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
95 t.Skip("Platform doesn't support Floats")
96 return
97 }
98 library, err := getSystemLibrary()
99 if err != nil {
100 t.Fatalf("couldn't get system library: %s", err)
101 }
102 libc, err := load.OpenLibrary(library)
103 if err != nil {
104 t.Fatalf("failed to dlopen: %s", err)
105 }
106
107 data := []int{88, 56, 100, 2, 25}
108 sorted := []int{2, 25, 56, 88, 100}
109 compare := func(_ purego.CDecl, a, b *int) int {
110 return *a - *b
111 }
112 var qsort func(data []int, nitms uintptr, size uintptr, compar func(_ purego.CDecl, a, b *int) int)
113 purego.RegisterLibFunc(&qsort, libc, "qsort")
114 qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare)
115 for i := range data {
116 if data[i] != sorted[i] {
117 t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i)
118 }
119 }
120}
121
122func TestRegisterFunc_Floats(t *testing.T) {
123 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
124 t.Skip("Platform doesn't support Floats")
125 return
126 }
127 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
128 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
129 }
130 library, err := getSystemLibrary()
131 if err != nil {
132 t.Fatalf("couldn't get system library: %s", err)
133 }
134 libc, err := load.OpenLibrary(library)
135 if err != nil {
136 t.Fatalf("failed to dlopen: %s", err)
137 }
138 {
139 var strtof func(arg string) float32
140 purego.RegisterLibFunc(&strtof, libc, "strtof")
141 const (
142 arg = "2"
143 )
144 got := strtof(arg)
145 expected := float32(2)
146 if got != expected {
147 t.Errorf("strtof failed. got %f but wanted %f", got, expected)
148 }
149 }
150 {
151 var strtod func(arg string, ptr **byte) float64
152 purego.RegisterLibFunc(&strtod, libc, "strtod")
153 const (
154 arg = "1"
155 )
156 got := strtod(arg, nil)
157 expected := float64(1)
158 if got != expected {
159 t.Errorf("strtod failed. got %f but wanted %f", got, expected)
160 }
161 }
162}
163
164func TestRegisterLibFunc_Bool(t *testing.T) {
165 if runtime.GOARCH != "arm" && runtime.GOARCH != "arm64" && runtime.GOARCH != "386" && runtime.GOARCH != "amd64" && runtime.GOARCH != "loong64" && runtime.GOARCH != "ppc64le" && runtime.GOARCH != "riscv64" && runtime.GOARCH != "s390x" {
166 t.Skip("Platform doesn't support callbacks")
167 return
168 }
169 // this callback recreates the state where the return register
170 // contains other information but the least significant byte is false
171 cbFalse := purego.NewCallback(func() uintptr {
172 x := uint64(0x7F5948AE9A00)
173 return uintptr(x)
174 })
175 var runFalse func() bool
176 purego.RegisterFunc(&runFalse, cbFalse)
177 expected := false
178 if got := runFalse(); got != expected {
179 t.Errorf("runFalse failed. got %t but wanted %t", got, expected)
180 }
181}
182
183func TestABI(t *testing.T) {
184 libFileName := filepath.Join(t.TempDir(), "abitest.so")
185 t.Logf("Build %v", libFileName)
186
187 if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
188 t.Fatal(err)
189 }
190
191 lib, err := load.OpenLibrary(libFileName)
192 if err != nil {
193 t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
194 }
195 defer func() {
196 if err := load.CloseLibrary(lib); err != nil {
197 t.Fatalf("failed to close library: %s", err)
198 }
199 }()
200 {
201 const cName = "stack_uint8_t"
202 const expect = 2047
203 var fn func(a, b, c, d, e, f, g, h uint32, i, j uint8, k uint32) uint32
204 purego.RegisterLibFunc(&fn, lib, cName)
205 res := fn(256, 512, 4, 8, 16, 32, 64, 128, 1, 2, 1024)
206 if res != expect {
207 t.Fatalf("%s: got %d, want %d", cName, res, expect)
208 }
209 }
210 {
211 const cName = "reg_uint8_t"
212 const expect = 1027
213 var fn func(a, b uint8, c uint32) uint32
214 purego.RegisterLibFunc(&fn, lib, cName)
215 res := fn(1, 2, 1024)
216 if res != expect {
217 t.Fatalf("%s: got %d, want %d", cName, res, expect)
218 }
219 }
220 {
221 const cName = "stack_string"
222 const expect = 255
223 var fn func(a, b, c, d, e, f, g, h uint32, i string) uint32
224 purego.RegisterLibFunc(&fn, lib, cName)
225 res := fn(1, 2, 4, 8, 16, 32, 64, 128, "test")
226 if res != expect {
227 t.Fatalf("%s: got %d, want %d", cName, res, expect)
228 }
229 }
230 {
231 const cName = "stack_8i32_3strings"
232 var fn func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, string, string)
233 purego.RegisterLibFunc(&fn, lib, cName)
234 buf := make([]byte, 256)
235 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
236 res := string(buf[:bytes.IndexByte(buf, 0)])
237 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
238 if res != want {
239 t.Fatalf("%s: got %q, want %q", cName, res, want)
240 }
241 }
242}
243
244func TestABI_ArgumentPassing(t *testing.T) {
245 libFileName := filepath.Join(t.TempDir(), "abitest.so")
246 if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
247 t.Fatal(err)
248 }
249 lib, err := load.OpenLibrary(libFileName)
250 if err != nil {
251 t.Fatalf("Failed to open library %q: %v", libFileName, err)
252 }
253 t.Cleanup(func() {
254 if err := load.CloseLibrary(lib); err != nil {
255 t.Errorf("Failed to close library: %v", err)
256 }
257 })
258
259 tests := []struct {
260 name string
261 fn any
262 cFn string
263 call func(any) string
264 want string
265 }{
266 {
267 name: "10_int32_baseline",
268 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
269 cFn: "stack_10_int32",
270 call: func(f any) string {
271 buf := make([]byte, 256)
272 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
273 return string(buf[:bytes.IndexByte(buf, 0)])
274 },
275 want: "1:2:3:4:5:6:7:8:9:10",
276 },
277 {
278 name: "11_int32",
279 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
280 cFn: "stack_11_int32",
281 call: func(f any) string {
282 buf := make([]byte, 256)
283 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)
284 return string(buf[:bytes.IndexByte(buf, 0)])
285 },
286 want: "1:2:3:4:5:6:7:8:9:10:11",
287 },
288 {
289 name: "10_float32",
290 fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)),
291 cFn: "stack_10_float32",
292 call: func(f any) string {
293 buf := make([]byte, 256)
294 (*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, float32, float32)))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0)
295 return string(buf[:bytes.IndexByte(buf, 0)])
296 },
297 want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
298 },
299 {
300 name: "mixed_stack_strings",
301 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)),
302 cFn: "stack_mixed_stack_4args",
303 call: func(f any) string {
304 buf := make([]byte, 256)
305 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, bool, int32, string)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, "foo", false, 99, "bar")
306 return string(buf[:bytes.IndexByte(buf, 0)])
307 },
308 want: "1:2:3:4:5:6:7:8:foo:0:99:bar",
309 },
310 {
311 name: "20_int32",
312 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)),
313 cFn: "stack_20_int32",
314 call: func(f any) string {
315 buf := make([]byte, 512)
316 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32, int32)))(&buf[0], 512, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
317 return string(buf[:bytes.IndexByte(buf, 0)])
318 },
319 want: "1:2:3:4:5:6:7:8:9:10:11:12:13:14:15:16:17:18:19:20",
320 },
321 {
322 name: "8int_hfa2_stack",
323 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
324 cFn: "stack_8int_hfa2_stack",
325 call: func(f any) string {
326 buf := make([]byte, 256)
327 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
328 return string(buf[:bytes.IndexByte(buf, 0)])
329 },
330 want: "1:2:3:4:5:6:7:8:10.0:20.0",
331 },
332 {
333 name: "8int_2structs_stack",
334 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })),
335 cFn: "stack_8int_2structs_stack",
336 call: func(f any) string {
337 buf := make([]byte, 256)
338 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, struct{ x, y int32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y int32 }{9, 10}, struct{ x, y int32 }{11, 12})
339 return string(buf[:bytes.IndexByte(buf, 0)])
340 },
341 want: "1:2:3:4:5:6:7:8:9:10:11:12",
342 },
343 {
344 name: "8float_hfa2_stack",
345 fn: new(func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })),
346 cFn: "stack_8float_hfa2_stack",
347 call: func(f any) string {
348 buf := make([]byte, 256)
349 (*f.(*func(*byte, uintptr, float32, float32, float32, float32, float32, float32, float32, float32, struct{ x, y float32 })))(&buf[0], 256, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, struct{ x, y float32 }{9.0, 10.0})
350 return string(buf[:bytes.IndexByte(buf, 0)])
351 },
352 want: "1.0:2.0:3.0:4.0:5.0:6.0:7.0:8.0:9.0:10.0",
353 },
354 {
355 name: "8int_hfa2_floatregs",
356 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })),
357 cFn: "stack_8int_hfa2_floatregs",
358 call: func(f any) string {
359 buf := make([]byte, 256)
360 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y float32 }{10.0, 20.0})
361 return string(buf[:bytes.IndexByte(buf, 0)])
362 },
363 want: "1:2:3:4:5:6:7:8:10.0:20.0",
364 },
365 {
366 name: "8int_int_struct_int",
367 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)),
368 cFn: "stack_8int_int_struct_int",
369 call: func(f any) string {
370 buf := make([]byte, 256)
371 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y int32 }, int32)))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, 9, struct{ x, y int32 }{10, 11}, 12)
372 return string(buf[:bytes.IndexByte(buf, 0)])
373 },
374 want: "1:2:3:4:5:6:7:8:9:10:11:12",
375 },
376 {
377 name: "8int_hfa4_stack",
378 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })),
379 cFn: "stack_8int_hfa4_stack",
380 call: func(f any) string {
381 buf := make([]byte, 256)
382 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct{ x, y, z, w float32 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct{ x, y, z, w float32 }{10.0, 20.0, 30.0, 40.0})
383 return string(buf[:bytes.IndexByte(buf, 0)])
384 },
385 want: "1:2:3:4:5:6:7:8:10.0:20.0:30.0:40.0",
386 },
387 {
388 name: "8int_mixed_struct",
389 fn: new(func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
390 a int32
391 b float32
392 })),
393 cFn: "stack_8int_mixed_struct",
394 call: func(f any) string {
395 buf := make([]byte, 256)
396 (*f.(*func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, struct {
397 a int32
398 b float32
399 })))(&buf[0], 256, 1, 2, 3, 4, 5, 6, 7, 8, struct {
400 a int32
401 b float32
402 }{9, 10.0})
403 return string(buf[:bytes.IndexByte(buf, 0)])
404 },
405 want: "1:2:3:4:5:6:7:8:9:10.0",
406 },
407 {
408 // check if unaligned 64bit argument and 64bit returned value is properly passed via registers
409 // arm-specific but must work everywhere
410 name: "arm_int64_unaligned_in_registers",
411 fn: new(func(uintptr, int64) int64),
412 cFn: "arm_int64_unaligned_in_registers",
413 call: func(f any) string {
414 fn := *(f).(*func(x uintptr, y int64) int64)
415 return strconv.FormatInt(fn(456, math.MaxInt32+1500), 10)
416 },
417 want: strconv.FormatInt(456*123+math.MaxInt32+1500, 10),
418 },
419 {
420 // check if unaligned 64bit argument and 64bit returned value is properly passed via stack
421 // arm-specific but must work everywhere
422 name: "arm_int64_unaligned_on_stack",
423 fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, int64) int64),
424 cFn: "arm_int64_unaligned_on_stack",
425 call: func(f any) string {
426 fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 int64) int64)
427 return strconv.FormatInt(fn(12, 34, 56, 78, 90, math.MaxInt32+1500), 10)
428 },
429 want: strconv.FormatInt(12*1+34*2+56*3+78*4+90*5+math.MaxInt32+1500, 10),
430 },
431 {
432 // check if unaligned 64bit argument and 64bit returned value is properly passed via stack when it's occupied by floats
433 // arm-specific but must work everywhere
434 name: "arm_int64_unaligned_on_stack_after_floats",
435 fn: new(func(
436 uintptr, uintptr, uintptr, uintptr,
437 float32, float32, float32, float32,
438 float32, float32, float32, float32,
439 float32, float32, float32, float32,
440 float32, float32, float32, float32,
441 float32, int64,
442 ) int64),
443 cFn: "arm_int64_unaligned_on_stack_after_floats",
444 call: func(f any) string {
445 fn := *(f).(*func(
446 a1, a2, a3, a4 uintptr,
447 f1, f2, f3, f4 float32,
448 f5, f6, f7, f8 float32,
449 f9, f10, f11, f12 float32,
450 f13, f14, f15, f16 float32,
451 f17 float32, a5 int64,
452 ) int64)
453 return strconv.FormatInt(fn(
454 12, 34, 56, 78,
455 0, 0, 0, 0,
456 0, 0, 0, 0,
457 0, 0, 0, 0,
458 0, 0, 0, 0,
459 0, math.MaxInt32+1500,
460 ), 10)
461 },
462 want: strconv.FormatInt(12*1+34*2+56*3+78*4+math.MaxInt32+1500, 10),
463 },
464 {
465 // check if unaligned float 64bit argument and float 64bit returned value is properly passed via registers
466 // arm-softfloat-specific but must work everywhere
467 name: "arm_float64_unaligned_in_registers",
468 fn: new(func(uintptr, float64) float64),
469 cFn: "arm_float64_unaligned_in_registers",
470 call: func(f any) string {
471 fn := *(f).(*func(x uintptr, y float64) float64)
472 return strconv.FormatFloat(fn(456, math.MaxFloat32+1500), 'b', 10, 64)
473 },
474 want: strconv.FormatFloat(456*123.5+math.MaxFloat32+1500, 'b', 10, 64),
475 },
476 {
477 // check if unaligned float 64bit argument and float 64bit returned value is properly passed via stack
478 // arm-softfloat-specific but must work everywhere
479 name: "arm_float64_unaligned_on_stack",
480 fn: new(func(uintptr, uintptr, uintptr, uintptr, uintptr, float64) float64),
481 cFn: "arm_float64_unaligned_on_stack",
482 call: func(f any) string {
483 fn := *(f).(*func(a1, a2, a3, a4, a5 uintptr, a6 float64) float64)
484 return strconv.FormatFloat(fn(12, 34, 56, 78, 90, math.MaxFloat32+1500), 'b', 10, 64)
485 },
486 want: strconv.FormatFloat(12*1+34*2+56*3+78*4+90*5+math.MaxFloat32+1500, 'b', 10, 64),
487 },
488 }
489
490 for _, tt := range tests {
491 t.Run(tt.name, func(t *testing.T) {
492 if tt.name == "20_int32" && runtime.GOARCH == "ppc64le" {
493 t.Skip("ppc64le retains the 15-argument limit")
494 }
495 if (tt.name == "10_float32" || tt.name == "arm_int64_unaligned_on_stack_after_floats") &&
496 (runtime.GOARCH == "loong64" || runtime.GOARCH == "ppc64le" || runtime.GOARCH == "riscv64" || runtime.GOARCH == "s390x") {
497 t.Skip("float32 stack arguments not yet supported on this platform")
498 }
499 // Struct tests require Darwin ARM64 or AMD64
500 if strings.HasPrefix(tt.name, "8int_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
501 t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
502 }
503 if strings.HasPrefix(tt.name, "8float_") && (runtime.GOOS != "darwin" || (runtime.GOARCH != "arm64" && runtime.GOARCH != "amd64")) {
504 t.Skip("struct argument tests only supported on Darwin ARM64/AMD64")
505 }
506
507 purego.RegisterLibFunc(tt.fn, lib, tt.cFn)
508 got := tt.call(tt.fn)
509 if got != tt.want {
510 t.Errorf("%s\n got: %q\n want: %q", tt.cFn, got, tt.want)
511 }
512 })
513 }
514
515 t.Run("20_uintptr", func(t *testing.T) {
516 if runtime.GOARCH == "ppc64le" {
517 t.Skip("ppc64le retains the 15-argument limit")
518 }
519 var fn func(uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr
520 purego.RegisterLibFunc(&fn, lib, "stack_20_uintptr")
521 got := fn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20)
522 const want = uintptr(210)
523 if got != want {
524 t.Fatalf("stack_20_uintptr: got %d, want %d", got, want)
525 }
526 })
527
528 t.Run("32_uintptr", func(t *testing.T) {
529 if runtime.GOARCH == "ppc64le" {
530 t.Skip("ppc64le retains the 15-argument limit")
531 }
532 var fn func(
533 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
534 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
535 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
536 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
537 ) uintptr
538 purego.RegisterLibFunc(&fn, lib, "stack_32_uintptr")
539 got := fn(
540 1, 2, 3, 4, 5, 6, 7, 8,
541 9, 10, 11, 12, 13, 14, 15, 16,
542 17, 18, 19, 20, 21, 22, 23, 24,
543 25, 26, 27, 28, 29, 30, 31, 32,
544 )
545 const want = uintptr(528)
546 if got != want {
547 t.Fatalf("stack_32_uintptr: got %d, want %d", got, want)
548 }
549 })
550
551 t.Run("syscalln_20_uintptr", func(t *testing.T) {
552 if runtime.GOARCH == "ppc64le" {
553 t.Skip("ppc64le retains the 15-argument limit")
554 }
555 fn, err := load.OpenSymbol(lib, "stack_20_uintptr")
556 if err != nil {
557 t.Fatalf("OpenSymbol(stack_20_uintptr) failed: %v", err)
558 }
559 got, _, _ := purego.SyscallN(fn,
560 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
561 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
562 )
563 const want = uintptr(210)
564 if got != want {
565 t.Fatalf("stack_20_uintptr SyscallN: got %d, want %d", got, want)
566 }
567 })
568
569 t.Run("syscalln_32_uintptr", func(t *testing.T) {
570 if runtime.GOARCH == "ppc64le" {
571 t.Skip("ppc64le retains the 15-argument limit")
572 }
573 fn, err := load.OpenSymbol(lib, "stack_32_uintptr")
574 if err != nil {
575 t.Fatalf("OpenSymbol(stack_32_uintptr) failed: %v", err)
576 }
577 got, _, _ := purego.SyscallN(fn,
578 1, 2, 3, 4, 5, 6, 7, 8,
579 9, 10, 11, 12, 13, 14, 15, 16,
580 17, 18, 19, 20, 21, 22, 23, 24,
581 25, 26, 27, 28, 29, 30, 31, 32,
582 )
583 const want = uintptr(528)
584 if got != want {
585 t.Fatalf("stack_32_uintptr SyscallN: got %d, want %d", got, want)
586 }
587 })
588
589 t.Run("32_mixed_int_float", func(t *testing.T) {
590 if unsafe.Sizeof(uintptr(0)) == 4 {
591 t.Skip("requires 64-bit uintptr slots")
592 }
593 if runtime.GOARCH == "ppc64le" {
594 t.Skip("mixed int/float stack arguments are not yet supported on ppc64le")
595 }
596
597 var fn func(
598 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
599 uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr,
600 float64, float64, float64, float64, float64, float64, float64, float64,
601 float64, float64, float64, float64, float64, float64, float64, float64,
602 ) float64
603 purego.RegisterLibFunc(&fn, lib, "stack_32_mixed_int_float")
604 got := fn(
605 1, 2, 3, 4, 5, 6, 7, 8,
606 9, 10, 11, 12, 13, 14, 15, 16,
607 1, 2, 3, 4, 5, 6, 7, 8,
608 9, 10, 11, 12, 13, 14, 15, 16,
609 )
610 const want = 5168.0
611 if got != want {
612 t.Fatalf("stack_32_mixed_int_float: got %f, want %f", got, want)
613 }
614 })
615}
616
617func TestABI_TooManyArguments(t *testing.T) {
618 mustPanic := func(t *testing.T, want string, f func()) {
619 t.Helper()
620 defer func() {
621 r := recover()
622 if r == nil {
623 t.Fatalf("expected panic %q, got none", want)
624 }
625 got := fmt.Sprint(r)
626 if got != want {
627 t.Fatalf("panic mismatch:\n got: %q\n want: %q", got, want)
628 }
629 }()
630 f()
631 }
632
633 // 33 int64 parameters exceeds maxArgs=32.
634 t.Run("registerfunc_33_int64_exceeds_limit", func(t *testing.T) {
635 mustPanic(t, "purego: too many stack arguments", func() {
636 var fn func(
637 int64, int64, int64, int64, int64, int64, int64, int64,
638 int64, int64, int64, int64, int64, int64, int64, int64,
639 int64, int64, int64, int64, int64, int64, int64, int64,
640 int64, int64, int64, int64, int64, int64, int64, int64,
641 int64,
642 )
643 purego.RegisterFunc(&fn, 1)
644 })
645 })
646
647 t.Run("registerfunc_16_int64_exceeds_ppc64le_limit", func(t *testing.T) {
648 if runtime.GOARCH != "ppc64le" {
649 t.Skip("ppc64le retains the 15-argument limit")
650 }
651 mustPanic(t, "purego: too many stack arguments", func() {
652 var fn func(
653 int64, int64, int64, int64, int64, int64, int64, int64,
654 int64, int64, int64, int64, int64, int64, int64, int64,
655 )
656 purego.RegisterFunc(&fn, 1)
657 })
658 })
659
660 t.Run("syscalln_33_uintptr_exceeds_limit", func(t *testing.T) {
661 mustPanic(t, "purego: too many arguments to SyscallN", func() {
662 purego.SyscallN(1,
663 1, 2, 3, 4, 5, 6, 7, 8,
664 9, 10, 11, 12, 13, 14, 15, 16,
665 17, 18, 19, 20, 21, 22, 23, 24,
666 25, 26, 27, 28, 29, 30, 31, 32,
667 33,
668 )
669 })
670 })
671
672 t.Run("syscalln_16_uintptr_exceeds_ppc64le_limit", func(t *testing.T) {
673 if runtime.GOARCH != "ppc64le" {
674 t.Skip("ppc64le retains the 15-argument limit")
675 }
676 mustPanic(t, "purego: too many arguments to SyscallN", func() {
677 purego.SyscallN(1,
678 1, 2, 3, 4, 5, 6, 7, 8,
679 9, 10, 11, 12, 13, 14, 15, 16,
680 )
681 })
682 })
683}
684
685func TestABI_StructReturnHiddenPointer(t *testing.T) {
686 // A struct returned in memory is passed a hidden pointer as the first
687 // integer argument, so maxArgs integer arguments plus it need one more slot
688 // than sysargs holds and RegisterFunc must reject the registration.
689 type bigStruct struct{ A, B, C uint64 } // larger than two eightbytes
690
691 if !purego.StructReturnInMemory(reflect.TypeFor[bigStruct]()) {
692 t.Skipf("GOARCH=%s does not return large structs via a hidden integer argument", runtime.GOARCH)
693 }
694
695 in := make([]reflect.Type, purego.MaxArgs)
696 for i := range in {
697 in[i] = reflect.TypeFor[uintptr]()
698 }
699 fnType := reflect.FuncOf(in, []reflect.Type{reflect.TypeFor[bigStruct]()}, false)
700 fptr := reflect.New(fnType)
701
702 defer func() {
703 switch r := recover(); {
704 case r == nil:
705 t.Fatal("RegisterFunc accepted the call; the hidden struct-return pointer was not counted against the stack limit")
706 case strings.Contains(fmt.Sprint(r), "too many stack arguments"):
707 // Expected: the guard fired.
708 case strings.Contains(fmt.Sprint(r), "only supported on"):
709 t.Skipf("struct returns are unsupported on this platform: %v", r)
710 default:
711 t.Fatalf("unexpected panic: %v", r)
712 }
713 }()
714
715 // A non-zero cfn passes the nil check; the panic fires during the preflight
716 // argument count, before the function is ever called.
717 purego.RegisterFunc(fptr.Interface(), uintptr(1))
718}
719
720func buildSharedLib(tb testing.TB, compilerEnv, libFile string, sources ...string) error {
721 tb.Helper()
722 // When PUREGO_TEST_PREBUILT_LIBDIR is set, the shared library has been
723 // cross-compiled ahead of time and placed in that directory under the
724 // base name of libFile. This allows running the tests on a target that
725 // has no C toolchain, such as an Android emulator.
726 if dir := os.Getenv("PUREGO_TEST_PREBUILT_LIBDIR"); dir != "" {
727 data, err := os.ReadFile(filepath.Join(dir, filepath.Base(libFile)))
728 if err != nil {
729 return fmt.Errorf("prebuilt lib: %w", err)
730 }
731 if err := os.WriteFile(libFile, data, 0o755); err != nil {
732 return fmt.Errorf("prebuilt lib: %w", err)
733 }
734 return nil
735 }
736
737 // Compiling the library needs a C toolchain targeting GOARCH. CI has none
738 // for Windows on 386 or arm64, so skip those (the prebuilt path above
739 // avoids the toolchain).
740 if runtime.GOOS == "windows" {
741 switch runtime.GOARCH {
742 case "386":
743 tb.Skip("need a 386 C toolchain to run this test") // TODO: find a 386 C toolchain for test
744 case "arm64":
745 tb.Skip("need an arm64 C toolchain to run this test")
746 }
747 }
748
749 out, err := exec.Command("go", "env", compilerEnv).Output()
750 if err != nil {
751 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
752 }
753
754 compiler := strings.TrimSpace(string(out))
755 if compiler == "" {
756 return errors.New("compiler not found")
757 }
758
759 args := []string{"-shared", "-Wall", "-Werror", "-fPIC", "-o", libFile}
760 if runtime.GOARCH == "386" {
761 args = append(args, "-m32")
762 }
763 // macOS arm64 can run amd64 tests through Rossetta.
764 // Build the shared library based on the GOARCH and not
765 // the default behavior of the compiler.
766 if runtime.GOOS == "darwin" {
767 var arch string
768 switch runtime.GOARCH {
769 case "arm64":
770 arch = "arm64"
771 case "amd64":
772 arch = "x86_64"
773 default:
774 return fmt.Errorf("unknown macOS architecture %s", runtime.GOARCH)
775 }
776 args = append(args, "-arch", arch)
777 }
778 cmd := exec.Command(compiler, append(args, sources...)...)
779 if out, err := cmd.CombinedOutput(); err != nil {
780 return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
781 }
782
783 return nil
784}