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