detached 440e526

objc: add SendSuper2 with an explicit defining class (#534)

SendSuper derives the dispatch class from the receiver's runtime
class, which can cause inherited overrides to recursively reenter
themselves.

Add ID.SendSuper2 and SendSuper2[T] with an explicit defining class
so callers can correctly dispatch to its superclass. Deprecate the
existing SendSuper helpers while preserving their behavior for
compatibility during migration.

Update examples and add regression tests for direct and inherited
calls, argument and receiver forwarding, return values, and amd64
struct-return dispatch.

Closes #533

Co-Authored-By: Codex <noreply@openai.com>
commit
440e5267f37d05c3604695a3661841daa6ee304b
author
Hajime Hoshi · · 2026-09-24 16:30 UTC
committer
GitHub · · 2026-09-24 16:30 UTC
parent
656af01
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
5 files · +236 -35
M examples/objc/main_darwin.go +1 -1
@@ -18,7 +18,7 @@ var (
1818 )
1919
2020 func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21- return id.SendSuper(cmd)
21+ return id.SendSuper2(objc.GetClass("BarObject"), cmd)
2222 }
2323
2424 func main() {
@@ -18,7 +18,7 @@ var (
18 )18 )
19 19
20 func BarInit(id objc.ID, cmd objc.SEL) objc.ID {20 func BarInit(id objc.ID, cmd objc.SEL) objc.ID {
21- return id.SendSuper(cmd)21+ return id.SendSuper2(objc.GetClass("BarObject"), cmd)
22 }22 }
23 23
24 func main() {24 func main() {
M objc/objc_runtime_darwin.go +22 -14
@@ -171,36 +171,44 @@ func Send[T any](id ID, sel SEL, args ...any) T {
171171 return fn(id, sel, args...)
172172 }
173173
174-// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
175-// as the receiver of a message.
176-//
177-// [SendSuper] calls objc_msgSendSuper2 instead of objc_msgSendSuper. Unlike objc_msgSendSuper, which expects
178-// the superclass to message, objc_msgSendSuper2 expects the current class and begins looking the method up
179-// in its superclass by itself, so the receiver's own class is what belongs in superClass.
174+// objc_super supplies the receiver and defining class to objc_msgSendSuper2,
175+// which begins method lookup in that class's superclass.
180176 type objc_super struct {
181177 _ structs.HostLayout
182178 receiver ID
183179 superClass Class
184180 }
185181
186-// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
187-// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
188-// of RegisterName.
182+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
183+//
184+// Deprecated: Use [ID.SendSuper2] with the class defining the executing method.
189185 func (id ID) SendSuper(sel SEL, args ...any) ID {
186+ return id.SendSuper2(id.Class(), sel, args...)
187+}
188+
189+// SendSuper2 sends a message starting lookup in the superclass of class.
190+// For a super call, class must be the class defining the executing method.
191+func (id ID) SendSuper2(class Class, sel SEL, args ...any) ID {
190192 super := &objc_super{
191193 receiver: id,
192- superClass: id.Class(),
194+ superClass: class,
193195 }
194196 return objc_msgSendSuper2(super, sel, args...)
195197 }
196198
197-// SendSuper is a convenience method for sending message to object's super that can return any type.
198-// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
199-// It is best to cache the result of RegisterName.
199+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
200+//
201+// Deprecated: Use [SendSuper2] with the class defining the executing method.
200202 func SendSuper[T any](id ID, sel SEL, args ...any) T {
203+ return SendSuper2[T](id, id.Class(), sel, args...)
204+}
205+
206+// SendSuper2 sends a message starting lookup in the superclass of class and returns T.
207+// For a super call, class must be the class defining the executing method.
208+func SendSuper2[T any](id ID, class Class, sel SEL, args ...any) T {
201209 super := &objc_super{
202210 receiver: id,
203- superClass: id.Class(),
211+ superClass: class,
204212 }
205213 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
206214 var zero T
@@ -171,36 +171,44 @@ func Send[T any](id ID, sel SEL, args ...any) T {
171 return fn(id, sel, args...)171 return fn(id, sel, args...)
172 }172 }
173 173
174-// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword174+// objc_super supplies the receiver and defining class to objc_msgSendSuper2,
175-// as the receiver of a message.175+// which begins method lookup in that class's superclass.
176-//
177-// [SendSuper] calls objc_msgSendSuper2 instead of objc_msgSendSuper. Unlike objc_msgSendSuper, which expects
178-// the superclass to message, objc_msgSendSuper2 expects the current class and begins looking the method up
179-// in its superclass by itself, so the receiver's own class is what belongs in superClass.
180 type objc_super struct {176 type objc_super struct {
181 _ structs.HostLayout177 _ structs.HostLayout
182 receiver ID178 receiver ID
183 superClass Class179 superClass Class
184 }180 }
185 181
186-// SendSuper is a convenience method for sending message to object's super. This function takes a SEL182+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
187-// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result183+//
188-// of RegisterName.184+// Deprecated: Use [ID.SendSuper2] with the class defining the executing method.
189 func (id ID) SendSuper(sel SEL, args ...any) ID {185 func (id ID) SendSuper(sel SEL, args ...any) ID {
186+ return id.SendSuper2(id.Class(), sel, args...)
187+}
188+
189+// SendSuper2 sends a message starting lookup in the superclass of class.
190+// For a super call, class must be the class defining the executing method.
191+func (id ID) SendSuper2(class Class, sel SEL, args ...any) ID {
190 super := &objc_super{192 super := &objc_super{
191 receiver: id,193 receiver: id,
192- superClass: id.Class(),194+ superClass: class,
193 }195 }
194 return objc_msgSendSuper2(super, sel, args...)196 return objc_msgSendSuper2(super, sel, args...)
195 }197 }
196 198
197-// SendSuper is a convenience method for sending message to object's super that can return any type.199+// SendSuper sends a message starting lookup in the receiver's runtime superclass.
198-// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.200+//
199-// It is best to cache the result of RegisterName.201+// Deprecated: Use [SendSuper2] with the class defining the executing method.
200 func SendSuper[T any](id ID, sel SEL, args ...any) T {202 func SendSuper[T any](id ID, sel SEL, args ...any) T {
203+ return SendSuper2[T](id, id.Class(), sel, args...)
204+}
205+
206+// SendSuper2 sends a message starting lookup in the superclass of class and returns T.
207+// For a super call, class must be the class defining the executing method.
208+func SendSuper2[T any](id ID, class Class, sel SEL, args ...any) T {
201 super := &objc_super{209 super := &objc_super{
202 receiver: id,210 receiver: id,
203- superClass: id.Class(),211+ superClass: class,
204 }212 }
205 var fn func(objcSuper *objc_super, sel SEL, args ...any) T213 var fn func(objcSuper *objc_super, sel SEL, args ...any) T
206 var zero T214 var zero T
M objc/objc_runtime_darwin_test.go +50 -20
@@ -39,6 +39,7 @@ func ExampleRegisterClass_helloworld() {
3939 }
4040
4141 func ExampleRegisterClass() {
42+ var class objc.Class
4243 var (
4344 sel_new = objc.RegisterName("new")
4445 sel_init = objc.RegisterName("init")
@@ -46,11 +47,12 @@ func ExampleRegisterClass() {
4647 sel_bar = objc.RegisterName("bar")
4748
4849 BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {
49- return id.SendSuper(cmd)
50+ return id.SendSuper2(class, cmd)
5051 }
5152 )
5253
53- class, err := objc.RegisterClass(
54+ var err error
55+ class, err = objc.RegisterClass(
5456 "BarObject",
5557 objc.GetClass("NSObject"),
5658 []*objc.Protocol{
@@ -95,17 +97,25 @@ func ExampleIMP() {
9597 // Output: IMP: 105 567 9 2 3 -5 4 8 9
9698 }
9799
98-func ExampleID_SendSuper() {
99- super, err := objc.RegisterClass(
100- "SuperObject",
100+// This example shows three-level inheritance with Base <- Child1 <- Child2.
101+// SendSuper2 takes Child1, the class defining the method.
102+func ExampleID_SendSuper2() {
103+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
104+ if err != nil {
105+ panic(err)
106+ }
107+
108+ sel := objc.RegisterName("run")
109+ base, err := objc.RegisterClass(
110+ "SuperExampleBase",
101111 objc.GetClass("NSObject"),
102112 nil,
103113 nil,
104114 []objc.MethodDef{
105115 {
106- Cmd: objc.RegisterName("doSomething"),
107- Fn: func(self objc.ID, _cmd objc.SEL) {
108- fmt.Println("In Super!")
116+ Cmd: sel,
117+ Fn: func(self objc.ID, cmd objc.SEL) {
118+ fmt.Println("Base")
109119 },
110120 },
111121 },
@@ -114,17 +124,19 @@ func ExampleID_SendSuper() {
114124 panic(err)
115125 }
116126
117- child, err := objc.RegisterClass(
118- "ChildObject",
119- super,
127+ var child1 objc.Class
128+ child1, err = objc.RegisterClass(
129+ "SuperExampleChild1",
130+ base,
120131 nil,
121132 nil,
122133 []objc.MethodDef{
123134 {
124- Cmd: objc.RegisterName("doSomething"),
125- Fn: func(self objc.ID, _cmd objc.SEL) {
126- fmt.Println("In Child")
127- self.SendSuper(_cmd)
135+ Cmd: sel,
136+ Fn: func(self objc.ID, cmd objc.SEL) {
137+ // Start lookup in the superclass of Child1.
138+ fmt.Print("SendSuper2: ")
139+ self.SendSuper2(child1, cmd)
128140 },
129141 },
130142 },
@@ -133,9 +145,27 @@ func ExampleID_SendSuper() {
133145 panic(err)
134146 }
135147
136- objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething"))
137- // Output: In Child
138- // In Super!
148+ // Child2 inherits Child1's implementation of run.
149+ child2, err := objc.RegisterClass("SuperExampleChild2", child1, nil, nil, nil)
150+ if err != nil {
151+ panic(err)
152+ }
153+
154+ for _, class := range []objc.Class{child1, child2} {
155+ object := objc.ID(class).Send(objc.RegisterName("new"))
156+ defer object.Send(objc.RegisterName("release"))
157+ if class == child1 {
158+ fmt.Println("Child1 instance")
159+ } else {
160+ fmt.Println("Child2 instance")
161+ }
162+ object.Send(sel)
163+ }
164+ // Output:
165+ // Child1 instance
166+ // SendSuper2: Base
167+ // Child2 instance
168+ // SendSuper2: Base
139169 }
140170
141171 func TestSend(t *testing.T) {
@@ -170,7 +200,7 @@ func ExampleSend() {
170200 // Output: 3 7
171201 }
172202
173-func ExampleSendSuper() {
203+func ExampleSendSuper2() {
174204 super, err := objc.RegisterClass(
175205 "SuperObject2",
176206 objc.GetClass("NSObject"),
@@ -207,7 +237,7 @@ func ExampleSendSuper() {
207237 panic(err)
208238 }
209239
210- res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething"))
240+ res := objc.SendSuper2[int](objc.ID(child).Send(objc.RegisterName("new")), child, objc.RegisterName("doSomething"))
211241 fmt.Println(res)
212242 // Output: 16
213243 }
@@ -39,6 +39,7 @@ func ExampleRegisterClass_helloworld() {
39 }39 }
40 40
41 func ExampleRegisterClass() {41 func ExampleRegisterClass() {
42+ var class objc.Class
42 var (43 var (
43 sel_new = objc.RegisterName("new")44 sel_new = objc.RegisterName("new")
44 sel_init = objc.RegisterName("init")45 sel_init = objc.RegisterName("init")
@@ -46,11 +47,12 @@ func ExampleRegisterClass() {
46 sel_bar = objc.RegisterName("bar")47 sel_bar = objc.RegisterName("bar")
47 48
48 BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {49 BarInit = func(id objc.ID, cmd objc.SEL) objc.ID {
49- return id.SendSuper(cmd)50+ return id.SendSuper2(class, cmd)
50 }51 }
51 )52 )
52 53
53- class, err := objc.RegisterClass(54+ var err error
55+ class, err = objc.RegisterClass(
54 "BarObject",56 "BarObject",
55 objc.GetClass("NSObject"),57 objc.GetClass("NSObject"),
56 []*objc.Protocol{58 []*objc.Protocol{
@@ -95,17 +97,25 @@ func ExampleIMP() {
95 // Output: IMP: 105 567 9 2 3 -5 4 8 997 // Output: IMP: 105 567 9 2 3 -5 4 8 9
96 }98 }
97 99
98-func ExampleID_SendSuper() {100+// This example shows three-level inheritance with Base <- Child1 <- Child2.
99- super, err := objc.RegisterClass(101+// SendSuper2 takes Child1, the class defining the method.
100- "SuperObject",102+func ExampleID_SendSuper2() {
103+ _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW)
104+ if err != nil {
105+ panic(err)
106+ }
107+
108+ sel := objc.RegisterName("run")
109+ base, err := objc.RegisterClass(
110+ "SuperExampleBase",
101 objc.GetClass("NSObject"),111 objc.GetClass("NSObject"),
102 nil,112 nil,
103 nil,113 nil,
104 []objc.MethodDef{114 []objc.MethodDef{
105 {115 {
106- Cmd: objc.RegisterName("doSomething"),116+ Cmd: sel,
107- Fn: func(self objc.ID, _cmd objc.SEL) {117+ Fn: func(self objc.ID, cmd objc.SEL) {
108- fmt.Println("In Super!")118+ fmt.Println("Base")
109 },119 },
110 },120 },
111 },121 },
@@ -114,17 +124,19 @@ func ExampleID_SendSuper() {
114 panic(err)124 panic(err)
115 }125 }
116 126
117- child, err := objc.RegisterClass(127+ var child1 objc.Class
118- "ChildObject",128+ child1, err = objc.RegisterClass(
119- super,129+ "SuperExampleChild1",
130+ base,
120 nil,131 nil,
121 nil,132 nil,
122 []objc.MethodDef{133 []objc.MethodDef{
123 {134 {
124- Cmd: objc.RegisterName("doSomething"),135+ Cmd: sel,
125- Fn: func(self objc.ID, _cmd objc.SEL) {136+ Fn: func(self objc.ID, cmd objc.SEL) {
126- fmt.Println("In Child")137+ // Start lookup in the superclass of Child1.
127- self.SendSuper(_cmd)138+ fmt.Print("SendSuper2: ")
139+ self.SendSuper2(child1, cmd)
128 },140 },
129 },141 },
130 },142 },
@@ -133,9 +145,27 @@ func ExampleID_SendSuper() {
133 panic(err)145 panic(err)
134 }146 }
135 147
136- objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething"))148+ // Child2 inherits Child1's implementation of run.
137- // Output: In Child149+ child2, err := objc.RegisterClass("SuperExampleChild2", child1, nil, nil, nil)
138- // In Super!150+ if err != nil {
151+ panic(err)
152+ }
153+
154+ for _, class := range []objc.Class{child1, child2} {
155+ object := objc.ID(class).Send(objc.RegisterName("new"))
156+ defer object.Send(objc.RegisterName("release"))
157+ if class == child1 {
158+ fmt.Println("Child1 instance")
159+ } else {
160+ fmt.Println("Child2 instance")
161+ }
162+ object.Send(sel)
163+ }
164+ // Output:
165+ // Child1 instance
166+ // SendSuper2: Base
167+ // Child2 instance
168+ // SendSuper2: Base
139 }169 }
140 170
141 func TestSend(t *testing.T) {171 func TestSend(t *testing.T) {
@@ -170,7 +200,7 @@ func ExampleSend() {
170 // Output: 3 7200 // Output: 3 7
171 }201 }
172 202
173-func ExampleSendSuper() {203+func ExampleSendSuper2() {
174 super, err := objc.RegisterClass(204 super, err := objc.RegisterClass(
175 "SuperObject2",205 "SuperObject2",
176 objc.GetClass("NSObject"),206 objc.GetClass("NSObject"),
@@ -207,7 +237,7 @@ func ExampleSendSuper() {
207 panic(err)237 panic(err)
208 }238 }
209 239
210- res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething"))240+ res := objc.SendSuper2[int](objc.ID(child).Send(objc.RegisterName("new")), child, objc.RegisterName("doSomething"))
211 fmt.Println(res)241 fmt.Println(res)
212 // Output: 16242 // Output: 16
213 }243 }
A objc/super_darwin_test.go +130 -0
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package objc_test
5+
6+import (
7+ "fmt"
8+ "os/exec"
9+ "path/filepath"
10+ "runtime"
11+ "structs"
12+ "sync/atomic"
13+ "testing"
14+
15+ "github.com/ebitengine/purego"
16+ "github.com/ebitengine/purego/objc"
17+)
18+
19+var superTestClassID atomic.Uint64
20+
21+// TestSendSuperDispatch checks argument and receiver forwarding, return values,
22+// and generic super calls from direct and inherited methods.
23+func TestSendSuperDispatch(t *testing.T) {
24+ if _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
25+ t.Fatal(err)
26+ }
27+ prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1))
28+ sel := objc.RegisterName("probe:")
29+ var baseCalls, child1Calls int
30+ var receiver objc.ID
31+ base, err := objc.RegisterClass(prefix+"Base", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{
32+ {
33+ Cmd: sel,
34+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
35+ baseCalls++
36+ receiver = self
37+ return value + 3
38+ },
39+ },
40+ })
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ var child1 objc.Class
45+ child1, err = objc.RegisterClass(prefix+"Child1", base, nil, nil, []objc.MethodDef{
46+ {
47+ Cmd: sel,
48+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
49+ child1Calls++
50+ // Bound recursion so an incorrect class anchor fails without overflowing the stack.
51+ if child1Calls > 1 {
52+ return -1
53+ }
54+
55+ return objc.SendSuper2[int](self, child1, cmd, value) + 5
56+ },
57+ },
58+ })
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+ child2, err := objc.RegisterClass(prefix+"Child2", child1, nil, nil, nil)
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ for _, class := range []objc.Class{child1, child2} {
67+ baseCalls, child1Calls, receiver = 0, 0, 0
68+ object := objc.ID(class).Send(objc.RegisterName("new"))
69+ defer object.Send(objc.RegisterName("release"))
70+ if got := objc.Send[int](object, sel, 34); got != 42 {
71+ t.Errorf("class %v: result = %d, want 42", class, got)
72+ }
73+ if baseCalls != 1 || child1Calls != 1 {
74+ t.Errorf("class %v: base calls = %d, child1 calls = %d; want 1 each", class, baseCalls, child1Calls)
75+ }
76+ if receiver != object {
77+ t.Errorf("base receiver = %v, want %v", receiver, object)
78+ }
79+ // Check forwarding through the ID method too.
80+ receiver = 0
81+ if got := object.SendSuper2(child1, sel, 34); got != 37 {
82+ t.Errorf("class %v: SendSuper2 = %d, want 37", class, got)
83+ }
84+ if receiver != object {
85+ t.Errorf("SendSuper2 receiver = %v, want %v", receiver, object)
86+ }
87+ }
88+ // The legacy helpers retain their direct-instance behavior.
89+ object := objc.ID(child1).Send(objc.RegisterName("new"))
90+ defer object.Send(objc.RegisterName("release"))
91+ if got := object.SendSuper(sel, 34); got != 37 {
92+ t.Errorf("SendSuper = %d, want 37", got)
93+ }
94+ if got := objc.SendSuper[int](object, sel, 34); got != 37 {
95+ t.Errorf("SendSuper[int] = %d, want 37", got)
96+ }
97+}
98+
99+func TestSendSuperStruct(t *testing.T) {
100+ arch := "arm64"
101+ if runtime.GOARCH == "amd64" {
102+ arch = "x86_64"
103+ }
104+ library := filepath.Join(t.TempDir(), "super.dylib")
105+ cmd := exec.Command("clang", "-dynamiclib", "-arch", arch, "-framework", "Foundation", "-o", library, "testdata/super.m")
106+ if out, err := cmd.CombinedOutput(); err != nil {
107+ t.Fatalf("compile super fixture: %v\n%s", err, out)
108+ }
109+ // Objective-C retains the registered classes and their implementations.
110+ if _, err := purego.Dlopen(library, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
111+ t.Fatal(err)
112+ }
113+ type result struct {
114+ _ structs.HostLayout
115+ A, B, C int64
116+ }
117+ want := result{
118+ A: 12,
119+ B: 34,
120+ C: 56,
121+ }
122+ child1 := objc.GetClass("PuregoSuperStructChild1")
123+ for _, class := range []objc.Class{child1, objc.GetClass("PuregoSuperStructChild2")} {
124+ object := objc.ID(class).Send(objc.RegisterName("new"))
125+ defer object.Send(objc.RegisterName("release"))
126+ if got := objc.SendSuper2[result](object, child1, objc.RegisterName("result")); got != want {
127+ t.Errorf("class %v: result = %+v, want %+v", class, got, want)
128+ }
129+ }
130+}
@@ -0,0 +1,130 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+package objc_test
5+
6+import (
7+ "fmt"
8+ "os/exec"
9+ "path/filepath"
10+ "runtime"
11+ "structs"
12+ "sync/atomic"
13+ "testing"
14+
15+ "github.com/ebitengine/purego"
16+ "github.com/ebitengine/purego/objc"
17+)
18+
19+var superTestClassID atomic.Uint64
20+
21+// TestSendSuperDispatch checks argument and receiver forwarding, return values,
22+// and generic super calls from direct and inherited methods.
23+func TestSendSuperDispatch(t *testing.T) {
24+ if _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
25+ t.Fatal(err)
26+ }
27+ prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1))
28+ sel := objc.RegisterName("probe:")
29+ var baseCalls, child1Calls int
30+ var receiver objc.ID
31+ base, err := objc.RegisterClass(prefix+"Base", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{
32+ {
33+ Cmd: sel,
34+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
35+ baseCalls++
36+ receiver = self
37+ return value + 3
38+ },
39+ },
40+ })
41+ if err != nil {
42+ t.Fatal(err)
43+ }
44+ var child1 objc.Class
45+ child1, err = objc.RegisterClass(prefix+"Child1", base, nil, nil, []objc.MethodDef{
46+ {
47+ Cmd: sel,
48+ Fn: func(self objc.ID, cmd objc.SEL, value int) int {
49+ child1Calls++
50+ // Bound recursion so an incorrect class anchor fails without overflowing the stack.
51+ if child1Calls > 1 {
52+ return -1
53+ }
54+
55+ return objc.SendSuper2[int](self, child1, cmd, value) + 5
56+ },
57+ },
58+ })
59+ if err != nil {
60+ t.Fatal(err)
61+ }
62+ child2, err := objc.RegisterClass(prefix+"Child2", child1, nil, nil, nil)
63+ if err != nil {
64+ t.Fatal(err)
65+ }
66+ for _, class := range []objc.Class{child1, child2} {
67+ baseCalls, child1Calls, receiver = 0, 0, 0
68+ object := objc.ID(class).Send(objc.RegisterName("new"))
69+ defer object.Send(objc.RegisterName("release"))
70+ if got := objc.Send[int](object, sel, 34); got != 42 {
71+ t.Errorf("class %v: result = %d, want 42", class, got)
72+ }
73+ if baseCalls != 1 || child1Calls != 1 {
74+ t.Errorf("class %v: base calls = %d, child1 calls = %d; want 1 each", class, baseCalls, child1Calls)
75+ }
76+ if receiver != object {
77+ t.Errorf("base receiver = %v, want %v", receiver, object)
78+ }
79+ // Check forwarding through the ID method too.
80+ receiver = 0
81+ if got := object.SendSuper2(child1, sel, 34); got != 37 {
82+ t.Errorf("class %v: SendSuper2 = %d, want 37", class, got)
83+ }
84+ if receiver != object {
85+ t.Errorf("SendSuper2 receiver = %v, want %v", receiver, object)
86+ }
87+ }
88+ // The legacy helpers retain their direct-instance behavior.
89+ object := objc.ID(child1).Send(objc.RegisterName("new"))
90+ defer object.Send(objc.RegisterName("release"))
91+ if got := object.SendSuper(sel, 34); got != 37 {
92+ t.Errorf("SendSuper = %d, want 37", got)
93+ }
94+ if got := objc.SendSuper[int](object, sel, 34); got != 37 {
95+ t.Errorf("SendSuper[int] = %d, want 37", got)
96+ }
97+}
98+
99+func TestSendSuperStruct(t *testing.T) {
100+ arch := "arm64"
101+ if runtime.GOARCH == "amd64" {
102+ arch = "x86_64"
103+ }
104+ library := filepath.Join(t.TempDir(), "super.dylib")
105+ cmd := exec.Command("clang", "-dynamiclib", "-arch", arch, "-framework", "Foundation", "-o", library, "testdata/super.m")
106+ if out, err := cmd.CombinedOutput(); err != nil {
107+ t.Fatalf("compile super fixture: %v\n%s", err, out)
108+ }
109+ // Objective-C retains the registered classes and their implementations.
110+ if _, err := purego.Dlopen(library, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil {
111+ t.Fatal(err)
112+ }
113+ type result struct {
114+ _ structs.HostLayout
115+ A, B, C int64
116+ }
117+ want := result{
118+ A: 12,
119+ B: 34,
120+ C: 56,
121+ }
122+ child1 := objc.GetClass("PuregoSuperStructChild1")
123+ for _, class := range []objc.Class{child1, objc.GetClass("PuregoSuperStructChild2")} {
124+ object := objc.ID(class).Send(objc.RegisterName("new"))
125+ defer object.Send(objc.RegisterName("release"))
126+ if got := objc.SendSuper2[result](object, child1, objc.RegisterName("result")); got != want {
127+ t.Errorf("class %v: result = %+v, want %+v", class, got, want)
128+ }
129+ }
130+}
A objc/testdata/super.m +33 -0
@@ -0,0 +1,33 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#import <Foundation/Foundation.h>
5+
6+struct Result {
7+ int64_t a, b, c;
8+};
9+
10+@interface PuregoSuperStructBase : NSObject
11+- (struct Result)result;
12+@end
13+
14+@implementation PuregoSuperStructBase
15+- (struct Result)result {
16+ return (struct Result){.a = 12, .b = 34, .c = 56};
17+}
18+@end
19+
20+@interface PuregoSuperStructChild1 : PuregoSuperStructBase
21+@end
22+
23+@implementation PuregoSuperStructChild1
24+- (struct Result)result {
25+ return (struct Result){.a = 78, .b = 90, .c = 12};
26+}
27+@end
28+
29+@interface PuregoSuperStructChild2 : PuregoSuperStructChild1
30+@end
31+
32+@implementation PuregoSuperStructChild2
33+@end
@@ -0,0 +1,33 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2026 The Ebitengine Authors
3+
4+#import <Foundation/Foundation.h>
5+
6+struct Result {
7+ int64_t a, b, c;
8+};
9+
10+@interface PuregoSuperStructBase : NSObject
11+- (struct Result)result;
12+@end
13+
14+@implementation PuregoSuperStructBase
15+- (struct Result)result {
16+ return (struct Result){.a = 12, .b = 34, .c = 56};
17+}
18+@end
19+
20+@interface PuregoSuperStructChild1 : PuregoSuperStructBase
21+@end
22+
23+@implementation PuregoSuperStructChild1
24+- (struct Result)result {
25+ return (struct Result){.a = 78, .b = 90, .c = 12};
26+}
27+@end
28+
29+@interface PuregoSuperStructChild2 : PuregoSuperStructChild1
30+@end
31+
32+@implementation PuregoSuperStructChild2
33+@end

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