purego: support []string and array values in RegisterFunc
- commit
- d38bc817822f21e2b5298c1ab2342a47ec14f3c5
- author
- brice · · 2026-09-27 06:02 UTC
- parent
- c0b72dd
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 175 | 175 | } | |
| 176 | 176 | case reflect.String, reflect.Uintptr, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, | |
| 177 | 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 | 179 | if ints < numOfIntegerRegisters() { | |
| 180 | 180 | ints++ | |
| 181 | 181 | } else { | |
| @@ -461,6 +461,19 @@ func RegisterFunc(fptr any, cfn uintptr) { | |||
| 461 | 461 | } | |
| 462 | 462 | case reflect.Struct: | |
| 463 | 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 | 477 | default: | |
| 465 | 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 | 516 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32: | |
| 504 | 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 | 542 | // There is no need to keepAlive this pointer separately because it is kept alive in the args variable | |
| 507 | 543 | addInt(v.Pointer()) | |
| 508 | 544 | 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 variable | 542 | // 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 | + | } | |||