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