v0.11.0-alpha.9 f9d41fc

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

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