v0.11.0-alpha.8 d476432

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"
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}

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