v0.11.0-bnema.4 cf1b83e

func_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close