From 96da3b0d886582bd8862cbf97c7e1f8ae4f758b7 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Wed, 23 Sep 2026 00:00:27 +0900 Subject: [PATCH 1/4] objc: add explicit-class super sends Add SendSuper1 and SendSuper2 methods and generic helpers matching objc_msgSendSuper and objc_msgSendSuper2. Explicit class arguments let inherited overrides dispatch to their defining class's superclass without recursively reentering the override. Deprecate SendSuper while preserving its existing behavior. Cover direct and inherited calls, receiver and argument forwarding, and struct returns, including the amd64 stret path. Update examples to show the different class arguments in a three-level hierarchy. Closes #533 Co-Authored-By: Codex --- examples/objc/main_darwin.go | 2 +- objc/objc_runtime_darwin.go | 79 ++++++++++++++---- objc/objc_runtime_darwin_test.go | 77 ++++++++++++----- objc/super_darwin_test.go | 137 +++++++++++++++++++++++++++++++ objc/testdata/super.m | 33 ++++++++ 5 files changed, 293 insertions(+), 35 deletions(-) create mode 100644 objc/super_darwin_test.go create mode 100644 objc/testdata/super.m diff --git a/examples/objc/main_darwin.go b/examples/objc/main_darwin.go index 1d7bee06..740b7911 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.SendSuper1(objc.GetClass("BarObject").SuperClass(), cmd) } func main() { diff --git a/objc/objc_runtime_darwin.go b/objc/objc_runtime_darwin.go index 0236db28..57684fb0 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -30,6 +30,9 @@ var ( objc_msgSend_fn uintptr objc_msgSend_stret_fn uintptr objc_msgSend func(obj ID, cmd SEL, args ...any) ID + objc_msgSendSuper_fn uintptr + objc_msgSendSuper_stret_fn uintptr + objc_msgSendSuper func(super *objc_super, cmd SEL, args ...any) ID objc_msgSendSuper2_fn uintptr objc_msgSendSuper2_stret_fn uintptr objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID @@ -81,12 +84,21 @@ func init() { if err != nil { panic(fmt.Errorf("objc: %w", err)) } + objc_msgSendSuper_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper_stret") + if err != nil { + panic(fmt.Errorf("objc: %w", err)) + } objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret") if err != nil { panic(fmt.Errorf("objc: %w", err)) } } purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn) + objc_msgSendSuper_fn, err = purego.Dlsym(objc, "objc_msgSendSuper") + if err != nil { + panic(fmt.Errorf("objc: %w", err)) + } + purego.RegisterFunc(&objc_msgSendSuper, objc_msgSendSuper_fn) objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2") if err != nil { panic(fmt.Errorf("objc: %w", err)) @@ -171,36 +183,75 @@ 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 class for super dispatch. +// objc_msgSendSuper starts lookup in superClass; objc_msgSendSuper2 starts in its 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.SendSuper1] with an explicit superclass or [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...) +} + +// SendSuper1 sends a message starting lookup in superClass. +// For a super call, superClass must be the superclass of the class defining the executing method. +func (id ID) SendSuper1(superClass Class, sel SEL, args ...any) ID { super := &objc_super{ receiver: id, - superClass: id.Class(), + superClass: superClass, + } + return objc_msgSendSuper(super, 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: 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 [SendSuper1] with an explicit superclass or [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...) +} + +// SendSuper1 sends a message starting lookup in superClass and returns T. +// For a super call, superClass must be the superclass of the class defining the executing method. +func SendSuper1[T any](id ID, superClass Class, sel SEL, args ...any) T { + super := &objc_super{ + receiver: id, + superClass: superClass, + } + var fn func(objcSuper *objc_super, sel SEL, args ...any) T + var zero T + if runtime.GOARCH == "amd64" && + reflect.ValueOf(zero).Kind() == reflect.Struct && + reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize { + purego.RegisterFunc(&fn, objc_msgSendSuper_stret_fn) + } else { + purego.RegisterFunc(&fn, objc_msgSendSuper_fn) + } + return fn(super, 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..0c7532f8 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,26 @@ func ExampleIMP() { // Output: IMP: 105 567 9 2 3 -5 4 8 9 } -func ExampleID_SendSuper() { - super, err := objc.RegisterClass( - "SuperObject", +// With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, +// even when the receiver is a Child2 instance. SendSuper1 takes Base, while +// 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 +125,23 @@ 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 Base, Child1's superclass. + fmt.Print("SendSuper1: ") + self.SendSuper1(base, cmd) + + // Start lookup in the superclass of Child1. + fmt.Print("SendSuper2: ") + self.SendSuper2(child1, cmd) }, }, }, @@ -133,9 +150,29 @@ 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 + // SendSuper1: Base + // SendSuper2: Base + // Child2 instance + // SendSuper1: Base + // SendSuper2: Base } func TestSend(t *testing.T) { @@ -170,7 +207,7 @@ func ExampleSend() { // Output: 3 7 } -func ExampleSendSuper() { +func ExampleSendSuper1() { super, err := objc.RegisterClass( "SuperObject2", objc.GetClass("NSObject"), @@ -207,7 +244,7 @@ func ExampleSendSuper() { panic(err) } - res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething")) + res := objc.SendSuper1[int](objc.ID(child).Send(objc.RegisterName("new")), child.SuperClass(), 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..2aace7f4 --- /dev/null +++ b/objc/super_darwin_test.go @@ -0,0 +1,137 @@ +// 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 + +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) + } + for _, version := range []int{1, 2} { + for _, generic := range []bool{false, true} { + t.Run(fmt.Sprintf("SendSuper%d/generic=%t", version, generic), func(t *testing.T) { + prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1)) + sel := objc.RegisterName("probe:") + var baseCalls, overrideCalls 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 override objc.Class + override, err = objc.RegisterClass(prefix+"Override", base, nil, nil, []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL, value int) int { + overrideCalls++ + // Bound recursion so an incorrect class anchor fails without overflowing the stack. + if overrideCalls > 1 { + return -1 + } + if version == 1 { + if generic { + return objc.SendSuper1[int](self, override.SuperClass(), cmd, value) + 5 + } + return int(self.SendSuper1(override.SuperClass(), cmd, value)) + 5 + } + if generic { + return objc.SendSuper2[int](self, override, cmd, value) + 5 + } + return int(self.SendSuper2(override, cmd, value)) + 5 + }, + }, + }) + if err != nil { + t.Fatal(err) + } + child, err := objc.RegisterClass(prefix+"Child", override, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + for _, class := range []objc.Class{override, child} { + baseCalls, overrideCalls, 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 || overrideCalls != 1 { + t.Errorf("class %v: base calls = %d, override calls = %d; want 1 each", class, baseCalls, overrideCalls) + } + if receiver != object { + t.Errorf("base receiver = %v, want %v", receiver, object) + } + } + // The legacy helpers retain their direct-instance behavior. + object := objc.ID(override).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.SendSuper1[result](object, child1.SuperClass(), objc.RegisterName("result")); got != want { + t.Errorf("class %v: SendSuper1 result = %+v, want %+v", class, got, want) + } + 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 From ac324f868f2a82ec0a6b33937d6fce9067938f8d Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Wed, 23 Sep 2026 02:12:55 +0900 Subject: [PATCH 2/4] objc: give each super-send method its own example Add an ID.SendSuper1 example showing direct and inherited receivers in a three-level class hierarchy. Keep the ID.SendSuper2 example focused on its own method and defining-class argument. Co-Authored-By: Codex --- objc/objc_runtime_darwin_test.go | 81 ++++++++++++++++++++++++++++---- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index 0c7532f8..329a716c 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -98,8 +98,79 @@ func ExampleIMP() { } // With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, -// even when the receiver is a Child2 instance. SendSuper1 takes Base, while -// SendSuper2 takes Child1, the class defining the method. +// even when the receiver is a Child2 instance. SendSuper1 takes Base, +// the superclass of the class defining the method. +func ExampleID_SendSuper1() { + _, 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( + "Super1ExampleBase", + objc.GetClass("NSObject"), + nil, + nil, + []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL) { + fmt.Println("Base") + }, + }, + }, + ) + if err != nil { + panic(err) + } + + child1, err := objc.RegisterClass( + "Super1ExampleChild1", + base, + nil, + nil, + []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL) { + // Start lookup in Base, the superclass of Child1. + fmt.Print("SendSuper1: ") + self.SendSuper1(base, cmd) + }, + }, + }, + ) + if err != nil { + panic(err) + } + + // Child2 inherits Child1's implementation of run. + child2, err := objc.RegisterClass("Super1ExampleChild2", 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 + // SendSuper1: Base + // Child2 instance + // SendSuper1: Base +} + +// With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, +// even when the receiver is a Child2 instance. 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 { @@ -135,10 +206,6 @@ func ExampleID_SendSuper2() { { Cmd: sel, Fn: func(self objc.ID, cmd objc.SEL) { - // Start lookup in Base, Child1's superclass. - fmt.Print("SendSuper1: ") - self.SendSuper1(base, cmd) - // Start lookup in the superclass of Child1. fmt.Print("SendSuper2: ") self.SendSuper2(child1, cmd) @@ -168,10 +235,8 @@ func ExampleID_SendSuper2() { } // Output: // Child1 instance - // SendSuper1: Base // SendSuper2: Base // Child2 instance - // SendSuper1: Base // SendSuper2: Base } From 8034ae918e823e32b333d551a0c3aaf5a07022a6 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 24 Sep 2026 00:30:07 +0900 Subject: [PATCH 3/4] objc: keep only SendSuper2 for explicit super calls Remove SendSuper1 and its runtime bindings. SendSuper2 covers Objective-C super dispatch using the defining class directly, without a separate superclass lookup. Update examples and regression tests to use SendSuper2, retaining coverage for inherited overrides and struct returns. Keep the legacy SendSuper helpers deprecated and behavior-compatible. Co-Authored-By: Codex --- examples/objc/main_darwin.go | 2 +- objc/objc_runtime_darwin.go | 51 +----------- objc/objc_runtime_darwin_test.go | 75 +---------------- objc/super_darwin_test.go | 136 ++++++++++++++----------------- 4 files changed, 70 insertions(+), 194 deletions(-) diff --git a/examples/objc/main_darwin.go b/examples/objc/main_darwin.go index 740b7911..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.SendSuper1(objc.GetClass("BarObject").SuperClass(), 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 57684fb0..058a3e2f 100644 --- a/objc/objc_runtime_darwin.go +++ b/objc/objc_runtime_darwin.go @@ -30,9 +30,6 @@ var ( objc_msgSend_fn uintptr objc_msgSend_stret_fn uintptr objc_msgSend func(obj ID, cmd SEL, args ...any) ID - objc_msgSendSuper_fn uintptr - objc_msgSendSuper_stret_fn uintptr - objc_msgSendSuper func(super *objc_super, cmd SEL, args ...any) ID objc_msgSendSuper2_fn uintptr objc_msgSendSuper2_stret_fn uintptr objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID @@ -84,21 +81,12 @@ func init() { if err != nil { panic(fmt.Errorf("objc: %w", err)) } - objc_msgSendSuper_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper_stret") - if err != nil { - panic(fmt.Errorf("objc: %w", err)) - } objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret") if err != nil { panic(fmt.Errorf("objc: %w", err)) } } purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn) - objc_msgSendSuper_fn, err = purego.Dlsym(objc, "objc_msgSendSuper") - if err != nil { - panic(fmt.Errorf("objc: %w", err)) - } - purego.RegisterFunc(&objc_msgSendSuper, objc_msgSendSuper_fn) objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2") if err != nil { panic(fmt.Errorf("objc: %w", err)) @@ -183,8 +171,8 @@ func Send[T any](id ID, sel SEL, args ...any) T { return fn(id, sel, args...) } -// objc_super supplies the receiver and class for super dispatch. -// objc_msgSendSuper starts lookup in superClass; objc_msgSendSuper2 starts in its 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 @@ -193,22 +181,11 @@ type objc_super struct { // SendSuper sends a message starting lookup in the receiver's runtime superclass. // -// Deprecated: Use [ID.SendSuper1] with an explicit superclass or [ID.SendSuper2] -// with the class defining the executing method. +// 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...) } -// SendSuper1 sends a message starting lookup in superClass. -// For a super call, superClass must be the superclass of the class defining the executing method. -func (id ID) SendSuper1(superClass Class, sel SEL, args ...any) ID { - super := &objc_super{ - receiver: id, - superClass: superClass, - } - return objc_msgSendSuper(super, 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 { @@ -221,31 +198,11 @@ func (id ID) SendSuper2(class Class, sel SEL, args ...any) ID { // SendSuper sends a message starting lookup in the receiver's runtime superclass. // -// Deprecated: Use [SendSuper1] with an explicit superclass or [SendSuper2] -// with the class defining the executing method. +// 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...) } -// SendSuper1 sends a message starting lookup in superClass and returns T. -// For a super call, superClass must be the superclass of the class defining the executing method. -func SendSuper1[T any](id ID, superClass Class, sel SEL, args ...any) T { - super := &objc_super{ - receiver: id, - superClass: superClass, - } - var fn func(objcSuper *objc_super, sel SEL, args ...any) T - var zero T - if runtime.GOARCH == "amd64" && - reflect.ValueOf(zero).Kind() == reflect.Struct && - reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize { - purego.RegisterFunc(&fn, objc_msgSendSuper_stret_fn) - } else { - purego.RegisterFunc(&fn, objc_msgSendSuper_fn) - } - return fn(super, 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 { diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index 329a716c..e98334f7 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -97,77 +97,6 @@ func ExampleIMP() { // Output: IMP: 105 567 9 2 3 -5 4 8 9 } -// With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, -// even when the receiver is a Child2 instance. SendSuper1 takes Base, -// the superclass of the class defining the method. -func ExampleID_SendSuper1() { - _, 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( - "Super1ExampleBase", - objc.GetClass("NSObject"), - nil, - nil, - []objc.MethodDef{ - { - Cmd: sel, - Fn: func(self objc.ID, cmd objc.SEL) { - fmt.Println("Base") - }, - }, - }, - ) - if err != nil { - panic(err) - } - - child1, err := objc.RegisterClass( - "Super1ExampleChild1", - base, - nil, - nil, - []objc.MethodDef{ - { - Cmd: sel, - Fn: func(self objc.ID, cmd objc.SEL) { - // Start lookup in Base, the superclass of Child1. - fmt.Print("SendSuper1: ") - self.SendSuper1(base, cmd) - }, - }, - }, - ) - if err != nil { - panic(err) - } - - // Child2 inherits Child1's implementation of run. - child2, err := objc.RegisterClass("Super1ExampleChild2", 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 - // SendSuper1: Base - // Child2 instance - // SendSuper1: Base -} - // With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, // even when the receiver is a Child2 instance. SendSuper2 takes Child1, // the class defining the method. @@ -272,7 +201,7 @@ func ExampleSend() { // Output: 3 7 } -func ExampleSendSuper1() { +func ExampleSendSuper2() { super, err := objc.RegisterClass( "SuperObject2", objc.GetClass("NSObject"), @@ -309,7 +238,7 @@ func ExampleSendSuper1() { panic(err) } - res := objc.SendSuper1[int](objc.ID(child).Send(objc.RegisterName("new")), child.SuperClass(), 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 index 2aace7f4..043d7cb4 100644 --- a/objc/super_darwin_test.go +++ b/objc/super_darwin_test.go @@ -22,81 +22,74 @@ 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) } - for _, version := range []int{1, 2} { - for _, generic := range []bool{false, true} { - t.Run(fmt.Sprintf("SendSuper%d/generic=%t", version, generic), func(t *testing.T) { - prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1)) - sel := objc.RegisterName("probe:") - var baseCalls, overrideCalls 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 - }, + for _, generic := range []bool{false, true} { + t.Run(fmt.Sprintf("generic=%t", generic), func(t *testing.T) { + prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1)) + sel := objc.RegisterName("probe:") + var baseCalls, overrideCalls 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 override objc.Class - override, err = objc.RegisterClass(prefix+"Override", base, nil, nil, []objc.MethodDef{ - { - Cmd: sel, - Fn: func(self objc.ID, cmd objc.SEL, value int) int { - overrideCalls++ - // Bound recursion so an incorrect class anchor fails without overflowing the stack. - if overrideCalls > 1 { - return -1 - } - if version == 1 { - if generic { - return objc.SendSuper1[int](self, override.SuperClass(), cmd, value) + 5 - } - return int(self.SendSuper1(override.SuperClass(), cmd, value)) + 5 - } - if generic { - return objc.SendSuper2[int](self, override, cmd, value) + 5 - } - return int(self.SendSuper2(override, cmd, value)) + 5 - }, + }, + }) + if err != nil { + t.Fatal(err) + } + var override objc.Class + override, err = objc.RegisterClass(prefix+"Override", base, nil, nil, []objc.MethodDef{ + { + Cmd: sel, + Fn: func(self objc.ID, cmd objc.SEL, value int) int { + overrideCalls++ + // Bound recursion so an incorrect class anchor fails without overflowing the stack. + if overrideCalls > 1 { + return -1 + } + + if generic { + return objc.SendSuper2[int](self, override, cmd, value) + 5 + } + return int(self.SendSuper2(override, cmd, value)) + 5 }, - }) - if err != nil { - t.Fatal(err) - } - child, err := objc.RegisterClass(prefix+"Child", override, nil, nil, nil) - if err != nil { - t.Fatal(err) - } - for _, class := range []objc.Class{override, child} { - baseCalls, overrideCalls, 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 || overrideCalls != 1 { - t.Errorf("class %v: base calls = %d, override calls = %d; want 1 each", class, baseCalls, overrideCalls) - } - if receiver != object { - t.Errorf("base receiver = %v, want %v", receiver, object) - } - } - // The legacy helpers retain their direct-instance behavior. - object := objc.ID(override).Send(objc.RegisterName("new")) + }, + }) + if err != nil { + t.Fatal(err) + } + child, err := objc.RegisterClass(prefix+"Child", override, nil, nil, nil) + if err != nil { + t.Fatal(err) + } + for _, class := range []objc.Class{override, child} { + baseCalls, overrideCalls, receiver = 0, 0, 0 + object := objc.ID(class).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.Send[int](object, sel, 34); got != 42 { + t.Errorf("class %v: result = %d, want 42", class, got) } - if got := objc.SendSuper[int](object, sel, 34); got != 37 { - t.Errorf("SendSuper[int] = %d, want 37", got) + if baseCalls != 1 || overrideCalls != 1 { + t.Errorf("class %v: base calls = %d, override calls = %d; want 1 each", class, baseCalls, overrideCalls) } - }) - } + if receiver != object { + t.Errorf("base receiver = %v, want %v", receiver, object) + } + } + // The legacy helpers retain their direct-instance behavior. + object := objc.ID(override).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) + } + }) } } @@ -127,9 +120,6 @@ func TestSendSuperStruct(t *testing.T) { 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.SendSuper1[result](object, child1.SuperClass(), objc.RegisterName("result")); got != want { - t.Errorf("class %v: SendSuper1 result = %+v, want %+v", class, got, want) - } if got := objc.SendSuper2[result](object, child1, objc.RegisterName("result")); got != want { t.Errorf("class %v: result = %+v, want %+v", class, got, want) } From 11b220392279e19e296b2e75a210225d34dd3181 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 24 Sep 2026 00:35:37 +0900 Subject: [PATCH 4/4] objc: clarify super-send examples and tests Clarify the three-level inheritance example and the defining-class argument to SendSuper2. Simplify the dispatch test by removing its mode loop and conditional dispatch, while retaining argument and receiver forwarding, return-value, and compatibility checks. Co-Authored-By: Codex --- objc/objc_runtime_darwin_test.go | 5 +- objc/super_darwin_test.go | 137 ++++++++++++++++--------------- 2 files changed, 72 insertions(+), 70 deletions(-) diff --git a/objc/objc_runtime_darwin_test.go b/objc/objc_runtime_darwin_test.go index e98334f7..feeaf974 100644 --- a/objc/objc_runtime_darwin_test.go +++ b/objc/objc_runtime_darwin_test.go @@ -97,9 +97,8 @@ func ExampleIMP() { // Output: IMP: 105 567 9 2 3 -5 4 8 9 } -// With Base <- Child1 <- Child2, a super call in Child1's method starts at Base, -// even when the receiver is a Child2 instance. SendSuper2 takes Child1, -// the class defining the method. +// 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 { diff --git a/objc/super_darwin_test.go b/objc/super_darwin_test.go index 043d7cb4..10d8b308 100644 --- a/objc/super_darwin_test.go +++ b/objc/super_darwin_test.go @@ -18,78 +18,81 @@ import ( 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) } - for _, generic := range []bool{false, true} { - t.Run(fmt.Sprintf("generic=%t", generic), func(t *testing.T) { - prefix := fmt.Sprintf("PuregoSuperTest%d", superTestClassID.Add(1)) - sel := objc.RegisterName("probe:") - var baseCalls, overrideCalls 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 override objc.Class - override, err = objc.RegisterClass(prefix+"Override", base, nil, nil, []objc.MethodDef{ - { - Cmd: sel, - Fn: func(self objc.ID, cmd objc.SEL, value int) int { - overrideCalls++ - // Bound recursion so an incorrect class anchor fails without overflowing the stack. - if overrideCalls > 1 { - return -1 - } - - if generic { - return objc.SendSuper2[int](self, override, cmd, value) + 5 - } - return int(self.SendSuper2(override, cmd, value)) + 5 - }, - }, - }) - if err != nil { - t.Fatal(err) - } - child, err := objc.RegisterClass(prefix+"Child", override, nil, nil, nil) - if err != nil { - t.Fatal(err) - } - for _, class := range []objc.Class{override, child} { - baseCalls, overrideCalls, 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 || overrideCalls != 1 { - t.Errorf("class %v: base calls = %d, override calls = %d; want 1 each", class, baseCalls, overrideCalls) - } - if receiver != object { - t.Errorf("base receiver = %v, want %v", receiver, object) + 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 } - } - // The legacy helpers retain their direct-instance behavior. - object := objc.ID(override).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) - } - }) + + 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) } }