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