Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/objc/main_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
36 changes: 22 additions & 14 deletions objc/objc_runtime_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
TotallyGamerJet marked this conversation as resolved.
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
Expand Down
70 changes: 50 additions & 20 deletions objc/objc_runtime_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,20 @@ func ExampleRegisterClass_helloworld() {
}

func ExampleRegisterClass() {
var class objc.Class
var (
sel_new = objc.RegisterName("new")
sel_init = objc.RegisterName("init")
sel_setBar = objc.RegisterName("setBar:")
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{
Expand Down Expand Up @@ -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")
},
},
},
Expand All @@ -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)
},
},
},
Expand All @@ -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) {
Expand Down Expand Up @@ -170,7 +200,7 @@ func ExampleSend() {
// Output: 3 7
}

func ExampleSendSuper() {
func ExampleSendSuper2() {
super, err := objc.RegisterClass(
"SuperObject2",
objc.GetClass("NSObject"),
Expand Down Expand Up @@ -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
}
Expand Down
130 changes: 130 additions & 0 deletions objc/super_darwin_test.go
Original file line number Diff line number Diff line change
@@ -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) {
Comment thread
hajimehoshi marked this conversation as resolved.
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)
}
}
}
33 changes: 33 additions & 0 deletions objc/testdata/super.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 The Ebitengine Authors

#import <Foundation/Foundation.h>

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
Loading