Skip to content

Commit ff2c2cc

Browse files
TotallyGamerJethajimehoshi
authored andcommittedApr 4, 2024·
all: struct return fixes (#221)
On amd64, there was a crash when calling objc.Send with a struct return type. This was due to calling the wrong objc message send function. On arm64, if a struct return contained structs that only contained floats it would expect it to be in R8 instead of the expected float registers Closes #223
1 parent 4889c1a commit ff2c2cc

File tree

6 files changed

+145
-38
lines changed

6 files changed

+145
-38
lines changed
 

‎func.go

+23-8
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ func RegisterFunc(fptr interface{}, cfn uintptr) {
250250
keepAlive = append(keepAlive, val)
251251
addInt(val.Pointer())
252252
} else if runtime.GOARCH == "arm64" && outType.Size() > maxRegAllocStructSize {
253-
if !isAllSameFloat(outType) || outType.NumField() > 4 {
253+
isAllFloats, numFields := isAllSameFloat(outType)
254+
if !isAllFloats || numFields > 4 {
254255
val := reflect.New(outType)
255256
keepAlive = append(keepAlive, val)
256257
syscall.arm64_r8 = val.Pointer()
@@ -351,20 +352,34 @@ func RegisterFunc(fptr interface{}, cfn uintptr) {
351352
// maxRegAllocStructSize is the biggest a struct can be while still fitting in registers.
352353
// if it is bigger than this than enough space must be allocated on the heap and then passed into
353354
// the function as the first parameter on amd64 or in R8 on arm64.
355+
//
356+
// If you change this make sure to update it in objc_runtime_darwin.go
354357
const maxRegAllocStructSize = 16
355358

356-
func isAllSameFloat(ty reflect.Type) bool {
357-
first := ty.Field(0).Type.Kind()
359+
func isAllSameFloat(ty reflect.Type) (allFloats bool, numFields int) {
360+
allFloats = true
361+
root := ty.Field(0).Type
362+
for root.Kind() == reflect.Struct {
363+
root = root.Field(0).Type
364+
}
365+
first := root.Kind()
358366
if first != reflect.Float32 && first != reflect.Float64 {
359-
return false
367+
allFloats = false
360368
}
361369
for i := 0; i < ty.NumField(); i++ {
362-
f := ty.Field(i)
363-
if f.Type.Kind() != first {
364-
return false
370+
f := ty.Field(i).Type
371+
if f.Kind() == reflect.Struct {
372+
var structNumFields int
373+
allFloats, structNumFields = isAllSameFloat(f)
374+
numFields += structNumFields
375+
continue
376+
}
377+
numFields++
378+
if f.Kind() != first {
379+
allFloats = false
365380
}
366381
}
367-
return true
382+
return allFloats, numFields
368383
}
369384

370385
func checkStructFieldsSupported(ty reflect.Type) {

‎objc/objc_runtime_darwin.go

+54-24
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"math"
1212
"reflect"
1313
"regexp"
14+
"runtime"
1415
"unicode"
1516
"unsafe"
1617

@@ -20,28 +21,30 @@ import (
2021
// TODO: support try/catch?
2122
// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
2223
var (
23-
objc_msgSend_fn uintptr
24-
objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
25-
objc_msgSendSuper2_fn uintptr
26-
objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...interface{}) ID
27-
objc_getClass func(name string) Class
28-
objc_getProtocol func(name string) *Protocol
29-
objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
30-
objc_registerClassPair func(class Class)
31-
sel_registerName func(name string) SEL
32-
class_getSuperclass func(class Class) Class
33-
class_getInstanceVariable func(class Class, name string) Ivar
34-
class_getInstanceSize func(class Class) uintptr
35-
class_addMethod func(class Class, name SEL, imp IMP, types string) bool
36-
class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
37-
class_addProtocol func(class Class, protocol *Protocol) bool
38-
ivar_getOffset func(ivar Ivar) uintptr
39-
ivar_getName func(ivar Ivar) string
40-
object_getClass func(obj ID) Class
41-
object_getIvar func(obj ID, ivar Ivar) ID
42-
object_setIvar func(obj ID, ivar Ivar, value ID)
43-
protocol_getName func(protocol *Protocol) string
44-
protocol_isEqual func(p *Protocol, p2 *Protocol) bool
24+
objc_msgSend_fn uintptr
25+
objc_msgSend_stret_fn uintptr
26+
objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID
27+
objc_msgSendSuper2_fn uintptr
28+
objc_msgSendSuper2_stret_fn uintptr
29+
objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...interface{}) ID
30+
objc_getClass func(name string) Class
31+
objc_getProtocol func(name string) *Protocol
32+
objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
33+
objc_registerClassPair func(class Class)
34+
sel_registerName func(name string) SEL
35+
class_getSuperclass func(class Class) Class
36+
class_getInstanceVariable func(class Class, name string) Ivar
37+
class_getInstanceSize func(class Class) uintptr
38+
class_addMethod func(class Class, name SEL, imp IMP, types string) bool
39+
class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
40+
class_addProtocol func(class Class, protocol *Protocol) bool
41+
ivar_getOffset func(ivar Ivar) uintptr
42+
ivar_getName func(ivar Ivar) string
43+
object_getClass func(obj ID) Class
44+
object_getIvar func(obj ID, ivar Ivar) ID
45+
object_setIvar func(obj ID, ivar Ivar, value ID)
46+
protocol_getName func(protocol *Protocol) string
47+
protocol_isEqual func(p *Protocol, p2 *Protocol) bool
4548
)
4649

4750
func init() {
@@ -53,6 +56,16 @@ func init() {
5356
if err != nil {
5457
panic(fmt.Errorf("objc: %w", err))
5558
}
59+
if runtime.GOARCH == "amd64" {
60+
objc_msgSend_stret_fn, err = purego.Dlsym(objc, "objc_msgSend_stret")
61+
if err != nil {
62+
panic(fmt.Errorf("objc: %w", err))
63+
}
64+
objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret")
65+
if err != nil {
66+
panic(fmt.Errorf("objc: %w", err))
67+
}
68+
}
5669
purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
5770
objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
5871
if err != nil {
@@ -104,12 +117,22 @@ func (id ID) SetIvar(ivar Ivar, value ID) {
104117
object_setIvar(id, ivar, value)
105118
}
106119

120+
// keep in sync with func.go
121+
const maxRegAllocStructSize = 16
122+
107123
// Send is a convenience method for sending messages to objects that can return any type.
108124
// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
109125
// It is best to cache the result of RegisterName.
110126
func Send[T any](id ID, sel SEL, args ...any) T {
111127
var fn func(id ID, sel SEL, args ...any) T
112-
purego.RegisterFunc(&fn, objc_msgSend_fn)
128+
var zero T
129+
if runtime.GOARCH == "amd64" &&
130+
reflect.ValueOf(zero).Kind() == reflect.Struct &&
131+
reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
132+
purego.RegisterFunc(&fn, objc_msgSend_stret_fn)
133+
} else {
134+
purego.RegisterFunc(&fn, objc_msgSend_fn)
135+
}
113136
return fn(id, sel, args...)
114137
}
115138

@@ -141,7 +164,14 @@ func SendSuper[T any](id ID, sel SEL, args ...any) T {
141164
superClass: id.Class(),
142165
}
143166
var fn func(objcSuper *objc_super, sel SEL, args ...any) T
144-
purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
167+
var zero T
168+
if runtime.GOARCH == "amd64" &&
169+
reflect.ValueOf(zero).Kind() == reflect.Struct &&
170+
reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
171+
purego.RegisterFunc(&fn, objc_msgSendSuper2_stret_fn)
172+
} else {
173+
purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
174+
}
145175
return fn(super, sel, args...)
146176
}
147177

‎objc/objc_runtime_darwin_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,39 @@ func TestSend(t *testing.T) {
117117
t.Failed()
118118
}
119119
}
120+
121+
func ExampleSend() {
122+
type NSRange struct {
123+
Location, Range uint
124+
}
125+
class_NSString := objc.GetClass("NSString")
126+
sel_stringWithUTF8String := objc.RegisterName("stringWithUTF8String:")
127+
128+
fullString := objc.ID(class_NSString).Send(sel_stringWithUTF8String, "Hello, World!\x00")
129+
subString := objc.ID(class_NSString).Send(sel_stringWithUTF8String, "lo, Wor\x00")
130+
131+
r := objc.Send[NSRange](fullString, objc.RegisterName("rangeOfString:"), subString)
132+
133+
fmt.Println(r)
134+
135+
// Output: {3 7}
136+
}
137+
138+
func ExampleSendSuper() {
139+
super := objc.AllocateClassPair(objc.GetClass("NSObject"), "SuperObject2", 0)
140+
super.AddMethod(objc.RegisterName("doSomething"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) int {
141+
return 16
142+
}), "i@:")
143+
super.Register()
144+
145+
child := objc.AllocateClassPair(super, "ChildObject2", 0)
146+
child.AddMethod(objc.RegisterName("doSomething"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) int {
147+
return 24
148+
}), "i@:")
149+
child.Register()
150+
151+
res := objc.SendSuper[int](objc.ID(child).Send(objc.RegisterName("new")), objc.RegisterName("doSomething"))
152+
153+
fmt.Println(res)
154+
// Output: 16
155+
}

‎struct_arm64.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,17 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
1616
return reflect.New(outType).Elem()
1717
case outSize <= 8:
1818
r1 := syscall.a1
19-
if isAllSameFloat(outType) {
19+
if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
2020
r1 = syscall.f1
21-
if outType.NumField() == 2 {
21+
if numFields == 2 {
2222
r1 = syscall.f2<<32 | syscall.f1
2323
}
2424
}
2525
return reflect.NewAt(outType, unsafe.Pointer(&struct{ a uintptr }{r1})).Elem()
2626
case outSize <= 16:
2727
r1, r2 := syscall.a1, syscall.a2
28-
if isAllSameFloat(outType) {
29-
switch outType.NumField() {
28+
if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats {
29+
switch numFields {
3030
case 4:
3131
r1 = syscall.f2<<32 | syscall.f1
3232
r2 = syscall.f4<<32 | syscall.f3
@@ -42,8 +42,8 @@ func getStruct(outType reflect.Type, syscall syscall15Args) (v reflect.Value) {
4242
}
4343
return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b uintptr }{r1, r2})).Elem()
4444
default:
45-
if isAllSameFloat(outType) && outType.NumField() <= 4 {
46-
switch outType.NumField() {
45+
if isAllFloats, numFields := isAllSameFloat(outType); isAllFloats && numFields <= 4 {
46+
switch numFields {
4747
case 4:
4848
return reflect.NewAt(outType, unsafe.Pointer(&struct{ a, b, c, d uintptr }{syscall.f1, syscall.f2, syscall.f3, syscall.f4})).Elem()
4949
case 3:

‎struct_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,18 @@ func TestRegisterFunc_structReturns(t *testing.T) {
585585
t.Fatalf("ReturnFourDoubles returned %+v wanted %+v", ret, expected)
586586
}
587587
}
588+
{
589+
type FourDoublesInternal struct {
590+
f struct{ a, b float64 }
591+
g struct{ c, d float64 }
592+
}
593+
var ReturnFourDoublesInternal func(a, b, c, d float64) FourDoublesInternal
594+
purego.RegisterLibFunc(&ReturnFourDoublesInternal, lib, "ReturnFourDoublesInternal")
595+
expected := FourDoublesInternal{f: struct{ a, b float64 }{a: 1, b: 2}, g: struct{ c, d float64 }{c: 3, d: 4}}
596+
if ret := ReturnFourDoublesInternal(1, 2, 3, 4); ret != expected {
597+
t.Fatalf("ReturnFourDoublesInternal returned %+v wanted %+v", ret, expected)
598+
}
599+
}
588600
{
589601
type FiveDoubles struct{ a, b, c, d, e float64 }
590602
var ReturnFiveDoubles func(a, b, c, d, e float64) FiveDoubles

‎testdata/structtest/structreturn_test.c

+14
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ struct FourDoubles ReturnFourDoubles(double a, double b, double c, double d) {
129129
return e;
130130
}
131131

132+
struct FourDoublesInternal{
133+
struct {
134+
double a, b;
135+
} f;
136+
struct {
137+
double c, d;
138+
} g;
139+
};
140+
141+
struct FourDoublesInternal ReturnFourDoublesInternal(double a, double b, double c, double d) {
142+
struct FourDoublesInternal e = { {a, b}, {c, d} };
143+
return e;
144+
}
145+
132146
struct FiveDoubles{
133147
double a, b, c, d, e;
134148
};

0 commit comments

Comments
 (0)
Please sign in to comment.