PureGo Version
Reproduced with v0.11.0. Source inspection of main at 656af01 shows the same runtime-class-based dispatch in both ID.SendSuper and SendSuper[T]; main was not executed.
Operating System
macOS 26.5.1 (25F80), arm64.
Go Version (go version)
go version go1.27.0 darwin/arm64
What steps will reproduce the problem?
Register three classes: Base : NSObject, Override : Base, and Child : Override. Define a method on Base and override it on Override, calling self.SendSuper(cmd) from that override. Child inherits the override without adding any methods.
Calling the method on an Override instance works. Calling it on a Child instance recursively reenters Override's method.
Save the following as main.go in a module requiring github.com/ebitengine/purego v0.11.0, then run go run . on macOS. It uses Foundation only and opens no window. The recursion is capped at five calls to avoid a stack overflow. The third case checks explicit defining-class dispatch through objc_msgSendSuper2.
package main
import (
"fmt"
"structs"
"github.com/ebitengine/purego"
"github.com/ebitengine/purego/objc"
)
type objcSuper struct {
_ structs.HostLayout
receiver objc.ID
class objc.Class
}
func main() {
lib, err := purego.Dlopen("/System/Library/Frameworks/Foundation.framework/Foundation", purego.RTLD_NOW|purego.RTLD_GLOBAL)
if err != nil {
panic(err)
}
var superSend func(*objcSuper, objc.SEL) objc.ID
purego.RegisterLibFunc(&superSend, lib, "objc_msgSendSuper2")
sel := objc.RegisterName("probe")
var baseCalls, overrideCalls int
var fixed bool
base, err := objc.RegisterClass("Issue3740Base", objc.GetClass("NSObject"), nil, nil, []objc.MethodDef{
{
Cmd: sel,
Fn: func(self objc.ID, cmd objc.SEL) { baseCalls++ },
},
})
if err != nil {
panic(err)
}
var override objc.Class
override, err = objc.RegisterClass("Issue3740Override", base, nil, nil, []objc.MethodDef{
{
Cmd: sel,
Fn: func(self objc.ID, cmd objc.SEL) {
overrideCalls++
if overrideCalls >= 5 {
return
}
if fixed {
superSend(&objcSuper{
receiver: self,
class: override,
}, cmd)
} else {
self.SendSuper(cmd)
}
},
},
})
if err != nil {
panic(err)
}
child, err := objc.RegisterClass("Issue3740DynamicSubclass", override, nil, nil, nil)
if err != nil {
panic(err)
}
for _, tc := range []struct {
name string
class objc.Class
fixed bool
wantBase int
wantOverride int
}{
{
name: "direct instance, existing SendSuper",
class: override,
wantBase: 1,
wantOverride: 1,
},
{
name: "subclass instance, existing SendSuper (guarded at 5)",
class: child,
wantOverride: 5,
},
{
name: "subclass instance, lexical-class super dispatch",
class: child,
fixed: true,
wantBase: 1,
wantOverride: 1,
},
} {
baseCalls, overrideCalls, fixed = 0, 0, tc.fixed
obj := objc.ID(tc.class).Send(objc.RegisterName("new"))
obj.Send(sel)
obj.Send(objc.RegisterName("release"))
fmt.Printf("%s: base=%d override=%d\n", tc.name, baseCalls, overrideCalls)
if baseCalls != tc.wantBase || overrideCalls != tc.wantOverride {
panic("unexpected dispatch counts")
}
}
}
What is the expected result?
A super call made from Override's method should reach Base's implementation once, including when the receiver is a Child instance. There should be an API that allows the caller to specify Override as the class defining the method, matching Objective-C super semantics.
What happens instead?
Observed output:
direct instance, existing SendSuper: base=1 override=1
subclass instance, existing SendSuper (guarded at 5): base=0 override=5
subclass instance, lexical-class super dispatch: base=1 override=1
For a Child instance, ID.SendSuper supplies id.Class() == Child to objc_msgSendSuper2. That function starts lookup in Child's superclass, Override, and finds the same override again. Without the guard, the method keeps recursively reentering itself.
Anything else you feel useful to add?
This differs from #509: passing the current class to objc_msgSendSuper2 is correct, and the direct-instance control above succeeds. The problem is using the receiver's runtime class instead of the class defining the executing method. Substituting id.Class().SuperClass() would break ordinary direct-instance calls by advancing an extra level.
An explicit defining-class parameter or a separate class-anchored super-send API would allow correct dispatch for inherited methods and dynamically subclassed receivers. Both the method and generic helpers currently use id.Class(); the executable reproduction above exercises the method helper and the raw explicit-class fix.
Discovered while investigating hajimehoshi/ebiten#3740. The connection to that application's crash remains unconfirmed: neither the reporter's dynamic receiver class nor a reproduction of the resizing trigger has been established locally. The class hierarchy above independently reproduces the purego API limitation.
Filed by Codex (OpenAI), on behalf of @hajimehoshi.
PureGo Version
Reproduced with v0.11.0. Source inspection of main at 656af01 shows the same runtime-class-based dispatch in both
ID.SendSuperandSendSuper[T]; main was not executed.Operating System
macOS 26.5.1 (25F80), arm64.
Go Version (
go version)go version go1.27.0 darwin/arm64
What steps will reproduce the problem?
Register three classes:
Base : NSObject,Override : Base, andChild : Override. Define a method on Base and override it on Override, callingself.SendSuper(cmd)from that override. Child inherits the override without adding any methods.Calling the method on an Override instance works. Calling it on a Child instance recursively reenters Override's method.
Save the following as
main.goin a module requiringgithub.com/ebitengine/purego v0.11.0, then rungo run .on macOS. It uses Foundation only and opens no window. The recursion is capped at five calls to avoid a stack overflow. The third case checks explicit defining-class dispatch throughobjc_msgSendSuper2.What is the expected result?
A super call made from Override's method should reach Base's implementation once, including when the receiver is a Child instance. There should be an API that allows the caller to specify Override as the class defining the method, matching Objective-C super semantics.
What happens instead?
Observed output:
For a Child instance,
ID.SendSupersuppliesid.Class() == Childtoobjc_msgSendSuper2. That function starts lookup in Child's superclass, Override, and finds the same override again. Without the guard, the method keeps recursively reentering itself.Anything else you feel useful to add?
This differs from #509: passing the current class to
objc_msgSendSuper2is correct, and the direct-instance control above succeeds. The problem is using the receiver's runtime class instead of the class defining the executing method. Substitutingid.Class().SuperClass()would break ordinary direct-instance calls by advancing an extra level.An explicit defining-class parameter or a separate class-anchored super-send API would allow correct dispatch for inherited methods and dynamically subclassed receivers. Both the method and generic helpers currently use
id.Class(); the executable reproduction above exercises the method helper and the raw explicit-class fix.Discovered while investigating hajimehoshi/ebiten#3740. The connection to that application's crash remains unconfirmed: neither the reporter's dynamic receiver class nor a reproduction of the resizing trigger has been established locally. The class hierarchy above independently reproduces the purego API limitation.
Filed by Codex (OpenAI), on behalf of @hajimehoshi.