Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions objc/encoding_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors

package objc

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"unsafe"
)

type encodeTypeTestStruct struct {
A int32
B float64
}

var encodeTypeTests = []struct {
typ reflect.Type
cType string
want string
}{
{reflect.TypeFor[bool](), "_Bool", "B"},
{reflect.TypeFor[int8](), "signed char", "c"},
{reflect.TypeFor[uint8](), "unsigned char", "C"},
{reflect.TypeFor[int16](), "short", "s"},
{reflect.TypeFor[uint16](), "unsigned short", "S"},
{reflect.TypeFor[int32](), "int", "i"},
{reflect.TypeFor[uint32](), "unsigned int", "I"},
{reflect.TypeFor[int64](), "long long", "q"},
{reflect.TypeFor[uint64](), "unsigned long long", "Q"},
{reflect.TypeFor[int](), "long", "q"},
{reflect.TypeFor[uint](), "unsigned long", "Q"},
{reflect.TypeFor[float32](), "float", "f"},
{reflect.TypeFor[float64](), "double", "d"},
{reflect.TypeFor[string](), "char *", "*"},
{reflect.TypeFor[unsafe.Pointer](), "void *", "^v"},
{reflect.TypeFor[*int32](), "int *", "^i"},
{reflect.TypeFor[**int32](), "int **", "^^i"},
{reflect.TypeFor[ID](), "id", "@"},
{reflect.TypeFor[Class](), "Class", "#"},
{reflect.TypeFor[SEL](), "SEL", ":"},
{reflect.TypeFor[encodeTypeTestStruct](), "struct encodeTypeTestStruct", "{encodeTypeTestStruct=id}"},
}

func TestEncodeType(t *testing.T) {
for _, tt := range encodeTypeTests {
got, err := encodeType(tt.typ, false)
if err != nil {
t.Errorf("encodeType(%v) returned error: %v", tt.typ, err)
continue
}
if got != tt.want {
t.Errorf("encodeType(%v) = %q; want %q (@encode(%s))", tt.typ, got, tt.want, tt.cType)
}
}
}

// TestEncodeTypeMatchesClang uses the C compiler as an oracle: it compiles a
// program that prints @encode for each C type in encodeTypeTests and checks
// that the expected encodings are the ones the compiler actually produces.
func TestEncodeTypeMatchesClang(t *testing.T) {
out, err := exec.Command("go", "env", "CC").Output()
if err != nil {
t.Fatalf("go env CC: %v", err)
}
compiler := strings.TrimSpace(string(out))
if compiler == "" {
t.Skip("no C compiler to use as an @encode oracle")
}
if _, err := exec.LookPath(compiler); err != nil {
t.Skipf("no C compiler to use as an @encode oracle: %v", err)
}

var src strings.Builder
src.WriteString("#include <stdio.h>\n#include <objc/objc.h>\n")
src.WriteString("struct encodeTypeTestStruct { int a; double b; };\n")
src.WriteString("int main(void) {\n")
for _, tt := range encodeTypeTests {
fmt.Fprintf(&src, "\tprintf(\"%%s\\n\", @encode(%s));\n", tt.cType)
}
src.WriteString("\treturn 0;\n}\n")

dir := t.TempDir()
srcFile := filepath.Join(dir, "encode.m")
if err := os.WriteFile(srcFile, []byte(src.String()), 0o644); err != nil {
t.Fatal(err)
}

// Built for the host architecture rather than GOARCH, unlike the shared
// libraries in the root package's tests: every darwin target Go supports
// is LP64, so these encodings do not vary by architecture, and building
// for the host avoids needing Rosetta to run an amd64 binary on arm64.
exeFile := filepath.Join(dir, "encode")
cmd := exec.Command(compiler, "-Wall", "-Werror", "-o", exeFile, srcFile)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("compile oracle: %v\n%q\n%s", err, cmd, out)
}

out, err = exec.Command(exeFile).Output()
if err != nil {
t.Fatalf("run oracle: %v", err)
}
lines := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
if len(lines) != len(encodeTypeTests) {
t.Fatalf("oracle printed %d encodings; want %d", len(lines), len(encodeTypeTests))
}
for i, tt := range encodeTypeTests {
if lines[i] != tt.want {
t.Errorf("@encode(%s) = %q; want %q", tt.cType, lines[i], tt.want)
}
}
}

// TestEncodeFunc checks how encodeFunc assembles a method signature: the
// return type, the implicit self and _cmd parameters, and the argument order.
// encodeType is checked per type by TestEncodeType, so the cases here vary the
// shape of the signature rather than the types in it. The first case is the
// signature from issue #493, which was recorded as v@:LLLLLLLLS - declaring
// 64-bit arguments as 32-bit made NSInvocation and other consumers of the type
// encoding truncate values above 2^32.
func TestEncodeFunc(t *testing.T) {
tests := []struct {
name string
fn any
want string
}{
{
name: "void return, 64-bit args",
fn: func(_ ID, _ SEL, a1, a2, a3, a4, a5, a6, a7, a8 uint64, a9 uint16) {},
want: "v@:QQQQQQQQS",
},
{
name: "value return, mixed integer kinds",
fn: func(_ ID, _ SEL, a int, b int64, c uint, d uint64) int { return 0 },
want: "q@:qqQQ",
},
{
name: "no arguments",
fn: func(_ ID, _ SEL) {},
want: "v@:",
},
{
name: "argument order is preserved",
fn: func(_ ID, _ SEL, a int8, b float64, c ID) Class { return 0 },
want: "#@:cd@",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := encodeFunc(tt.fn)
if err != nil {
t.Fatal(err)
}
if got != tt.want {
t.Errorf("encodeFunc = %q; want %q", got, tt.want)
}
})
}
}

func TestEncodeFuncErrors(t *testing.T) {
tests := []struct {
name string
fn any
}{
{"not a func", 0},
{"too many return values", func(_ ID, _ SEL) (int, int) { return 0, 0 }},
{"missing self and _cmd", func() {}},
{"missing _cmd", func(_ ID) {}},
{"unencodable argument", func(_ ID, _ SEL, c chan int) {}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got, err := encodeFunc(tt.fn); err == nil {
t.Errorf("encodeFunc = %q; want an error", got)
}
})
}
}
12 changes: 6 additions & 6 deletions objc/objc_runtime_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,8 @@ const (
encUShort = "S"
encInt = "i"
encUInt = "I"
encLong = "l"
encULong = "L"
Comment thread
hajimehoshi marked this conversation as resolved.
encLongLong = "q"
encULongLong = "Q"
encFloat = "f"
encDouble = "d"
encBool = "B"
Expand Down Expand Up @@ -432,25 +432,25 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
case reflect.Bool:
return encBool, nil
case reflect.Int:
return encLong, nil
return encLongLong, nil
case reflect.Int8:
return encChar, nil
case reflect.Int16:
return encShort, nil
case reflect.Int32:
return encInt, nil
case reflect.Int64:
return encULong, nil
return encLongLong, nil
case reflect.Uint:
return encULong, nil
return encULongLong, nil
case reflect.Uint8:
return encUChar, nil
case reflect.Uint16:
return encUShort, nil
case reflect.Uint32:
return encUInt, nil
case reflect.Uint64:
return encULong, nil
return encULongLong, nil
case reflect.Uintptr:
return encPtr, nil
case reflect.Float32:
Expand Down