objc: support string arguments to Send/SendSuper (#18)
- commit
- 375146121d78e0bcfd0a08556735914a895553de
- author
- TotallyGamerJet · · 2022-08-13 16:56 UTC
- committer
- GitHub · · 2022-08-13 16:56 UTC
- parent
- 05ec841
- signature
- GPG Unverified
4AEE18F83AFDEB23The key is unknown
2 files · +61 -25
M objc/objc_runtime_darwin.go +26 -10
| @@ -47,7 +47,8 @@ func (id ID) Class() Class { | |||
| 47 | 47 | ||
| 48 | 48 | // Send is a convenience method for sending messages to objects. | |
| 49 | 49 | func (id ID) Send(sel SEL, args ...interface{}) ID { | |
| 50 | - | tmp := createArgs(id, sel, args...) | |
| 50 | + | tmp, keepAlive := createArgs(id, sel, args...) | |
| 51 | + | defer keepAlive() | |
| 51 | 52 | ret, _, _ := purego.SyscallN(objc_msgSend, tmp...) | |
| 52 | 53 | return ID(ret) | |
| 53 | 54 | } | |
| @@ -66,16 +67,18 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | |||
| 66 | 67 | receiver: id, | |
| 67 | 68 | superClass: id.Class(), | |
| 68 | 69 | } | |
| 69 | - | tmp := createArgs(0, sel, args...) | |
| 70 | + | tmp, keepAlive := createArgs(0, sel, args...) | |
| 71 | + | defer keepAlive() | |
| 70 | 72 | tmp[0] = uintptr(unsafe.Pointer(super)) // if createArgs splits the stack the pointer would be wrong | |
| 71 | 73 | ret, _, _ := purego.SyscallN(objc_msgSendSuper2, tmp...) | |
| 72 | 74 | return ID(ret) | |
| 73 | 75 | } | |
| 74 | 76 | ||
| 75 | - | func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | |
| 77 | + | func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr, keepAlive func()) { | |
| 76 | 78 | out = make([]uintptr, 2, len(args)+2) | |
| 77 | 79 | out[0] = uintptr(cls) | |
| 78 | 80 | out[1] = uintptr(sel) | |
| 81 | + | var alive []interface{} | |
| 79 | 82 | for _, a := range args { | |
| 80 | 83 | switch v := a.(type) { | |
| 81 | 84 | case ID: | |
| @@ -92,7 +95,18 @@ func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | |||
| 92 | 95 | } else { | |
| 93 | 96 | out = append(out, uintptr(0)) | |
| 94 | 97 | } | |
| 98 | + | case string: | |
| 99 | + | c := strings.CString(v) | |
| 100 | + | alive = append(alive, c) | |
| 101 | + | // NOTE: that although the pointer will be kept alive | |
| 102 | + | // if there are any stack splits before this uintptr | |
| 103 | + | // is no longer needed the pointer will point to random memory. | |
| 104 | + | out = append(out, uintptr(unsafe.Pointer(c))) | |
| 95 | 105 | case unsafe.Pointer: | |
| 106 | + | alive = append(alive, v) | |
| 107 | + | // NOTE: that although the pointer will be kept alive | |
| 108 | + | // if there are any stack splits before this uintptr | |
| 109 | + | // is no longer needed the pointer will point to random memory. | |
| 96 | 110 | out = append(out, uintptr(v)) | |
| 97 | 111 | case uintptr: | |
| 98 | 112 | out = append(out, v) | |
| @@ -104,7 +118,9 @@ func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | |||
| 104 | 118 | panic(fmt.Sprintf("objc: unknown type %T", v)) | |
| 105 | 119 | } | |
| 106 | 120 | } | |
| 107 | - | return out | |
| 121 | + | return out, func() { | |
| 122 | + | runtime.KeepAlive(alive) | |
| 123 | + | } | |
| 108 | 124 | } | |
| 109 | 125 | ||
| 110 | 126 | // SEL is an opaque type that represents a method selector | |
| @@ -192,7 +208,7 @@ type Ivar uintptr | |||
| 192 | 208 | ||
| 193 | 209 | // Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value. | |
| 194 | 210 | // | |
| 195 | - | // For instance variables of type id or other object types, call Ivar and SetIvar instead | |
| 211 | + | // For instance variables of type ID or other object types, call Ivar and SetIvar instead | |
| 196 | 212 | // of using this offset to access the instance variable data directly. | |
| 197 | 213 | func (i Ivar) Offset() uintptr { | |
| 198 | 214 | ret, _, _ := purego.SyscallN(ivar_getOffset, uintptr(i)) | |
| @@ -213,18 +229,18 @@ func IMP(fn interface{}) _IMP { | |||
| 213 | 229 | if x, ok := fn.(uintptr); ok { | |
| 214 | 230 | return _IMP(x) | |
| 215 | 231 | } | |
| 216 | - | val := reflect.ValueOf(fn) | |
| 217 | - | if val.Kind() != reflect.Func { | |
| 232 | + | ty := reflect.TypeOf(fn) | |
| 233 | + | if ty.Kind() != reflect.Func { | |
| 218 | 234 | panic("objc: not a function") | |
| 219 | 235 | } | |
| 220 | 236 | // IMP is stricter than a normal callback | |
| 221 | 237 | // id (*IMP)(id, SEL, ...) | |
| 222 | 238 | switch { | |
| 223 | - | case val.Type().NumIn() < 2: | |
| 239 | + | case ty.NumIn() < 2: | |
| 224 | 240 | fallthrough | |
| 225 | - | case val.Type().In(0).Kind() != reflect.Uintptr: | |
| 241 | + | case ty.In(0).Kind() != reflect.Uintptr: | |
| 226 | 242 | fallthrough | |
| 227 | - | case val.Type().In(1).Kind() != reflect.Uintptr: | |
| 243 | + | case ty.In(1).Kind() != reflect.Uintptr: | |
| 228 | 244 | panic("objc: IMP must take a (id, SEL) as its first two arguments") | |
| 229 | 245 | } | |
| 230 | 246 | return _IMP(purego.NewCallback(fn)) | |
| @@ -47,7 +47,8 @@ func (id ID) Class() Class { | |||||
| 47 | 47 | ||||
| 48 | // Send is a convenience method for sending messages to objects. | 48 | // Send is a convenience method for sending messages to objects. | ||
| 49 | func (id ID) Send(sel SEL, args ...interface{}) ID { | 49 | func (id ID) Send(sel SEL, args ...interface{}) ID { | ||
| 50 | - | tmp := createArgs(id, sel, args...) | 50 | + | tmp, keepAlive := createArgs(id, sel, args...) |
| 51 | + | defer keepAlive() | |||
| 51 | ret, _, _ := purego.SyscallN(objc_msgSend, tmp...) | 52 | ret, _, _ := purego.SyscallN(objc_msgSend, tmp...) | ||
| 52 | return ID(ret) | 53 | return ID(ret) | ||
| 53 | } | 54 | } | ||
| @@ -66,16 +67,18 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | |||||
| 66 | receiver: id, | 67 | receiver: id, | ||
| 67 | superClass: id.Class(), | 68 | superClass: id.Class(), | ||
| 68 | } | 69 | } | ||
| 69 | - | tmp := createArgs(0, sel, args...) | 70 | + | tmp, keepAlive := createArgs(0, sel, args...) |
| 71 | + | defer keepAlive() | |||
| 70 | tmp[0] = uintptr(unsafe.Pointer(super)) // if createArgs splits the stack the pointer would be wrong | 72 | tmp[0] = uintptr(unsafe.Pointer(super)) // if createArgs splits the stack the pointer would be wrong | ||
| 71 | ret, _, _ := purego.SyscallN(objc_msgSendSuper2, tmp...) | 73 | ret, _, _ := purego.SyscallN(objc_msgSendSuper2, tmp...) | ||
| 72 | return ID(ret) | 74 | return ID(ret) | ||
| 73 | } | 75 | } | ||
| 74 | 76 | ||||
| 75 | - | func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | 77 | + | func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr, keepAlive func()) { |
| 76 | out = make([]uintptr, 2, len(args)+2) | 78 | out = make([]uintptr, 2, len(args)+2) | ||
| 77 | out[0] = uintptr(cls) | 79 | out[0] = uintptr(cls) | ||
| 78 | out[1] = uintptr(sel) | 80 | out[1] = uintptr(sel) | ||
| 81 | + | var alive []interface{} | |||
| 79 | for _, a := range args { | 82 | for _, a := range args { | ||
| 80 | switch v := a.(type) { | 83 | switch v := a.(type) { | ||
| 81 | case ID: | 84 | case ID: | ||
| @@ -92,7 +95,18 @@ func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | |||||
| 92 | } else { | 95 | } else { | ||
| 93 | out = append(out, uintptr(0)) | 96 | out = append(out, uintptr(0)) | ||
| 94 | } | 97 | } | ||
| 98 | + | case string: | |||
| 99 | + | c := strings.CString(v) | |||
| 100 | + | alive = append(alive, c) | |||
| 101 | + | // NOTE: that although the pointer will be kept alive | |||
| 102 | + | // if there are any stack splits before this uintptr | |||
| 103 | + | // is no longer needed the pointer will point to random memory. | |||
| 104 | + | out = append(out, uintptr(unsafe.Pointer(c))) | |||
| 95 | case unsafe.Pointer: | 105 | case unsafe.Pointer: | ||
| 106 | + | alive = append(alive, v) | |||
| 107 | + | // NOTE: that although the pointer will be kept alive | |||
| 108 | + | // if there are any stack splits before this uintptr | |||
| 109 | + | // is no longer needed the pointer will point to random memory. | |||
| 96 | out = append(out, uintptr(v)) | 110 | out = append(out, uintptr(v)) | ||
| 97 | case uintptr: | 111 | case uintptr: | ||
| 98 | out = append(out, v) | 112 | out = append(out, v) | ||
| @@ -104,7 +118,9 @@ func createArgs(cls ID, sel SEL, args ...interface{}) (out []uintptr) { | |||||
| 104 | panic(fmt.Sprintf("objc: unknown type %T", v)) | 118 | panic(fmt.Sprintf("objc: unknown type %T", v)) | ||
| 105 | } | 119 | } | ||
| 106 | } | 120 | } | ||
| 107 | - | return out | 121 | + | return out, func() { |
| 122 | + | runtime.KeepAlive(alive) | |||
| 123 | + | } | |||
| 108 | } | 124 | } | ||
| 109 | 125 | ||||
| 110 | // SEL is an opaque type that represents a method selector | 126 | // SEL is an opaque type that represents a method selector | ||
| @@ -192,7 +208,7 @@ type Ivar uintptr | |||||
| 192 | 208 | ||||
| 193 | // Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value. | 209 | // Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value. | ||
| 194 | // | 210 | // | ||
| 195 | - | // For instance variables of type id or other object types, call Ivar and SetIvar instead | 211 | + | // For instance variables of type ID or other object types, call Ivar and SetIvar instead |
| 196 | // of using this offset to access the instance variable data directly. | 212 | // of using this offset to access the instance variable data directly. | ||
| 197 | func (i Ivar) Offset() uintptr { | 213 | func (i Ivar) Offset() uintptr { | ||
| 198 | ret, _, _ := purego.SyscallN(ivar_getOffset, uintptr(i)) | 214 | ret, _, _ := purego.SyscallN(ivar_getOffset, uintptr(i)) | ||
| @@ -213,18 +229,18 @@ func IMP(fn interface{}) _IMP { | |||||
| 213 | if x, ok := fn.(uintptr); ok { | 229 | if x, ok := fn.(uintptr); ok { | ||
| 214 | return _IMP(x) | 230 | return _IMP(x) | ||
| 215 | } | 231 | } | ||
| 216 | - | val := reflect.ValueOf(fn) | 232 | + | ty := reflect.TypeOf(fn) |
| 217 | - | if val.Kind() != reflect.Func { | 233 | + | if ty.Kind() != reflect.Func { |
| 218 | panic("objc: not a function") | 234 | panic("objc: not a function") | ||
| 219 | } | 235 | } | ||
| 220 | // IMP is stricter than a normal callback | 236 | // IMP is stricter than a normal callback | ||
| 221 | // id (*IMP)(id, SEL, ...) | 237 | // id (*IMP)(id, SEL, ...) | ||
| 222 | switch { | 238 | switch { | ||
| 223 | - | case val.Type().NumIn() < 2: | 239 | + | case ty.NumIn() < 2: |
| 224 | fallthrough | 240 | fallthrough | ||
| 225 | - | case val.Type().In(0).Kind() != reflect.Uintptr: | 241 | + | case ty.In(0).Kind() != reflect.Uintptr: |
| 226 | fallthrough | 242 | fallthrough | ||
| 227 | - | case val.Type().In(1).Kind() != reflect.Uintptr: | 243 | + | case ty.In(1).Kind() != reflect.Uintptr: |
| 228 | panic("objc: IMP must take a (id, SEL) as its first two arguments") | 244 | panic("objc: IMP must take a (id, SEL) as its first two arguments") | ||
| 229 | } | 245 | } | ||
| 230 | return _IMP(purego.NewCallback(fn)) | 246 | return _IMP(purego.NewCallback(fn)) | ||
M objc/objc_runtime_darwin_test.go +35 -15
| @@ -21,32 +21,32 @@ func init() { | |||
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | func ExampleAllocateClassPair() { | |
| 24 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject\x00"), "FooObject\x00", 0) | |
| 25 | - | class.AddMethod(objc.RegisterName("run\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |
| 24 | + | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) | |
| 25 | + | class.AddMethod(objc.RegisterName("run"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |
| 26 | 26 | fmt.Println("Hello World!") | |
| 27 | - | }), "v@:\x00") | |
| 27 | + | }), "v@:") | |
| 28 | 28 | class.Register() | |
| 29 | 29 | ||
| 30 | - | var fooObject = objc.ID(class).Send(objc.RegisterName("new\x00")) | |
| 31 | - | fooObject.Send(objc.RegisterName("run\x00")) | |
| 30 | + | var fooObject = objc.ID(class).Send(objc.RegisterName("new")) | |
| 31 | + | fooObject.Send(objc.RegisterName("run")) | |
| 32 | 32 | // Output: Hello World! | |
| 33 | 33 | } | |
| 34 | 34 | ||
| 35 | 35 | func ExampleClass_AddIvar() { | |
| 36 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject\x00"), "BarObject\x00", 0) | |
| 37 | - | class.AddIvar("bar\x00", int(0), "q\x00") | |
| 38 | - | var barOffset = class.InstanceVariable("bar\x00").Offset() | |
| 39 | - | class.AddMethod(objc.RegisterName("bar\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL) int { | |
| 36 | + | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0) | |
| 37 | + | class.AddIvar("bar", int(0), "q") | |
| 38 | + | var barOffset = class.InstanceVariable("bar").Offset() | |
| 39 | + | class.AddMethod(objc.RegisterName("bar"), objc.IMP(func(self objc.ID, _cmd objc.SEL) int { | |
| 40 | 40 | return *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) | |
| 41 | - | }), "q@:\x00") | |
| 42 | - | class.AddMethod(objc.RegisterName("setBar:\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL, bar int) { | |
| 41 | + | }), "q@:") | |
| 42 | + | class.AddMethod(objc.RegisterName("setBar:"), objc.IMP(func(self objc.ID, _cmd objc.SEL, bar int) { | |
| 43 | 43 | *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) = bar | |
| 44 | - | }), "v@:q\x00") | |
| 44 | + | }), "v@:q") | |
| 45 | 45 | class.Register() | |
| 46 | 46 | ||
| 47 | - | var barObject = objc.ID(class).Send(objc.RegisterName("new\x00")) | |
| 48 | - | barObject.Send(objc.RegisterName("setBar:\x00"), 123) | |
| 49 | - | var bar = int(barObject.Send(objc.RegisterName("bar\x00"))) | |
| 47 | + | var barObject = objc.ID(class).Send(objc.RegisterName("new")) | |
| 48 | + | barObject.Send(objc.RegisterName("setBar:"), 123) | |
| 49 | + | var bar = int(barObject.Send(objc.RegisterName("bar"))) | |
| 50 | 50 | fmt.Println(bar) | |
| 51 | 51 | // Output: 123 | |
| 52 | 52 | } | |
| @@ -60,3 +60,23 @@ func ExampleIMP() { | |||
| 60 | 60 | ||
| 61 | 61 | // Output: IMP: 105 567 | |
| 62 | 62 | } | |
| 63 | + | ||
| 64 | + | func ExampleID_SendSuper() { | |
| 65 | + | super := objc.AllocateClassPair(objc.GetClass("NSObject"), "SuperObject", 0) | |
| 66 | + | super.AddMethod(objc.RegisterName("doSomething"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |
| 67 | + | fmt.Println("In Super!") | |
| 68 | + | }), "v@:") | |
| 69 | + | super.Register() | |
| 70 | + | ||
| 71 | + | child := objc.AllocateClassPair(super, "ChildObject", 0) | |
| 72 | + | child.AddMethod(objc.RegisterName("doSomething"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |
| 73 | + | fmt.Println("In Child") | |
| 74 | + | self.SendSuper(_cmd) | |
| 75 | + | }), "v@:") | |
| 76 | + | child.Register() | |
| 77 | + | ||
| 78 | + | objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething")) | |
| 79 | + | ||
| 80 | + | // Output: In Child | |
| 81 | + | // In Super! | |
| 82 | + | } | |
| @@ -21,32 +21,32 @@ func init() { | |||||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | func ExampleAllocateClassPair() { | 23 | func ExampleAllocateClassPair() { | ||
| 24 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject\x00"), "FooObject\x00", 0) | 24 | + | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) |
| 25 | - | class.AddMethod(objc.RegisterName("run\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | 25 | + | class.AddMethod(objc.RegisterName("run"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { |
| 26 | fmt.Println("Hello World!") | 26 | fmt.Println("Hello World!") | ||
| 27 | - | }), "v@:\x00") | 27 | + | }), "v@:") |
| 28 | class.Register() | 28 | class.Register() | ||
| 29 | 29 | ||||
| 30 | - | var fooObject = objc.ID(class).Send(objc.RegisterName("new\x00")) | 30 | + | var fooObject = objc.ID(class).Send(objc.RegisterName("new")) |
| 31 | - | fooObject.Send(objc.RegisterName("run\x00")) | 31 | + | fooObject.Send(objc.RegisterName("run")) |
| 32 | // Output: Hello World! | 32 | // Output: Hello World! | ||
| 33 | } | 33 | } | ||
| 34 | 34 | ||||
| 35 | func ExampleClass_AddIvar() { | 35 | func ExampleClass_AddIvar() { | ||
| 36 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject\x00"), "BarObject\x00", 0) | 36 | + | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0) |
| 37 | - | class.AddIvar("bar\x00", int(0), "q\x00") | 37 | + | class.AddIvar("bar", int(0), "q") |
| 38 | - | var barOffset = class.InstanceVariable("bar\x00").Offset() | 38 | + | var barOffset = class.InstanceVariable("bar").Offset() |
| 39 | - | class.AddMethod(objc.RegisterName("bar\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL) int { | 39 | + | class.AddMethod(objc.RegisterName("bar"), objc.IMP(func(self objc.ID, _cmd objc.SEL) int { |
| 40 | return *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) | 40 | return *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) | ||
| 41 | - | }), "q@:\x00") | 41 | + | }), "q@:") |
| 42 | - | class.AddMethod(objc.RegisterName("setBar:\x00"), objc.IMP(func(self objc.ID, _cmd objc.SEL, bar int) { | 42 | + | class.AddMethod(objc.RegisterName("setBar:"), objc.IMP(func(self objc.ID, _cmd objc.SEL, bar int) { |
| 43 | *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) = bar | 43 | *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(self)) + barOffset)) = bar | ||
| 44 | - | }), "v@:q\x00") | 44 | + | }), "v@:q") |
| 45 | class.Register() | 45 | class.Register() | ||
| 46 | 46 | ||||
| 47 | - | var barObject = objc.ID(class).Send(objc.RegisterName("new\x00")) | 47 | + | var barObject = objc.ID(class).Send(objc.RegisterName("new")) |
| 48 | - | barObject.Send(objc.RegisterName("setBar:\x00"), 123) | 48 | + | barObject.Send(objc.RegisterName("setBar:"), 123) |
| 49 | - | var bar = int(barObject.Send(objc.RegisterName("bar\x00"))) | 49 | + | var bar = int(barObject.Send(objc.RegisterName("bar"))) |
| 50 | fmt.Println(bar) | 50 | fmt.Println(bar) | ||
| 51 | // Output: 123 | 51 | // Output: 123 | ||
| 52 | } | 52 | } | ||
| @@ -60,3 +60,23 @@ func ExampleIMP() { | |||||
| 60 | 60 | ||||
| 61 | // Output: IMP: 105 567 | 61 | // Output: IMP: 105 567 | ||
| 62 | } | 62 | } | ||
| 63 | + | ||||
| 64 | + | func ExampleID_SendSuper() { | |||
| 65 | + | super := objc.AllocateClassPair(objc.GetClass("NSObject"), "SuperObject", 0) | |||
| 66 | + | super.AddMethod(objc.RegisterName("doSomething"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |||
| 67 | + | fmt.Println("In Super!") | |||
| 68 | + | }), "v@:") | |||
| 69 | + | super.Register() | |||
| 70 | + | ||||
| 71 | + | child := objc.AllocateClassPair(super, "ChildObject", 0) | |||
| 72 | + | child.AddMethod(objc.RegisterName("doSomething"), objc.IMP(func(self objc.ID, _cmd objc.SEL) { | |||
| 73 | + | fmt.Println("In Child") | |||
| 74 | + | self.SendSuper(_cmd) | |||
| 75 | + | }), "v@:") | |||
| 76 | + | child.Register() | |||
| 77 | + | ||||
| 78 | + | objc.ID(child).Send(objc.RegisterName("new")).Send(objc.RegisterName("doSomething")) | |||
| 79 | + | ||||
| 80 | + | // Output: In Child | |||
| 81 | + | // In Super! | |||
| 82 | + | } | |||