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/exec"
11 "path/filepath"
12 "runtime"
13 "strings"
14 "sync"
15 "testing"
16 "unsafe"
17
18 "github.com/bnema/purego"
19 "github.com/bnema/purego/internal/load"
20)
21
22func getSystemLibrary() (string, error) {
23 switch runtime.GOOS {
24 case "darwin":
25 return "/usr/lib/libSystem.B.dylib", nil
26 case "freebsd":
27 return "libc.so.7", nil
28 case "linux":
29 return "libc.so.6", nil
30 case "netbsd":
31 return "libc.so", nil
32 case "windows":
33 return "ucrtbase.dll", nil
34 default:
35 return "", fmt.Errorf("GOOS=%s is not supported", runtime.GOOS)
36 }
37}
38
39func TestRegisterFunc_ConcurrentPointerReturn(t *testing.T) {
40 library, err := getSystemLibrary()
41 if err != nil {
42 t.Fatalf("couldn't get system library: %s", err)
43 }
44 libc, err := load.OpenLibrary(library)
45 if err != nil {
46 t.Fatalf("failed to dlopen: %s", err)
47 }
48
49 var alloc func(uint64) *byte
50 var free func(*byte)
51 purego.RegisterLibFunc(&alloc, libc, "malloc")
52 purego.RegisterLibFunc(&free, libc, "free")
53
54 var wg sync.WaitGroup
55 for i := 0; i < runtime.NumCPU(); i++ {
56 wg.Add(1)
57 go func() {
58 defer wg.Done()
59 for j := 0; j < 400_000; j++ {
60 ptr := alloc(5)
61 if ptr == nil {
62 continue
63 }
64 free(ptr)
65 }
66 }()
67 }
68 wg.Wait()
69}
70
71func TestRegisterFunc(t *testing.T) {
72 library, err := getSystemLibrary()
73 if err != nil {
74 t.Fatalf("couldn't get system library: %s", err)
75 }
76 libc, err := load.OpenLibrary(library)
77 if err != nil {
78 t.Fatalf("failed to dlopen: %s", err)
79 }
80 var puts func(string)
81 purego.RegisterLibFunc(&puts, libc, "puts")
82 puts("Calling C from from Go without Cgo!")
83}
84
85func Test_qsort(t *testing.T) {
86 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" {
87 t.Skip("Platform doesn't support Floats")
88 return
89 }
90 library, err := getSystemLibrary()
91 if err != nil {
92 t.Fatalf("couldn't get system library: %s", err)
93 }
94 libc, err := load.OpenLibrary(library)
95 if err != nil {
96 t.Fatalf("failed to dlopen: %s", err)
97 }
98
99 data := []int{88, 56, 100, 2, 25}
100 sorted := []int{2, 25, 56, 88, 100}
101 compare := func(_ purego.CDecl, a, b *int) int {
102 return *a - *b
103 }
104 var qsort func(data []int, nitms uintptr, size uintptr, compar func(_ purego.CDecl, a, b *int) int)
105 purego.RegisterLibFunc(&qsort, libc, "qsort")
106 qsort(data, uintptr(len(data)), unsafe.Sizeof(int(0)), compare)
107 for i := range data {
108 if data[i] != sorted[i] {
109 t.Errorf("got %d wanted %d at %d", data[i], sorted[i], i)
110 }
111 }
112}
113
114func TestRegisterFunc_Floats(t *testing.T) {
115 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" {
116 t.Skip("Platform doesn't support Floats")
117 return
118 }
119 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
120 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
121 }
122 library, err := getSystemLibrary()
123 if err != nil {
124 t.Fatalf("couldn't get system library: %s", err)
125 }
126 libc, err := load.OpenLibrary(library)
127 if err != nil {
128 t.Fatalf("failed to dlopen: %s", err)
129 }
130 {
131 var strtof func(arg string) float32
132 purego.RegisterLibFunc(&strtof, libc, "strtof")
133 const (
134 arg = "2"
135 )
136 got := strtof(arg)
137 expected := float32(2)
138 if got != expected {
139 t.Errorf("strtof failed. got %f but wanted %f", got, expected)
140 }
141 }
142 {
143 var strtod func(arg string, ptr **byte) float64
144 purego.RegisterLibFunc(&strtod, libc, "strtod")
145 const (
146 arg = "1"
147 )
148 got := strtod(arg, nil)
149 expected := float64(1)
150 if got != expected {
151 t.Errorf("strtod failed. got %f but wanted %f", got, expected)
152 }
153 }
154}
155
156func TestRegisterLibFunc_Bool(t *testing.T) {
157 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" {
158 t.Skip("Platform doesn't support callbacks")
159 return
160 }
161 // this callback recreates the state where the return register
162 // contains other information but the least significant byte is false
163 cbFalse := purego.NewCallback(func() uintptr {
164 x := uint64(0x7F5948AE9A00)
165 return uintptr(x)
166 })
167 var runFalse func() bool
168 purego.RegisterFunc(&runFalse, cbFalse)
169 expected := false
170 if got := runFalse(); got != expected {
171 t.Errorf("runFalse failed. got %t but wanted %t", got, expected)
172 }
173}
174
175func TestABI(t *testing.T) {
176 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
177 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
178 }
179 libFileName := filepath.Join(t.TempDir(), "abitest.so")
180 t.Logf("Build %v", libFileName)
181
182 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
183 t.Fatal(err)
184 }
185
186 lib, err := load.OpenLibrary(libFileName)
187 if err != nil {
188 t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
189 }
190 defer func() {
191 if err := load.CloseLibrary(lib); err != nil {
192 t.Fatalf("failed to close library: %s", err)
193 }
194 }()
195 {
196 const cName = "stack_uint8_t"
197 const expect = 2047
198 var fn func(a, b, c, d, e, f, g, h uint32, i, j uint8, k uint32) uint32
199 purego.RegisterLibFunc(&fn, lib, cName)
200 res := fn(256, 512, 4, 8, 16, 32, 64, 128, 1, 2, 1024)
201 if res != expect {
202 t.Fatalf("%s: got %d, want %d", cName, res, expect)
203 }
204 }
205 {
206 const cName = "reg_uint8_t"
207 const expect = 1027
208 var fn func(a, b uint8, c uint32) uint32
209 purego.RegisterLibFunc(&fn, lib, cName)
210 res := fn(1, 2, 1024)
211 if res != expect {
212 t.Fatalf("%s: got %d, want %d", cName, res, expect)
213 }
214 }
215 {
216 const cName = "stack_string"
217 const expect = 255
218 var fn func(a, b, c, d, e, f, g, h uint32, i string) uint32
219 purego.RegisterLibFunc(&fn, lib, cName)
220 res := fn(1, 2, 4, 8, 16, 32, 64, 128, "test")
221 if res != expect {
222 t.Fatalf("%s: got %d, want %d", cName, res, expect)
223 }
224 }
225 {
226 const cName = "stack_8i32_3strings"
227 var fn func(*byte, uintptr, int32, int32, int32, int32, int32, int32, int32, int32, string, string, string)
228 purego.RegisterLibFunc(&fn, lib, cName)
229 buf := make([]byte, 256)
230 fn(&buf[0], uintptr(len(buf)), 1, 2, 3, 4, 5, 6, 7, 8, "foo", "bar", "baz")
231 res := string(buf[:bytes.IndexByte(buf, 0)])
232 const want = "1:2:3:4:5:6:7:8:foo:bar:baz"
233 if res != want {
234 t.Fatalf("%s: got %q, want %q", cName, res, want)
235 }
236 }
237}
238
239func TestABI_ArgumentPassing(t *testing.T) {
240 if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
241 t.Skip("need a 32bit gcc to run this test") // TODO: find 32bit gcc for test
242 }
243 libFileName := filepath.Join(t.TempDir(), "abitest.so")
244 if err := buildSharedLib("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.GOOS != "darwin" || runtime.GOARCH != "arm64") {
410 t.Skip("20 int32 arguments only supported on Darwin ARM64 with smart stack checking")
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
432func TestABI_TooManyArguments(t *testing.T) {
433 if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
434 t.Skip("This test is specific to Darwin ARM64")
435 }
436
437 libFileName := filepath.Join(t.TempDir(), "abitest.so")
438 if err := buildSharedLib("CC", libFileName, filepath.Join("testdata", "abitest", "abi_test.c")); err != nil {
439 t.Fatal(err)
440 }
441 lib, err := load.OpenLibrary(libFileName)
442 if err != nil {
443 t.Fatalf("Failed to open library %q: %v", libFileName, err)
444 }
445 t.Cleanup(func() {
446 if err := load.CloseLibrary(lib); err != nil {
447 t.Errorf("Failed to close library: %v", err)
448 }
449 })
450
451 // Test that 35 int64 arguments (27 slots needed) exceeds the limit
452 t.Run("35_int64_exceeds_limit", func(t *testing.T) {
453 defer func() {
454 if r := recover(); r != nil {
455 t.Logf("Got expected panic: %v", r)
456 } else {
457 t.Errorf("Expected panic but didn't get one")
458 }
459 }()
460
461 var fn func(*byte, uintptr, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64, int64)
462 purego.RegisterLibFunc(&fn, lib, "stack_35_int64_exceeds")
463 })
464}
465
466func buildSharedLib(compilerEnv, libFile string, sources ...string) error {
467 out, err := exec.Command("go", "env", compilerEnv).Output()
468 if err != nil {
469 return fmt.Errorf("go env %s error: %w", compilerEnv, err)
470 }
471
472 compiler := strings.TrimSpace(string(out))
473 if compiler == "" {
474 return errors.New("compiler not found")
475 }
476
477 args := []string{"-shared", "-Wall", "-Werror", "-fPIC", "-o", libFile}
478 if runtime.GOARCH == "386" {
479 args = append(args, "-m32")
480 }
481 // macOS arm64 can run amd64 tests through Rossetta.
482 // Build the shared library based on the GOARCH and not
483 // the default behavior of the compiler.
484 if runtime.GOOS == "darwin" {
485 var arch string
486 switch runtime.GOARCH {
487 case "arm64":
488 arch = "arm64"
489 case "amd64":
490 arch = "x86_64"
491 default:
492 return fmt.Errorf("unknown macOS architecture %s", runtime.GOARCH)
493 }
494 args = append(args, "-arch", arch)
495 }
496 cmd := exec.Command(compiler, append(args, sources...)...)
497 if out, err := cmd.CombinedOutput(); err != nil {
498 return fmt.Errorf("compile lib: %w\n%q\n%s", err, cmd, string(out))
499 }
500
501 return nil
502}