1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3
4package objc
5
6import (
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "structs"
14 "testing"
15 "unsafe"
16)
17
18type encodeTypeTestStruct struct {
19 _ structs.HostLayout
20 A int32
21 B float64
22}
23
24var encodeTypeTests = []struct {
25 typ reflect.Type
26 cType string
27 want string
28}{
29 {reflect.TypeFor[bool](), "_Bool", "B"},
30 {reflect.TypeFor[int8](), "signed char", "c"},
31 {reflect.TypeFor[uint8](), "unsigned char", "C"},
32 {reflect.TypeFor[int16](), "short", "s"},
33 {reflect.TypeFor[uint16](), "unsigned short", "S"},
34 {reflect.TypeFor[int32](), "int", "i"},
35 {reflect.TypeFor[uint32](), "unsigned int", "I"},
36 {reflect.TypeFor[int64](), "long long", "q"},
37 {reflect.TypeFor[uint64](), "unsigned long long", "Q"},
38 {reflect.TypeFor[int](), "long", "q"},
39 {reflect.TypeFor[uint](), "unsigned long", "Q"},
40 {reflect.TypeFor[float32](), "float", "f"},
41 {reflect.TypeFor[float64](), "double", "d"},
42 {reflect.TypeFor[string](), "char *", "*"},
43 {reflect.TypeFor[unsafe.Pointer](), "void *", "^v"},
44 {reflect.TypeFor[*int32](), "int *", "^i"},
45 {reflect.TypeFor[**int32](), "int **", "^^i"},
46 {reflect.TypeFor[ID](), "id", "@"},
47 {reflect.TypeFor[Class](), "Class", "#"},
48 {reflect.TypeFor[SEL](), "SEL", ":"},
49 {reflect.TypeFor[encodeTypeTestStruct](), "struct encodeTypeTestStruct", "{encodeTypeTestStruct=id}"},
50}
51
52func TestEncodeType(t *testing.T) {
53 for _, tt := range encodeTypeTests {
54 got, err := encodeType(tt.typ, false)
55 if err != nil {
56 t.Errorf("encodeType(%v) returned error: %v", tt.typ, err)
57 continue
58 }
59 if got != tt.want {
60 t.Errorf("encodeType(%v) = %q; want %q (@encode(%s))", tt.typ, got, tt.want, tt.cType)
61 }
62 }
63}
64
65// TestEncodeTypeMatchesClang uses the C compiler as an oracle: it compiles a
66// program that prints @encode for each C type in encodeTypeTests and checks
67// that the expected encodings are the ones the compiler actually produces.
68func TestEncodeTypeMatchesClang(t *testing.T) {
69 out, err := exec.Command("go", "env", "CC").Output()
70 if err != nil {
71 t.Fatalf("go env CC: %v", err)
72 }
73 compiler := strings.TrimSpace(string(out))
74 if compiler == "" {
75 t.Skip("no C compiler to use as an @encode oracle")
76 }
77 if _, err := exec.LookPath(compiler); err != nil {
78 t.Skipf("no C compiler to use as an @encode oracle: %v", err)
79 }
80
81 var src strings.Builder
82 src.WriteString("#include <stdio.h>\n#include <objc/objc.h>\n")
83 src.WriteString("struct encodeTypeTestStruct { int a; double b; };\n")
84 src.WriteString("int main(void) {\n")
85 for _, tt := range encodeTypeTests {
86 fmt.Fprintf(&src, "\tprintf(\"%%s\\n\", @encode(%s));\n", tt.cType)
87 }
88 src.WriteString("\treturn 0;\n}\n")
89
90 dir := t.TempDir()
91 srcFile := filepath.Join(dir, "encode.m")
92 if err := os.WriteFile(srcFile, []byte(src.String()), 0o644); err != nil {
93 t.Fatal(err)
94 }
95
96 // Built for the host architecture rather than GOARCH, unlike the shared
97 // libraries in the root package's tests: every darwin target Go supports
98 // is LP64, so these encodings do not vary by architecture, and building
99 // for the host avoids needing Rosetta to run an amd64 binary on arm64.
100 exeFile := filepath.Join(dir, "encode")
101 cmd := exec.Command(compiler, "-Wall", "-Werror", "-o", exeFile, srcFile)
102 if out, err := cmd.CombinedOutput(); err != nil {
103 t.Fatalf("compile oracle: %v\n%q\n%s", err, cmd, out)
104 }
105
106 out, err = exec.Command(exeFile).Output()
107 if err != nil {
108 t.Fatalf("run oracle: %v", err)
109 }
110 lines := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n")
111 if len(lines) != len(encodeTypeTests) {
112 t.Fatalf("oracle printed %d encodings; want %d", len(lines), len(encodeTypeTests))
113 }
114 for i, tt := range encodeTypeTests {
115 if lines[i] != tt.want {
116 t.Errorf("@encode(%s) = %q; want %q", tt.cType, lines[i], tt.want)
117 }
118 }
119}
120
121// TestEncodeFunc checks how encodeFunc assembles a method signature: the
122// return type, the implicit self and _cmd parameters, and the argument order.
123// encodeType is checked per type by TestEncodeType, so the cases here vary the
124// shape of the signature rather than the types in it. The first case is the
125// signature from issue #493, which was recorded as v@:LLLLLLLLS - declaring
126// 64-bit arguments as 32-bit made NSInvocation and other consumers of the type
127// encoding truncate values above 2^32.
128func TestEncodeFunc(t *testing.T) {
129 tests := []struct {
130 name string
131 fn any
132 want string
133 }{
134 {
135 name: "void return, 64-bit args",
136 fn: func(_ ID, _ SEL, a1, a2, a3, a4, a5, a6, a7, a8 uint64, a9 uint16) {},
137 want: "v@:QQQQQQQQS",
138 },
139 {
140 name: "value return, mixed integer kinds",
141 fn: func(_ ID, _ SEL, a int, b int64, c uint, d uint64) int { return 0 },
142 want: "q@:qqQQ",
143 },
144 {
145 name: "no arguments",
146 fn: func(_ ID, _ SEL) {},
147 want: "v@:",
148 },
149 {
150 name: "argument order is preserved",
151 fn: func(_ ID, _ SEL, a int8, b float64, c ID) Class { return 0 },
152 want: "#@:cd@",
153 },
154 }
155 for _, tt := range tests {
156 t.Run(tt.name, func(t *testing.T) {
157 got, err := encodeFunc(tt.fn)
158 if err != nil {
159 t.Fatal(err)
160 }
161 if got != tt.want {
162 t.Errorf("encodeFunc = %q; want %q", got, tt.want)
163 }
164 })
165 }
166}
167
168func TestEncodeFuncErrors(t *testing.T) {
169 tests := []struct {
170 name string
171 fn any
172 }{
173 {"not a func", 0},
174 {"too many return values", func(_ ID, _ SEL) (int, int) { return 0, 0 }},
175 {"missing self and _cmd", func() {}},
176 {"missing _cmd", func(_ ID) {}},
177 {"unencodable argument", func(_ ID, _ SEL, c chan int) {}},
178 }
179 for _, tt := range tests {
180 t.Run(tt.name, func(t *testing.T) {
181 if got, err := encodeFunc(tt.fn); err == nil {
182 t.Errorf("encodeFunc = %q; want an error", got)
183 }
184 })
185 }
186}