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 Unverified
B5690EEEBB952194The key is unknown
5 files · +236 -35
M examples/objc/main_darwin.go +1 -1
| @@ -18,7 +18,7 @@ var ( | |||
| 18 | 18 | ) | |
| 19 | 19 | ||
| 20 | 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 | 24 | 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 { | |||
| 171 | 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 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. | |
| 180 | 176 | type objc_super struct { | |
| 181 | 177 | _ structs.HostLayout | |
| 182 | 178 | receiver ID | |
| 183 | 179 | superClass Class | |
| 184 | 180 | } | |
| 185 | 181 | ||
| 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. | |
| 189 | 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 | 192 | super := &objc_super{ | |
| 191 | 193 | receiver: id, | |
| 192 | - | superClass: id.Class(), | |
| 194 | + | superClass: class, | |
| 193 | 195 | } | |
| 194 | 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. | |
| 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. | |
| 200 | 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 | 209 | super := &objc_super{ | |
| 202 | 210 | receiver: id, | |
| 203 | - | superClass: id.Class(), | |
| 211 | + | superClass: class, | |
| 204 | 212 | } | |
| 205 | 213 | var fn func(objcSuper *objc_super, sel SEL, args ...any) T | |
| 206 | 214 | 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 keyword | 174 | + | // 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.HostLayout | 177 | _ structs.HostLayout | ||
| 182 | receiver ID | 178 | receiver ID | ||
| 183 | superClass Class | 179 | superClass Class | ||
| 184 | } | 180 | } | ||
| 185 | 181 | ||||
| 186 | - | // SendSuper is a convenience method for sending message to object's super. This function takes a SEL | 182 | + | // 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 result | 183 | + | // |
| 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) T | 213 | var fn func(objcSuper *objc_super, sel SEL, args ...any) T | ||
| 206 | var zero T | 214 | var zero T | ||
M objc/objc_runtime_darwin_test.go +50 -20
| @@ -39,6 +39,7 @@ func ExampleRegisterClass_helloworld() { | |||
| 39 | 39 | } | |
| 40 | 40 | ||
| 41 | 41 | func ExampleRegisterClass() { | |
| 42 | + | var class objc.Class | |
| 42 | 43 | var ( | |
| 43 | 44 | sel_new = objc.RegisterName("new") | |
| 44 | 45 | sel_init = objc.RegisterName("init") | |
| @@ -46,11 +47,12 @@ func ExampleRegisterClass() { | |||
| 46 | 47 | sel_bar = objc.RegisterName("bar") | |
| 47 | 48 | ||
| 48 | 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 | 56 | "BarObject", | |
| 55 | 57 | objc.GetClass("NSObject"), | |
| 56 | 58 | []*objc.Protocol{ | |
| @@ -95,17 +97,25 @@ func ExampleIMP() { | |||
| 95 | 97 | // Output: IMP: 105 567 9 2 3 -5 4 8 9 | |
| 96 | 98 | } | |
| 97 | 99 | ||
| 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", | |
| 101 | 111 | objc.GetClass("NSObject"), | |
| 102 | 112 | nil, | |
| 103 | 113 | nil, | |
| 104 | 114 | []objc.MethodDef{ | |
| 105 | 115 | { | |
| 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") | |
| 109 | 119 | }, | |
| 110 | 120 | }, | |
| 111 | 121 | }, | |
| @@ -114,17 +124,19 @@ func ExampleID_SendSuper() { | |||
| 114 | 124 | panic(err) | |
| 115 | 125 | } | |
| 116 | 126 | ||
| 117 | - | child, err := objc.RegisterClass( | |
| 118 | - | "ChildObject", | |
| 119 | - | super, | |
| 127 | + | var child1 objc.Class | |
| 128 | + | child1, err = objc.RegisterClass( | |
| 129 | + | "SuperExampleChild1", | |
| 130 | + | base, | |
| 120 | 131 | nil, | |
| 121 | 132 | nil, | |
| 122 | 133 | []objc.MethodDef{ | |
| 123 | 134 | { | |
| 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) | |
| 128 | 140 | }, | |
| 129 | 141 | }, | |
| 130 | 142 | }, | |
| @@ -133,9 +145,27 @@ func ExampleID_SendSuper() { | |||
| 133 | 145 | panic(err) | |
| 134 | 146 | } | |
| 135 | 147 | ||
| 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 | |
| 139 | 169 | } | |
| 140 | 170 | ||
| 141 | 171 | func TestSend(t *testing.T) { | |
| @@ -170,7 +200,7 @@ func ExampleSend() { | |||
| 170 | 200 | // Output: 3 7 | |
| 171 | 201 | } | |
| 172 | 202 | ||
| 173 | - | func ExampleSendSuper() { | |
| 203 | + | func ExampleSendSuper2() { | |
| 174 | 204 | super, err := objc.RegisterClass( | |
| 175 | 205 | "SuperObject2", | |
| 176 | 206 | objc.GetClass("NSObject"), | |
| @@ -207,7 +237,7 @@ func ExampleSendSuper() { | |||
| 207 | 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 | 241 | fmt.Println(res) | |
| 212 | 242 | // Output: 16 | |
| 213 | 243 | } | |
| @@ -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 9 | 97 | // 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 Child | 149 | + | 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 7 | 200 | // 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: 16 | 242 | // 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 | |||