detached d38bc81

purego: support []string and array values in RegisterFunc

commit
d38bc817822f21e2b5298c1ab2342a47ec14f3c5
author
brice · · 2026-09-27 06:02 UTC
parent
c0b72dd
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
tag: v0.12.0-bnema.1
3 files · +133 -2
M func.go +38 -2
@@ -175,7 +175,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
175175 }
176176 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
177177 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Pointer, reflect.UnsafePointer,
178- reflect.Slice, reflect.Bool:
178+ reflect.Slice, reflect.Array, reflect.Bool:
179179 if ints < numOfIntegerRegisters() {
180180 ints++
181181 } else {
@@ -461,6 +461,19 @@ func RegisterFunc(fptr any, cfn uintptr) {
461461 }
462462 case reflect.Struct:
463463 v = getStruct(outType, *syscall)
464+ case reflect.Slice:
465+ if outType.Elem().Kind() != reflect.String {
466+ panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
467+ }
468+ // C returns a NULL-terminated char** array.
469+ if syscall.a1 != 0 {
470+ var result []string
471+ p := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
472+ for ; *(*uintptr)(p) != 0; p = unsafe.Add(p, unsafe.Sizeof(uintptr(0))) {
473+ result = append(result, strings.GoString(*(*uintptr)(p)))
474+ }
475+ v = reflect.ValueOf(result).Convert(outType)
476+ }
464477 default:
465478 panic("purego: unsupported return kind: " + outType.Kind().String())
466479 }
@@ -502,7 +515,30 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
502515 }
503516 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
504517 addInt(uintptr(v.Int()))
505- case reflect.Pointer, reflect.UnsafePointer, reflect.Slice:
518+ case reflect.Slice:
519+ if v.Type().Elem().Kind() != reflect.String {
520+ // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
521+ addInt(v.Pointer())
522+ break
523+ }
524+ if v.IsNil() {
525+ addInt(0)
526+ break
527+ }
528+ // Pass []string as a NULL-terminated char** array.
529+ res := make([]*byte, v.Len()+1)
530+ for i := range v.Len() {
531+ res[i] = strings.CString(v.Index(i).String())
532+ }
533+ keepAlive = append(keepAlive, res)
534+ addInt(uintptr(unsafe.Pointer(&res[0])))
535+ case reflect.Array:
536+ // C array parameters decay to pointers, so pass a pointer to a copy.
537+ tmp := reflect.New(v.Type())
538+ tmp.Elem().Set(v)
539+ keepAlive = append(keepAlive, tmp)
540+ addInt(tmp.Pointer())
541+ case reflect.Pointer, reflect.UnsafePointer:
506542 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
507543 addInt(v.Pointer())
508544 case reflect.Func:
@@ -175,7 +175,7 @@ func RegisterFunc(fptr any, cfn uintptr) {
175 }175 }
176 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,176 case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32,
177 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Pointer, reflect.UnsafePointer,177 reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Pointer, reflect.UnsafePointer,
178- reflect.Slice, reflect.Bool:178+ reflect.Slice, reflect.Array, reflect.Bool:
179 if ints < numOfIntegerRegisters() {179 if ints < numOfIntegerRegisters() {
180 ints++180 ints++
181 } else {181 } else {
@@ -461,6 +461,19 @@ func RegisterFunc(fptr any, cfn uintptr) {
461 }461 }
462 case reflect.Struct:462 case reflect.Struct:
463 v = getStruct(outType, *syscall)463 v = getStruct(outType, *syscall)
464+ case reflect.Slice:
465+ if outType.Elem().Kind() != reflect.String {
466+ panic("purego: unsupported return slice element kind: " + outType.Elem().Kind().String())
467+ }
468+ // C returns a NULL-terminated char** array.
469+ if syscall.a1 != 0 {
470+ var result []string
471+ p := *(*unsafe.Pointer)(unsafe.Pointer(&syscall.a1))
472+ for ; *(*uintptr)(p) != 0; p = unsafe.Add(p, unsafe.Sizeof(uintptr(0))) {
473+ result = append(result, strings.GoString(*(*uintptr)(p)))
474+ }
475+ v = reflect.ValueOf(result).Convert(outType)
476+ }
464 default:477 default:
465 panic("purego: unsupported return kind: " + outType.Kind().String())478 panic("purego: unsupported return kind: " + outType.Kind().String())
466 }479 }
@@ -502,7 +515,30 @@ func addValue(v reflect.Value, keepAlive []any, addInt func(x uintptr), addFloat
502 }515 }
503 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:516 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
504 addInt(uintptr(v.Int()))517 addInt(uintptr(v.Int()))
505- case reflect.Pointer, reflect.UnsafePointer, reflect.Slice:518+ case reflect.Slice:
519+ if v.Type().Elem().Kind() != reflect.String {
520+ // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
521+ addInt(v.Pointer())
522+ break
523+ }
524+ if v.IsNil() {
525+ addInt(0)
526+ break
527+ }
528+ // Pass []string as a NULL-terminated char** array.
529+ res := make([]*byte, v.Len()+1)
530+ for i := range v.Len() {
531+ res[i] = strings.CString(v.Index(i).String())
532+ }
533+ keepAlive = append(keepAlive, res)
534+ addInt(uintptr(unsafe.Pointer(&res[0])))
535+ case reflect.Array:
536+ // C array parameters decay to pointers, so pass a pointer to a copy.
537+ tmp := reflect.New(v.Type())
538+ tmp.Elem().Set(v)
539+ keepAlive = append(keepAlive, tmp)
540+ addInt(tmp.Pointer())
541+ case reflect.Pointer, reflect.UnsafePointer:
506 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable542 // There is no need to keepAlive this pointer separately because it is kept alive in the args variable
507 addInt(v.Pointer())543 addInt(v.Pointer())
508 case reflect.Func:544 case reflect.Func:
A func_slice_test.go +61 -0
@@ -0,0 +1,61 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd
5+
6+package purego_test
7+
8+import (
9+ "path/filepath"
10+ "slices"
11+ "testing"
12+
13+ "github.com/bnema/purego"
14+ "github.com/bnema/purego/internal/load"
15+)
16+
17+func TestRegisterFuncStringSliceAndArray(t *testing.T) {
18+ libFileName := filepath.Join(t.TempDir(), "slicetest.so")
19+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "slicetest", "slice_test.c")); err != nil {
20+ t.Fatal(err)
21+ }
22+ lib, err := load.OpenLibrary(libFileName)
23+ if err != nil {
24+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
25+ }
26+ defer load.CloseLibrary(lib)
27+
28+ var join func([]string) string
29+ purego.RegisterLibFunc(&join, lib, "join_strings")
30+ var get func(int32) []string
31+ purego.RegisterLibFunc(&get, lib, "get_strings")
32+ var sum func([4]float32) float32
33+ purego.RegisterLibFunc(&sum, lib, "sum_floats4")
34+
35+ t.Run("[]string argument", func(t *testing.T) {
36+ if got := join([]string{"a", "bc", "def"}); got != "a,bc,def" {
37+ t.Errorf("join = %q, want %q", got, "a,bc,def")
38+ }
39+ if got := join([]string{}); got != "" {
40+ t.Errorf("join(empty) = %q, want empty", got)
41+ }
42+ if got := join(nil); got != "(null)" {
43+ t.Errorf("join(nil) = %q, want %q", got, "(null)")
44+ }
45+ })
46+
47+ t.Run("[]string return", func(t *testing.T) {
48+ if got, want := get(0), []string{"alpha", "beta", "gamma"}; !slices.Equal(got, want) {
49+ t.Errorf("get = %q, want %q", got, want)
50+ }
51+ if got := get(1); got != nil {
52+ t.Errorf("get(NULL) = %q, want nil", got)
53+ }
54+ })
55+
56+ t.Run("array argument", func(t *testing.T) {
57+ if got := sum([4]float32{1, 2, 3, 4.5}); got != 10.5 {
58+ t.Errorf("sum = %v, want 10.5", got)
59+ }
60+ })
61+}
@@ -0,0 +1,61 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+//go:build darwin || freebsd || linux || netbsd
5+
6+package purego_test
7+
8+import (
9+ "path/filepath"
10+ "slices"
11+ "testing"
12+
13+ "github.com/bnema/purego"
14+ "github.com/bnema/purego/internal/load"
15+)
16+
17+func TestRegisterFuncStringSliceAndArray(t *testing.T) {
18+ libFileName := filepath.Join(t.TempDir(), "slicetest.so")
19+ if err := buildSharedLib(t, "CC", libFileName, filepath.Join("testdata", "slicetest", "slice_test.c")); err != nil {
20+ t.Fatal(err)
21+ }
22+ lib, err := load.OpenLibrary(libFileName)
23+ if err != nil {
24+ t.Fatalf("Dlopen(%q) failed: %v", libFileName, err)
25+ }
26+ defer load.CloseLibrary(lib)
27+
28+ var join func([]string) string
29+ purego.RegisterLibFunc(&join, lib, "join_strings")
30+ var get func(int32) []string
31+ purego.RegisterLibFunc(&get, lib, "get_strings")
32+ var sum func([4]float32) float32
33+ purego.RegisterLibFunc(&sum, lib, "sum_floats4")
34+
35+ t.Run("[]string argument", func(t *testing.T) {
36+ if got := join([]string{"a", "bc", "def"}); got != "a,bc,def" {
37+ t.Errorf("join = %q, want %q", got, "a,bc,def")
38+ }
39+ if got := join([]string{}); got != "" {
40+ t.Errorf("join(empty) = %q, want empty", got)
41+ }
42+ if got := join(nil); got != "(null)" {
43+ t.Errorf("join(nil) = %q, want %q", got, "(null)")
44+ }
45+ })
46+
47+ t.Run("[]string return", func(t *testing.T) {
48+ if got, want := get(0), []string{"alpha", "beta", "gamma"}; !slices.Equal(got, want) {
49+ t.Errorf("get = %q, want %q", got, want)
50+ }
51+ if got := get(1); got != nil {
52+ t.Errorf("get(NULL) = %q, want nil", got)
53+ }
54+ })
55+
56+ t.Run("array argument", func(t *testing.T) {
57+ if got := sum([4]float32{1, 2, 3, 4.5}); got != 10.5 {
58+ t.Errorf("sum = %v, want 10.5", got)
59+ }
60+ })
61+}
A testdata/slicetest/slice_test.c +34 -0
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <stddef.h>
5+#include <string.h>
6+
7+static char buf[256];
8+
9+const char *join_strings(const char **strs) {
10+ buf[0] = '\0';
11+ if (strs == NULL) {
12+ return "(null)";
13+ }
14+ for (size_t i = 0; strs[i] != NULL; i++) {
15+ if (i > 0) {
16+ strcat(buf, ",");
17+ }
18+ strcat(buf, strs[i]);
19+ }
20+ return buf;
21+}
22+
23+static const char *items[] = {"alpha", "beta", "gamma", NULL};
24+
25+const char **get_strings(int empty) {
26+ if (empty) {
27+ return NULL;
28+ }
29+ return items;
30+}
31+
32+float sum_floats4(const float v[4]) {
33+ return v[0] + v[1] + v[2] + v[3];
34+}
@@ -0,0 +1,34 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#include <stddef.h>
5+#include <string.h>
6+
7+static char buf[256];
8+
9+const char *join_strings(const char **strs) {
10+ buf[0] = '\0';
11+ if (strs == NULL) {
12+ return "(null)";
13+ }
14+ for (size_t i = 0; strs[i] != NULL; i++) {
15+ if (i > 0) {
16+ strcat(buf, ",");
17+ }
18+ strcat(buf, strs[i]);
19+ }
20+ return buf;
21+}
22+
23+static const char *items[] = {"alpha", "beta", "gamma", NULL};
24+
25+const char **get_strings(int empty) {
26+ if (empty) {
27+ return NULL;
28+ }
29+ return items;
30+}
31+
32+float sum_floats4(const float v[4]) {
33+ return v[0] + v[1] + v[2] + v[3];
34+}

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