diff --git a/examples/objc/main_darwin.go b/examples/objc/main_darwin.go index 1d7bee06..244a4953 100644 --- a/examples/objc/main_darwin.go +++ b/examples/objc/main_darwin.go @@ -18,7 +18,7 @@ var ( ) func BarInit(id objc.ID, cmd objc.SEL) objc.ID { - return id.SendSuper(cmd) + return id.SendSuper2(objc.GetClass("BarObject"), cmd) } func main() { diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 0236db28..058a3e2f 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -171,36 +171,44 @@ func Send[T any](id ID, sel SEL, args ...any) T { return fn(id, sel, args...) } -// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword -// as the receiver of a message. -// -// [SendSuper] calls objc_msgSendSuper2 instead of objc_msgSendSuper. Unlike objc_msgSendSuper, which expects -// the superclass to message, objc_msgSendSuper2 expects the current class and begins looking the method up -// in its superclass by itself, so the receiver's own class is what belongs in superClass. +// objc_super supplies the receiver and defining class to objc_msgSendSuper2, +// which begins method lookup in that class's superclass. type objc_super struct { _ structs.HostLayout receiver ID superClass Class } -// SendSuper is a convenience method for sending message to object's super. This function takes a SEL -// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result -// of RegisterName. +// SendSuper sends a message starting lookup in the receiver's runtime superclass. +// +// Deprecated: Use [ID.SendSuper2] with the class defining the executing method. func (id ID) SendSuper(sel SEL, args ...any) ID { + return id.SendSuper2(id.Class(), sel, args...) +} + +// SendSuper2 sends a message starting lookup in the superclass of class. +// For a super call, class must be the class defining the executing method. +func (id ID) SendSuper2(class Class, sel SEL, args ...any) ID { super := &objc_super{ receiver: id, - superClass: id.Class(), + superClass: class, } return objc_msgSendSuper2(super, sel, args...) } -// SendSuper is a convenience method for sending message to object's super that can return any type. -// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. -// It is best to cache the result of RegisterName. +// SendSuper sends a message starting lookup in the receiver's runtime superclass. +// +// Deprecated: Use [SendSuper2] with the class defining the executing method. func SendSuper[T any](id ID, sel SEL, args ...any) T { + return SendSuper2[T](id, id.Class(), sel, args...) +} + +// SendSuper2 sends a message starting lookup in the superclass of class and returns T. +// For a super call, class must be the class defining the executing method. +func SendSuper2[T any](id ID, class Class, sel SEL, args ...any) T { super := &objc_super{ receiver: id, - superClass: id.Class(), + superClass: class, } var fn func(objcSuper *objc_super, sel SEL, args ...any) T var zero T diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index c701a50d..feeaf974 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -39,6 +39,7 @@ func ExampleRegisterClass_helloworld() { } func ExampleRegisterClass() { + var class objc.Class var ( sel_new = objc.RegisterName("new") sel_init = objc.RegisterName("init") @@ -46,11 +47,12 @@ func ExampleRegisterClass() { sel_bar = objc.RegisterName("bar") BarInit = func(id objc.ID, cmd objc.SEL) objc.ID { - return id.SendSuper(cmd) + return id.SendSuper2(class, cmd) } ) - class, err := objc.RegisterClass( + var err error + class, err = objc.RegisterClass( "BarObject", objc.GetClass("NSObject"), []*objc.Protocol{ @@ -95,17 +97,25 @@ func ExampleIMP() { // Output: IMP: 105 567 9 2 3 -5 4 8 9 } -func ExampleID_SendSuper() { - super, err := objc.RegisterClass( - "SuperObject", +// This example shows three-level inheritance with Base <- Child1 <- Child2. +// SendSuper2 takes Child1, the class defining the method. +func ExampleID_SendSuper2() { + _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW) + if err != nil { + panic(err) + } + + sel := objc.RegisterName("run") + base, err := objc.RegisterClass( + "SuperExampleBase", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{ { - Cmd: objc.RegisterName("doSomething"), - Fn: func(self objc.ID, _cmd objc.SEL) { - fmt.Println("In Super!") + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL) { + fmt.Println("Base") }, }, }, @@ -114,17 +124,19 @@ func ExampleID_SendSuper() { panic(err) } - child, err := objc.RegisterClass( - "ChildObject", - super, + var child1 objc.Class + child1, err = objc.RegisterClass( + "SuperExampleChild1", + base, nil, nil, []objc.MethodDef{ { - Cmd: objc.RegisterName("doSomething"), - Fn: func(self objc.ID, _cmd objc.SEL) { - fmt.Println("In Child") - self.SendSuper(_cmd) + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL) { + // Start lookup in the superclass of Child1. + fmt.Print("SendSuper2: ") + self.SendSuper2(child1, cmd) }, }, }, @@ -133,9 +145,27 @@ func ExampleID_SendSuper() { panic(err) } - objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething")) - // Output: In Child - // In Super! + // Child2 inherits Child1's implementation of run. + child2, err := objc.RegisterClass("SuperExampleChild2", child1, nil, nil, nil) + if err != nil { + panic(err) + } + + for _, class := range []objc.Class{child1, child2} { + object := objc.ID(class).Send(objc.RegisterName("new")) + defer object.Send(objc.RegisterName("release")) + if class == child1 { + fmt.Println("Child1 instance") + } else { + fmt.Println("Child2 instance") + } + object.Send(sel) + } + // Output: + // Child1 instance + // SendSuper2: Base + // Child2 instance + // SendSuper2: Base } func TestSend(t *testing.T) { @@ -170,7 +200,7 @@ func ExampleSend() { // Output: 3 7 } -func ExampleSendSuper() { +func ExampleSendSuper2() { super, err := objc.RegisterClass( "SuperObject2", objc.GetClass("NSObject"), @@ -207,7 +237,7 @@ func ExampleSendSuper() { panic(err) } - res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething")) + res := objc.SendSuper2[int](objc.ID(child).Send(objc.RegisterName("new")), child, objc.RegisterName("doSomething")) fmt.Println(res) // Output: 16 } diff --git a/objc/super_darwin_test.go b/objc/super_darwin_test.go new file mode 100644 index 00000000..10d8b308 --- /dev/null +++ b/objc/super_darwin_test.go @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +package objc_test + +import ( + "fmt" + "os/exec" + "path/filepath" + "runtime" + "structs" + "sync/atomic" + "testing" + + "github.com/ebitengine/purego" + "github.com/ebitengine/purego/objc" +) + +var superTestClassID atomic.Uint64 + +// TestSendSuperDispatch checks argument and receiver forwarding, return values, +// and generic super calls from direct and inherited methods. +func TestSendSuperDispatch(t *testing.T) { + if _, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil { + t.Fatal(err) + } + prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1)) + sel := objc.RegisterName("probe:") + var baseCalls, child1Calls int + var receiver objc.ID + base, err := objc.RegisterClass(prefix+"Base", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL, value int) int { + baseCalls++ + receiver = self + return value + 3 + }, + }, + }) + if err != nil { + t.Fatal(err) + } + var child1 objc.Class + child1, err = objc.RegisterClass(prefix+"Child1", base, nil, nil, []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL, value int) int { + child1Calls++ + // Bound recursion so an incorrect class anchor fails without overflowing the stack. + if child1Calls > 1 { + return -1 + } + + return objc.SendSuper2[int](self, child1, cmd, value) + 5 + }, + }, + }) + if err != nil { + t.Fatal(err) + } + child2, err := objc.RegisterClass(prefix+"Child2", child1, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + for _, class := range []objc.Class{child1, child2} { + baseCalls, child1Calls, receiver = 0, 0, 0 + object := objc.ID(class).Send(objc.RegisterName("new")) + defer object.Send(objc.RegisterName("release")) + if got := objc.Send[int](object, sel, 34); got != 42 { + t.Errorf("class %v: result = %d, want 42", class, got) + } + if baseCalls != 1 || child1Calls != 1 { + t.Errorf("class %v: base calls = %d, child1 calls = %d; want 1 each", class, baseCalls, child1Calls) + } + if receiver != object { + t.Errorf("base receiver = %v, want %v", receiver, object) + } + // Check forwarding through the ID method too. + receiver = 0 + if got := object.SendSuper2(child1, sel, 34); got != 37 { + t.Errorf("class %v: SendSuper2 = %d, want 37", class, got) + } + if receiver != object { + t.Errorf("SendSuper2 receiver = %v, want %v", receiver, object) + } + } + // The legacy helpers retain their direct-instance behavior. + object := objc.ID(child1).Send(objc.RegisterName("new")) + defer object.Send(objc.RegisterName("release")) + if got := object.SendSuper(sel, 34); got != 37 { + t.Errorf("SendSuper = %d, want 37", got) + } + if got := objc.SendSuper[int](object, sel, 34); got != 37 { + t.Errorf("SendSuper[int] = %d, want 37", got) + } +} + +func TestSendSuperStruct(t *testing.T) { + arch := "arm64" + if runtime.GOARCH == "amd64" { + arch = "x86_64" + } + library := filepath.Join(t.TempDir(), "super.dylib") + cmd := exec.Command("clang", "-dynamiclib", "-arch", arch, "-framework", "Foundation", "-o", library, "testdata/super.m") + if out, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("compile super fixture: %v\n%s", err, out) + } + // Objective-C retains the registered classes and their implementations. + if _, err := purego.Dlopen(library, purego.RTLD_GLOBAL|purego.RTLD_NOW); err != nil { + t.Fatal(err) + } + type result struct { + _ structs.HostLayout + A, B, C int64 + } + want := result{ + A: 12, + B: 34, + C: 56, + } + child1 := objc.GetClass("PuregoSuperStructChild1") + for _, class := range []objc.Class{child1, objc.GetClass("PuregoSuperStructChild2")} { + object := objc.ID(class).Send(objc.RegisterName("new")) + defer object.Send(objc.RegisterName("release")) + if got := objc.SendSuper2[result](object, child1, objc.RegisterName("result")); got != want { + t.Errorf("class %v: result = %+v, want %+v", class, got, want) + } + } +} diff --git a/objc/testdata/super.m b/objc/testdata/super.m new file mode 100644 index 00000000..60b1effd --- /dev/null +++ b/objc/testdata/super.m @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2026 The Ebitengine Authors + +#import + +struct Result { + int64_t a, b, c; +}; + +@interface PuregoSuperStructBase : NSObject +- (struct Result)result; +@end + +@implementation PuregoSuperStructBase +- (struct Result)result { + return (struct Result){.a = 12, .b = 34, .c = 56}; +} +@end + +@interface PuregoSuperStructChild1 : PuregoSuperStructBase +@end + +@implementation PuregoSuperStructChild1 +- (struct Result)result { + return (struct Result){.a = 78, .b = 90, .c = 12}; +} +@end + +@interface PuregoSuperStructChild2 : PuregoSuperStructChild1 +@end + +@implementation PuregoSuperStructChild2 +@end