objc: add safe Objc Classes (#160)
Closes #143
- commit
- 63192295690dab022ec6fab0f2e0a80deae002c4
- author
- TotallyGamerJet · · 2023-09-09 15:48 UTC
- committer
- GitHub · · 2023-09-09 15:48 UTC
- parent
- 0f25ec6
- signature
- GPG Unverified
4AEE18F83AFDEB23The key is unknown - refs
- tag: v0.5.0-alpha.3
4 files · +246 -233
M examples/objc/main_darwin.go +30 -32
| @@ -5,7 +5,7 @@ package main | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "fmt" | |
| 8 | - | "unsafe" | |
| 8 | + | "reflect" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/ebitengine/purego/objc" | |
| 11 | 11 | ) | |
| @@ -17,45 +17,43 @@ var ( | |||
| 17 | 17 | sel_bar = objc.RegisterName("bar") | |
| 18 | 18 | ) | |
| 19 | 19 | ||
| 20 | - | type barObject struct { | |
| 21 | - | isa objc.Class `objc:"BarObject : NSObject <NSDelegateWindow>"` | |
| 22 | - | bar int | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | func (b *barObject) Init(_cmd objc.SEL) objc.ID { | |
| 26 | - | return objc.ID(unsafe.Pointer(b)).SendSuper(_cmd) | |
| 27 | - | } | |
| 28 | - | ||
| 29 | - | func (b *barObject) Bar(_cmd objc.SEL) int { | |
| 30 | - | return b.bar | |
| 31 | - | } | |
| 32 | - | ||
| 33 | - | func (b *barObject) SetBar(_cmd objc.SEL, bar int) { | |
| 34 | - | b.bar = bar | |
| 35 | - | } | |
| 36 | - | ||
| 37 | - | func (_ *barObject) Selector(metName string) objc.SEL { | |
| 38 | - | switch metName { | |
| 39 | - | case "Init": | |
| 40 | - | return sel_init | |
| 41 | - | case "SetBar": | |
| 42 | - | return sel_setBar | |
| 43 | - | case "Bar": | |
| 44 | - | return sel_bar | |
| 45 | - | default: | |
| 46 | - | return 0 | |
| 47 | - | } | |
| 20 | + | func BarInit(id objc.ID, cmd objc.SEL) objc.ID { | |
| 21 | + | return id.SendSuper(cmd) | |
| 48 | 22 | } | |
| 49 | 23 | ||
| 50 | 24 | func main() { | |
| 51 | - | class, err := objc.RegisterClass(&barObject{}) | |
| 25 | + | // This struct is equivalent to the following Objective-C definition. | |
| 26 | + | // | |
| 27 | + | // @interface BarObject : NSObject <NSDelegateWindow> | |
| 28 | + | // @property (readwrite) bar int | |
| 29 | + | // @end | |
| 30 | + | class, err := objc.RegisterClass( | |
| 31 | + | "BarObject", | |
| 32 | + | objc.GetClass("NSObject"), | |
| 33 | + | []*objc.Protocol{ | |
| 34 | + | objc.GetProtocol("NSDelegateWindow"), | |
| 35 | + | }, | |
| 36 | + | []objc.FieldDef{ | |
| 37 | + | { | |
| 38 | + | Name: "bar", | |
| 39 | + | Type: reflect.TypeOf(int(0)), | |
| 40 | + | Attribute: objc.ReadWrite, | |
| 41 | + | }, | |
| 42 | + | }, | |
| 43 | + | []objc.MethodDef{ | |
| 44 | + | { | |
| 45 | + | Cmd: sel_init, | |
| 46 | + | Fn: BarInit, | |
| 47 | + | }, | |
| 48 | + | }, | |
| 49 | + | ) | |
| 52 | 50 | if err != nil { | |
| 53 | 51 | panic(err) | |
| 54 | 52 | } | |
| 55 | 53 | ||
| 56 | - | var object = objc.ID(class).Send(sel_new) | |
| 54 | + | object := objc.ID(class).Send(sel_new) | |
| 57 | 55 | object.Send(sel_setBar, 123) | |
| 58 | - | var bar = int(object.Send(sel_bar)) | |
| 56 | + | bar := int(object.Send(sel_bar)) | |
| 59 | 57 | fmt.Println(bar) | |
| 60 | 58 | // Output: 123 | |
| 61 | 59 | } | |
| @@ -5,7 +5,7 @@ package main | |||||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | - | "unsafe" | 8 | + | "reflect" |
| 9 | 9 | ||||
| 10 | "github.com/ebitengine/purego/objc" | 10 | "github.com/ebitengine/purego/objc" | ||
| 11 | ) | 11 | ) | ||
| @@ -17,45 +17,43 @@ var ( | |||||
| 17 | sel_bar = objc.RegisterName("bar") | 17 | sel_bar = objc.RegisterName("bar") | ||
| 18 | ) | 18 | ) | ||
| 19 | 19 | ||||
| 20 | - | type barObject struct { | 20 | + | func BarInit(id objc.ID, cmd objc.SEL) objc.ID { |
| 21 | - | isa objc.Class `objc:"BarObject : NSObject <NSDelegateWindow>"` | 21 | + | return id.SendSuper(cmd) |
| 22 | - | bar int | |||
| 23 | - | } | |||
| 24 | - | ||||
| 25 | - | func (b *barObject) Init(_cmd objc.SEL) objc.ID { | |||
| 26 | - | return objc.ID(unsafe.Pointer(b)).SendSuper(_cmd) | |||
| 27 | - | } | |||
| 28 | - | ||||
| 29 | - | func (b *barObject) Bar(_cmd objc.SEL) int { | |||
| 30 | - | return b.bar | |||
| 31 | - | } | |||
| 32 | - | ||||
| 33 | - | func (b *barObject) SetBar(_cmd objc.SEL, bar int) { | |||
| 34 | - | b.bar = bar | |||
| 35 | - | } | |||
| 36 | - | ||||
| 37 | - | func (_ *barObject) Selector(metName string) objc.SEL { | |||
| 38 | - | switch metName { | |||
| 39 | - | case "Init": | |||
| 40 | - | return sel_init | |||
| 41 | - | case "SetBar": | |||
| 42 | - | return sel_setBar | |||
| 43 | - | case "Bar": | |||
| 44 | - | return sel_bar | |||
| 45 | - | default: | |||
| 46 | - | return 0 | |||
| 47 | - | } | |||
| 48 | } | 22 | } | ||
| 49 | 23 | ||||
| 50 | func main() { | 24 | func main() { | ||
| 51 | - | class, err := objc.RegisterClass(&barObject{}) | 25 | + | // This struct is equivalent to the following Objective-C definition. |
| 26 | + | // | |||
| 27 | + | // @interface BarObject : NSObject <NSDelegateWindow> | |||
| 28 | + | // @property (readwrite) bar int | |||
| 29 | + | // @end | |||
| 30 | + | class, err := objc.RegisterClass( | |||
| 31 | + | "BarObject", | |||
| 32 | + | objc.GetClass("NSObject"), | |||
| 33 | + | []*objc.Protocol{ | |||
| 34 | + | objc.GetProtocol("NSDelegateWindow"), | |||
| 35 | + | }, | |||
| 36 | + | []objc.FieldDef{ | |||
| 37 | + | { | |||
| 38 | + | Name: "bar", | |||
| 39 | + | Type: reflect.TypeOf(int(0)), | |||
| 40 | + | Attribute: objc.ReadWrite, | |||
| 41 | + | }, | |||
| 42 | + | }, | |||
| 43 | + | []objc.MethodDef{ | |||
| 44 | + | { | |||
| 45 | + | Cmd: sel_init, | |||
| 46 | + | Fn: BarInit, | |||
| 47 | + | }, | |||
| 48 | + | }, | |||
| 49 | + | ) | |||
| 52 | if err != nil { | 50 | if err != nil { | ||
| 53 | panic(err) | 51 | panic(err) | ||
| 54 | } | 52 | } | ||
| 55 | 53 | ||||
| 56 | - | var object = objc.ID(class).Send(sel_new) | 54 | + | object := objc.ID(class).Send(sel_new) |
| 57 | object.Send(sel_setBar, 123) | 55 | object.Send(sel_setBar, 123) | ||
| 58 | - | var bar = int(object.Send(sel_bar)) | 56 | + | bar := int(object.Send(sel_bar)) |
| 59 | fmt.Println(bar) | 57 | fmt.Println(bar) | ||
| 60 | // Output: 123 | 58 | // Output: 123 | ||
| 61 | } | 59 | } | ||
M internal/strings/strings.go +2 -2
| @@ -17,12 +17,12 @@ func CString(name string) *byte { | |||
| 17 | 17 | if hasSuffix(name, "\x00") { | |
| 18 | 18 | return &(*(*[]byte)(unsafe.Pointer(&name)))[0] | |
| 19 | 19 | } | |
| 20 | - | var b = make([]byte, len(name)+1) | |
| 20 | + | b := make([]byte, len(name)+1) | |
| 21 | 21 | copy(b, name) | |
| 22 | 22 | return &b[0] | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | - | // GoString copies a char* to a Go string. | |
| 25 | + | // GoString copies a null-terminated char* to a Go string. | |
| 26 | 26 | func GoString(c uintptr) string { | |
| 27 | 27 | // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer | |
| 28 | 28 | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c)) | |
| @@ -17,12 +17,12 @@ func CString(name string) *byte { | |||||
| 17 | if hasSuffix(name, "\x00") { | 17 | if hasSuffix(name, "\x00") { | ||
| 18 | return &(*(*[]byte)(unsafe.Pointer(&name)))[0] | 18 | return &(*(*[]byte)(unsafe.Pointer(&name)))[0] | ||
| 19 | } | 19 | } | ||
| 20 | - | var b = make([]byte, len(name)+1) | 20 | + | b := make([]byte, len(name)+1) |
| 21 | copy(b, name) | 21 | copy(b, name) | ||
| 22 | return &b[0] | 22 | return &b[0] | ||
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | - | // GoString copies a char* to a Go string. | 25 | + | // GoString copies a null-terminated char* to a Go string. |
| 26 | func GoString(c uintptr) string { | 26 | func GoString(c uintptr) string { | ||
| 27 | // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer | 27 | // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer | ||
| 28 | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c)) | 28 | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c)) | ||
M objc/objc_runtime_darwin.go +168 -180
| @@ -10,13 +10,15 @@ import ( | |||
| 10 | 10 | "fmt" | |
| 11 | 11 | "math" | |
| 12 | 12 | "reflect" | |
| 13 | + | "regexp" | |
| 14 | + | "unicode" | |
| 15 | + | "unsafe" | |
| 13 | 16 | ||
| 14 | 17 | "github.com/ebitengine/purego" | |
| 15 | 18 | ) | |
| 16 | 19 | ||
| 17 | - | //TODO: support try/catch? | |
| 18 | - | //https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime | |
| 19 | - | ||
| 20 | + | // TODO: support try/catch? | |
| 21 | + | // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime | |
| 20 | 22 | var ( | |
| 21 | 23 | objc_msgSend_fn uintptr | |
| 22 | 24 | objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID | |
| @@ -34,7 +36,12 @@ var ( | |||
| 34 | 36 | class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool | |
| 35 | 37 | class_addProtocol func(class Class, protocol *Protocol) bool | |
| 36 | 38 | ivar_getOffset func(ivar Ivar) uintptr | |
| 39 | + | ivar_getName func(ivar Ivar) string | |
| 37 | 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 | |
| 38 | 45 | ) | |
| 39 | 46 | ||
| 40 | 47 | func init() { | |
| @@ -65,6 +72,11 @@ func init() { | |||
| 65 | 72 | purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol") | |
| 66 | 73 | purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize") | |
| 67 | 74 | purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset") | |
| 75 | + | purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName") | |
| 76 | + | purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName") | |
| 77 | + | purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual") | |
| 78 | + | purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar") | |
| 79 | + | purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar") | |
| 68 | 80 | } | |
| 69 | 81 | ||
| 70 | 82 | // ID is an opaque pointer to some Objective-C object | |
| @@ -82,6 +94,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID { | |||
| 82 | 94 | return objc_msgSend(id, sel, args...) | |
| 83 | 95 | } | |
| 84 | 96 | ||
| 97 | + | // GetIvar reads the value of an instance variable in an object. | |
| 98 | + | func (id ID) GetIvar(ivar Ivar) ID { | |
| 99 | + | return object_getIvar(id, ivar) | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // SetIvar sets the value of an instance variable in an object. | |
| 103 | + | func (id ID) SetIvar(ivar Ivar, value ID) { | |
| 104 | + | object_setIvar(id, ivar, value) | |
| 105 | + | } | |
| 106 | + | ||
| 85 | 107 | // Send is a convenience method for sending messages to objects that can return any type. | |
| 86 | 108 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | |
| 87 | 109 | // It is best to cache the result of RegisterName. | |
| @@ -103,7 +125,7 @@ type objc_super struct { | |||
| 103 | 125 | // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result | |
| 104 | 126 | // of RegisterName. | |
| 105 | 127 | func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | |
| 106 | - | var super = &objc_super{ | |
| 128 | + | super := &objc_super{ | |
| 107 | 129 | receiver: id, | |
| 108 | 130 | superClass: id.Class(), | |
| 109 | 131 | } | |
| @@ -114,7 +136,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | |||
| 114 | 136 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | |
| 115 | 137 | // It is best to cache the result of RegisterName. | |
| 116 | 138 | func SendSuper[T any](id ID, sel SEL, args ...any) T { | |
| 117 | - | var super = &objc_super{ | |
| 139 | + | super := &objc_super{ | |
| 118 | 140 | receiver: id, | |
| 119 | 141 | superClass: id.Class(), | |
| 120 | 142 | } | |
| @@ -148,203 +170,164 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class { | |||
| 148 | 170 | return objc_allocateClassPair(super, name, extraBytes) | |
| 149 | 171 | } | |
| 150 | 172 | ||
| 151 | - | // Selector is an interface that takes a Go method name | |
| 152 | - | // and returns the selector equivalent name. | |
| 153 | - | // If it returns a nil SEL then that method | |
| 154 | - | // is not added to the class object. | |
| 155 | - | type Selector interface { | |
| 156 | - | Selector(string) SEL | |
| 173 | + | // MethodDef represents the Go function and the selector that ObjC uses to access that function. | |
| 174 | + | type MethodDef struct { | |
| 175 | + | Cmd SEL | |
| 176 | + | Fn any | |
| 157 | 177 | } | |
| 158 | 178 | ||
| 159 | - | // TagFormatError occurs when the parser fails to parse the objc tag in a Selector object | |
| 160 | - | var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`) | |
| 179 | + | // IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically | |
| 180 | + | // generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes. | |
| 181 | + | // The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib. | |
| 182 | + | // | |
| 183 | + | // Take for example this Objective-C code: | |
| 184 | + | // | |
| 185 | + | // @property (readwrite) float value; | |
| 186 | + | // | |
| 187 | + | // In Go, the functions can be accessed as followed: | |
| 188 | + | // | |
| 189 | + | // var value = purego.Send[float32](id, purego.RegisterName("value")) | |
| 190 | + | // id.Send(purego.RegisterName("setValue:"), 3.46) | |
| 191 | + | // | |
| 192 | + | // [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html | |
| 193 | + | type IvarAttrib int | |
| 161 | 194 | ||
| 162 | - | // MismatchError occurs when the Go struct definition doesn't match that of Objective-C | |
| 163 | - | var MismatchError = errors.New("go struct doesn't match objective-c struct") | |
| 195 | + | const ( | |
| 196 | + | ReadOnly IvarAttrib = 1 << iota | |
| 197 | + | ReadWrite | |
| 198 | + | ) | |
| 164 | 199 | ||
| 165 | - | // RegisterClass takes a pointer to a struct that implements the Selector interface. | |
| 166 | - | // It will register the structs fields and pointer receiver methods in the Objective-C | |
| 167 | - | // runtime using the SEL returned from Selector. Any errors that occur trying to add | |
| 168 | - | // a Method or Ivar is returned as an error. Such errors may occur in parsing or because | |
| 169 | - | // the size of the struct does not match the size in Objective-C. If no errors occur | |
| 170 | - | // then the returned Class has been registered successfully. | |
| 171 | - | // | |
| 172 | - | // The struct's first field must be of type Class and have a tag that matches the format | |
| 173 | - | // `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class | |
| 174 | - | // would be defined in Objective-C. | |
| 175 | - | func RegisterClass(object Selector) (Class, error) { | |
| 176 | - | ptr := reflect.TypeOf(object) | |
| 177 | - | strct := ptr.Elem() | |
| 178 | - | if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) { | |
| 179 | - | return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError) | |
| 180 | - | } | |
| 181 | - | isa := strct.Field(0) | |
| 182 | - | tag := isa.Tag.Get("objc") | |
| 183 | - | if tag == "" { | |
| 184 | - | return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError) | |
| 185 | - | } | |
| 186 | - | // split contains the class name and super class name followed by all the Protocols | |
| 187 | - | // start with two for ClassName : SuperClassName | |
| 188 | - | var split = make([]string, 2) | |
| 189 | - | { | |
| 190 | - | // This is a simple parser for the objc tag that looks for the format | |
| 191 | - | // "ClassName : SuperClassName <Protocol, ...>" | |
| 192 | - | // It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...] | |
| 193 | - | ||
| 194 | - | var i int // from tag[0:i] is whatever identifier is next | |
| 195 | - | var r rune // r is the current rune | |
| 196 | - | skipSpace := func() { | |
| 197 | - | for _, c := range tag { | |
| 198 | - | if c == ' ' { | |
| 199 | - | tag = tag[1:] | |
| 200 | - | continue | |
| 201 | - | } | |
| 202 | - | break | |
| 203 | - | } | |
| 204 | - | } | |
| 205 | - | skipSpace() | |
| 206 | - | // get ClassName | |
| 207 | - | for i, r = range tag { | |
| 208 | - | if r == ' ' || r == ':' { | |
| 209 | - | break | |
| 210 | - | } | |
| 211 | - | } | |
| 212 | - | split[0] = tag[0:i] // store ClassName | |
| 213 | - | tag = tag[i:] | |
| 200 | + | // FieldDef is a definition of a field to add to an Objective-C class. | |
| 201 | + | // The name of the field is what will be used to access it through the Ivar. If the type is bool | |
| 202 | + | // the name cannot start with `is` since a getter will be generated with the name `isBoolName`. | |
| 203 | + | // The name also cannot contain any spaces. | |
| 204 | + | // The type is the Go equivalent type of the Ivar. | |
| 205 | + | // Attribute determines if a getter and or setter method is generated for this field. | |
| 206 | + | type FieldDef struct { | |
| 207 | + | Name string | |
| 208 | + | Type reflect.Type | |
| 209 | + | Attribute IvarAttrib | |
| 210 | + | } | |
| 214 | 211 | ||
| 215 | - | skipSpace() | |
| 212 | + | // ivarRegex checks to make sure the Ivar is correctly formatted | |
| 213 | + | var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*") | |
| 216 | 214 | ||
| 217 | - | // check for ':' | |
| 218 | - | if len(tag) > 0 && tag[0] != ':' { | |
| 219 | - | return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError) | |
| 220 | - | } | |
| 221 | - | tag = tag[1:] // skip ':' | |
| 222 | - | skipSpace() | |
| 223 | - | ||
| 224 | - | // get SuperClassName | |
| 225 | - | for i, r = range tag { | |
| 226 | - | if r == ' ' { | |
| 227 | - | break | |
| 228 | - | } else if i+1 == len(tag) { | |
| 229 | - | // if this is the last character in the string | |
| 230 | - | // make sure to increment i so that tag[:i] | |
| 231 | - | // includes the last character | |
| 232 | - | i++ | |
| 233 | - | break | |
| 234 | - | } | |
| 235 | - | } | |
| 236 | - | if len(tag) < i { | |
| 237 | - | return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError) | |
| 238 | - | } | |
| 239 | - | split[1] = tag[:i] // store SuperClassName | |
| 240 | - | tag = tag[i:] // drop SuperClassName | |
| 241 | - | skipSpace() | |
| 242 | - | if len(tag) > 0 { | |
| 243 | - | if tag[0] != '<' { | |
| 244 | - | return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError) | |
| 245 | - | } | |
| 246 | - | tag = tag[1:] // drop '<' | |
| 247 | - | // get Protocols | |
| 248 | - | outer: | |
| 249 | - | for { | |
| 250 | - | skipSpace() | |
| 251 | - | for i, r = range tag { | |
| 252 | - | switch r { | |
| 253 | - | case ' ': | |
| 254 | - | split = append(split, tag[:i]) | |
| 255 | - | tag = tag[i:] | |
| 256 | - | continue outer | |
| 257 | - | case ',': | |
| 258 | - | // If there is actually an identifier - add it. | |
| 259 | - | if i > 0 { | |
| 260 | - | split = append(split, tag[:i]) | |
| 261 | - | tag = tag[i:] | |
| 262 | - | } else { | |
| 263 | - | // Otherwise, drop ',' | |
| 264 | - | tag = tag[1:] | |
| 265 | - | } | |
| 266 | - | continue outer | |
| 267 | - | case '>': | |
| 268 | - | // If there is actually an identifier - add it. | |
| 269 | - | if i > 0 { | |
| 270 | - | split = append(split, tag[:i]) | |
| 271 | - | tag = tag[i:] | |
| 272 | - | } | |
| 273 | - | break outer | |
| 274 | - | } | |
| 275 | - | } | |
| 276 | - | return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError) | |
| 277 | - | } | |
| 278 | - | } | |
| 279 | - | } | |
| 280 | - | class := objc_allocateClassPair(GetClass(split[1]), split[0], 0) | |
| 215 | + | // RegisterClass takes the name of the class to create, the superclass, a list of protocols this class | |
| 216 | + | // implements, a list of fields this class has and a list of methods. It returns the created class or an error | |
| 217 | + | // describing what went wrong. | |
| 218 | + | func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) { | |
| 219 | + | class := objc_allocateClassPair(superClass, name, 0) | |
| 281 | 220 | if class == 0 { | |
| 282 | - | return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0]) | |
| 221 | + | return 0, fmt.Errorf("objc: failed to create class with name '%s'", name) | |
| 283 | 222 | } | |
| 284 | - | if len(split) > 2 { | |
| 285 | - | // Add Protocols | |
| 286 | - | for _, n := range split[2:] { | |
| 287 | - | if !class.AddProtocol(GetProtocol(n)) { | |
| 288 | - | return 0, fmt.Errorf("objc: couldn't add Protocol %s", n) | |
| 289 | - | } | |
| 223 | + | // Add Protocols | |
| 224 | + | for _, p := range protocols { | |
| 225 | + | if !class.AddProtocol(p) { | |
| 226 | + | return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p)) | |
| 290 | 227 | } | |
| 291 | 228 | } | |
| 292 | - | // Add exported methods based on the selectors returned from Selector(string) SEL | |
| 293 | - | for i := 0; i < ptr.NumMethod(); i++ { | |
| 294 | - | met := ptr.Method(i) | |
| 295 | - | // we know this method is the interface one since RegisterClass | |
| 296 | - | // requires that the struct implement Selector. | |
| 297 | - | if met.Name == "Selector" { | |
| 298 | - | continue | |
| 299 | - | } | |
| 300 | - | sel := object.Selector(met.Name) | |
| 301 | - | if sel == 0 { | |
| 302 | - | continue | |
| 303 | - | } | |
| 304 | - | fn := met.Func.Interface() | |
| 229 | + | // Add exported methods based on the selectors returned from ClassDef(string) SEL | |
| 230 | + | for idx, def := range methods { | |
| 305 | 231 | imp, err := func() (imp IMP, err error) { | |
| 306 | 232 | defer func() { | |
| 307 | 233 | if r := recover(); r != nil { | |
| 308 | 234 | err = fmt.Errorf("objc: failed to create IMP: %s", r) | |
| 309 | 235 | } | |
| 310 | 236 | }() | |
| 311 | - | return NewIMP(fn), nil | |
| 237 | + | return NewIMP(def.Fn), nil | |
| 312 | 238 | }() | |
| 313 | 239 | if err != nil { | |
| 314 | - | return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err) | |
| 240 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err) | |
| 315 | 241 | } | |
| 316 | - | encoding, err := encodeFunc(fn) | |
| 242 | + | encoding, err := encodeFunc(def.Fn) | |
| 317 | 243 | if err != nil { | |
| 318 | - | return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err) | |
| 244 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err) | |
| 319 | 245 | } | |
| 320 | - | if !class.AddMethod(sel, imp, encoding) { | |
| 321 | - | return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name) | |
| 246 | + | if !class.AddMethod(def.Cmd, imp, encoding) { | |
| 247 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx) | |
| 322 | 248 | } | |
| 323 | 249 | } | |
| 324 | 250 | // Add Ivars | |
| 325 | - | // Start at 1 because we skip the class object which is first | |
| 326 | - | for i := 1; i < strct.NumField(); i++ { | |
| 327 | - | f := strct.Field(i) | |
| 328 | - | if f.Name == "_" { | |
| 329 | - | continue | |
| 251 | + | for _, instVar := range ivars { | |
| 252 | + | ivar := instVar | |
| 253 | + | if !ivarRegex.MatchString(ivar.Name) { | |
| 254 | + | return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name) | |
| 330 | 255 | } | |
| 331 | - | size := f.Type.Size() | |
| 332 | - | alignment := uint8(math.Log2(float64(f.Type.Align()))) | |
| 333 | - | enc, err := encodeType(f.Type, false) | |
| 256 | + | size := ivar.Type.Size() | |
| 257 | + | alignment := uint8(math.Log2(float64(ivar.Type.Align()))) | |
| 258 | + | enc, err := encodeType(ivar.Type, false) | |
| 334 | 259 | if err != nil { | |
| 335 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err) | |
| 260 | + | return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err) | |
| 336 | 261 | } | |
| 337 | - | if !class_addIvar(class, f.Name, size, alignment, enc) { | |
| 338 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name) | |
| 262 | + | if !class_addIvar(class, ivar.Name, size, alignment, enc) { | |
| 263 | + | return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name) | |
| 339 | 264 | } | |
| 340 | - | if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset { | |
| 341 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset) | |
| 265 | + | offset := class.InstanceVariable(ivar.Name).Offset() | |
| 266 | + | switch ivar.Attribute { | |
| 267 | + | case ReadWrite: | |
| 268 | + | ty := reflect.FuncOf( | |
| 269 | + | []reflect.Type{ | |
| 270 | + | reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type, | |
| 271 | + | }, | |
| 272 | + | nil, false, | |
| 273 | + | ) | |
| 274 | + | var encoding string | |
| 275 | + | if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil { | |
| 276 | + | return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err) | |
| 277 | + | } | |
| 278 | + | val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |
| 279 | + | // on entry the first and second arguments are ID and SEL followed by the value | |
| 280 | + | if len(args) != 3 { | |
| 281 | + | panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args))) | |
| 282 | + | } | |
| 283 | + | // The following reflect code does the equivalent of this: | |
| 284 | + | // | |
| 285 | + | // ((*struct { | |
| 286 | + | // Padding [offset]byte | |
| 287 | + | // Value int | |
| 288 | + | // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123 | |
| 289 | + | // | |
| 290 | + | // However, since the type of the variable is unknown reflection is used to actually assign the value | |
| 291 | + | id := args[0].Interface().(ID) | |
| 292 | + | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet | |
| 293 | + | reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2]) | |
| 294 | + | return nil | |
| 295 | + | }).Interface() | |
| 296 | + | // this code only works for ascii but that shouldn't be a problem | |
| 297 | + | selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00" | |
| 298 | + | class.AddMethod(RegisterName(selector), NewIMP(val), encoding) | |
| 299 | + | fallthrough // also implement the read method | |
| 300 | + | case ReadOnly: | |
| 301 | + | ty := reflect.FuncOf( | |
| 302 | + | []reflect.Type{ | |
| 303 | + | reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), | |
| 304 | + | }, | |
| 305 | + | []reflect.Type{ivar.Type}, false, | |
| 306 | + | ) | |
| 307 | + | var encoding string | |
| 308 | + | if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil { | |
| 309 | + | return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err) | |
| 310 | + | } | |
| 311 | + | val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |
| 312 | + | // on entry the first and second arguments are ID and SEL | |
| 313 | + | if len(args) != 2 { | |
| 314 | + | panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args))) | |
| 315 | + | } | |
| 316 | + | id := args[0].Interface().(ID) | |
| 317 | + | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet | |
| 318 | + | // the variable is located at an offset from the id | |
| 319 | + | return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()} | |
| 320 | + | }).Interface() | |
| 321 | + | if ivar.Type.Kind() == reflect.Bool { | |
| 322 | + | // this code only works for ascii but that shouldn't be a problem | |
| 323 | + | ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] | |
| 324 | + | } | |
| 325 | + | class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding) | |
| 326 | + | default: | |
| 327 | + | return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute) | |
| 342 | 328 | } | |
| 343 | 329 | } | |
| 344 | 330 | objc_registerClassPair(class) | |
| 345 | - | if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 { | |
| 346 | - | return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError) | |
| 347 | - | } | |
| 348 | 331 | return class, nil | |
| 349 | 332 | } | |
| 350 | 333 | ||
| @@ -420,7 +403,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) { | |||
| 420 | 403 | if insidePtr { | |
| 421 | 404 | return encStructBegin + typ.Name() + encStructEnd, nil | |
| 422 | 405 | } | |
| 423 | - | var encoding = encStructBegin | |
| 406 | + | encoding := encStructBegin | |
| 424 | 407 | encoding += typ.Name() | |
| 425 | 408 | encoding += "=" | |
| 426 | 409 | for i := 0; i < typ.NumField(); i++ { | |
| @@ -543,20 +526,29 @@ func (i Ivar) Offset() uintptr { | |||
| 543 | 526 | return ivar_getOffset(i) | |
| 544 | 527 | } | |
| 545 | 528 | ||
| 529 | + | func (i Ivar) Name() string { | |
| 530 | + | return ivar_getName(i) | |
| 531 | + | } | |
| 532 | + | ||
| 546 | 533 | // Protocol is a type that declares methods that can be implemented by any class. | |
| 547 | - | type Protocol uintptr | |
| 534 | + | type Protocol [0]func() | |
| 548 | 535 | ||
| 549 | 536 | // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name. | |
| 550 | 537 | func GetProtocol(name string) *Protocol { | |
| 551 | 538 | return objc_getProtocol(name) | |
| 552 | 539 | } | |
| 553 | 540 | ||
| 541 | + | // Equals return true if the two protocols are the same. | |
| 542 | + | func (p *Protocol) Equals(p2 *Protocol) bool { | |
| 543 | + | return protocol_isEqual(p, p2) | |
| 544 | + | } | |
| 545 | + | ||
| 554 | 546 | // IMP is a function pointer that can be called by Objective-C code. | |
| 555 | 547 | type IMP uintptr | |
| 556 | 548 | ||
| 557 | 549 | // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments. | |
| 558 | - | // ID may instead be a pointer to a struct whose first field has type Class. | |
| 559 | 550 | // It returns an IMP function pointer that can be called by Objective-C code. | |
| 551 | + | // The function panics if an error occurs. | |
| 560 | 552 | // The function pointer is never deallocated. | |
| 561 | 553 | func NewIMP(fn interface{}) IMP { | |
| 562 | 554 | ty := reflect.TypeOf(fn) | |
| @@ -568,13 +560,9 @@ func NewIMP(fn interface{}) IMP { | |||
| 568 | 560 | switch { | |
| 569 | 561 | case ty.NumIn() < 2: | |
| 570 | 562 | fallthrough | |
| 571 | - | case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID | |
| 572 | - | // or that it's a pointer to a struct | |
| 573 | - | (ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct || | |
| 574 | - | // and that the structs first field is an objc.Class | |
| 575 | - | ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))): | |
| 563 | + | case ty.In(0) != reflect.TypeOf(ID(0)): | |
| 576 | 564 | fallthrough | |
| 577 | - | case ty.In(1).Kind() != reflect.Uintptr: | |
| 565 | + | case ty.In(1) != reflect.TypeOf(SEL(0)): | |
| 578 | 566 | panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String()) | |
| 579 | 567 | } | |
| 580 | 568 | return IMP(purego.NewCallback(fn)) | |
| @@ -10,13 +10,15 @@ import ( | |||||
| 10 | "fmt" | 10 | "fmt" | ||
| 11 | "math" | 11 | "math" | ||
| 12 | "reflect" | 12 | "reflect" | ||
| 13 | + | "regexp" | |||
| 14 | + | "unicode" | |||
| 15 | + | "unsafe" | |||
| 13 | 16 | ||||
| 14 | "github.com/ebitengine/purego" | 17 | "github.com/ebitengine/purego" | ||
| 15 | ) | 18 | ) | ||
| 16 | 19 | ||||
| 17 | - | //TODO: support try/catch? | 20 | + | // TODO: support try/catch? |
| 18 | - | //https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime | 21 | + | // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime |
| 19 | - | ||||
| 20 | var ( | 22 | var ( | ||
| 21 | objc_msgSend_fn uintptr | 23 | objc_msgSend_fn uintptr | ||
| 22 | objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID | 24 | objc_msgSend func(obj ID, cmd SEL, args ...interface{}) ID | ||
| @@ -34,7 +36,12 @@ var ( | |||||
| 34 | class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool | 36 | class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool | ||
| 35 | class_addProtocol func(class Class, protocol *Protocol) bool | 37 | class_addProtocol func(class Class, protocol *Protocol) bool | ||
| 36 | ivar_getOffset func(ivar Ivar) uintptr | 38 | ivar_getOffset func(ivar Ivar) uintptr | ||
| 39 | + | ivar_getName func(ivar Ivar) string | |||
| 37 | object_getClass func(obj ID) Class | 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 | |||
| 38 | ) | 45 | ) | ||
| 39 | 46 | ||||
| 40 | func init() { | 47 | func init() { | ||
| @@ -65,6 +72,11 @@ func init() { | |||||
| 65 | purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol") | 72 | purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol") | ||
| 66 | purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize") | 73 | purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize") | ||
| 67 | purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset") | 74 | purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset") | ||
| 75 | + | purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName") | |||
| 76 | + | purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName") | |||
| 77 | + | purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual") | |||
| 78 | + | purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar") | |||
| 79 | + | purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar") | |||
| 68 | } | 80 | } | ||
| 69 | 81 | ||||
| 70 | // ID is an opaque pointer to some Objective-C object | 82 | // ID is an opaque pointer to some Objective-C object | ||
| @@ -82,6 +94,16 @@ func (id ID) Send(sel SEL, args ...interface{}) ID { | |||||
| 82 | return objc_msgSend(id, sel, args...) | 94 | return objc_msgSend(id, sel, args...) | ||
| 83 | } | 95 | } | ||
| 84 | 96 | ||||
| 97 | + | // GetIvar reads the value of an instance variable in an object. | |||
| 98 | + | func (id ID) GetIvar(ivar Ivar) ID { | |||
| 99 | + | return object_getIvar(id, ivar) | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // SetIvar sets the value of an instance variable in an object. | |||
| 103 | + | func (id ID) SetIvar(ivar Ivar, value ID) { | |||
| 104 | + | object_setIvar(id, ivar, value) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 85 | // Send is a convenience method for sending messages to objects that can return any type. | 107 | // Send is a convenience method for sending messages to objects that can return any type. | ||
| 86 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | 108 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | ||
| 87 | // It is best to cache the result of RegisterName. | 109 | // It is best to cache the result of RegisterName. | ||
| @@ -103,7 +125,7 @@ type objc_super struct { | |||||
| 103 | // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result | 125 | // instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result | ||
| 104 | // of RegisterName. | 126 | // of RegisterName. | ||
| 105 | func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | 127 | func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | ||
| 106 | - | var super = &objc_super{ | 128 | + | super := &objc_super{ |
| 107 | receiver: id, | 129 | receiver: id, | ||
| 108 | superClass: id.Class(), | 130 | superClass: id.Class(), | ||
| 109 | } | 131 | } | ||
| @@ -114,7 +136,7 @@ func (id ID) SendSuper(sel SEL, args ...interface{}) ID { | |||||
| 114 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | 136 | // This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock. | ||
| 115 | // It is best to cache the result of RegisterName. | 137 | // It is best to cache the result of RegisterName. | ||
| 116 | func SendSuper[T any](id ID, sel SEL, args ...any) T { | 138 | func SendSuper[T any](id ID, sel SEL, args ...any) T { | ||
| 117 | - | var super = &objc_super{ | 139 | + | super := &objc_super{ |
| 118 | receiver: id, | 140 | receiver: id, | ||
| 119 | superClass: id.Class(), | 141 | superClass: id.Class(), | ||
| 120 | } | 142 | } | ||
| @@ -148,203 +170,164 @@ func AllocateClassPair(super Class, name string, extraBytes uintptr) Class { | |||||
| 148 | return objc_allocateClassPair(super, name, extraBytes) | 170 | return objc_allocateClassPair(super, name, extraBytes) | ||
| 149 | } | 171 | } | ||
| 150 | 172 | ||||
| 151 | - | // Selector is an interface that takes a Go method name | 173 | + | // MethodDef represents the Go function and the selector that ObjC uses to access that function. |
| 152 | - | // and returns the selector equivalent name. | 174 | + | type MethodDef struct { |
| 153 | - | // If it returns a nil SEL then that method | 175 | + | Cmd SEL |
| 154 | - | // is not added to the class object. | 176 | + | Fn any |
| 155 | - | type Selector interface { | |||
| 156 | - | Selector(string) SEL | |||
| 157 | } | 177 | } | ||
| 158 | 178 | ||||
| 159 | - | // TagFormatError occurs when the parser fails to parse the objc tag in a Selector object | 179 | + | // IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically |
| 160 | - | var TagFormatError = errors.New(`objc tag doesn't match "ClassName : SuperClassName <Protocol, ...>""`) | 180 | + | // generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes. |
| 181 | + | // The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib. | |||
| 182 | + | // | |||
| 183 | + | // Take for example this Objective-C code: | |||
| 184 | + | // | |||
| 185 | + | // @property (readwrite) float value; | |||
| 186 | + | // | |||
| 187 | + | // In Go, the functions can be accessed as followed: | |||
| 188 | + | // | |||
| 189 | + | // var value = purego.Send[float32](id, purego.RegisterName("value")) | |||
| 190 | + | // id.Send(purego.RegisterName("setValue:"), 3.46) | |||
| 191 | + | // | |||
| 192 | + | // [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html | |||
| 193 | + | type IvarAttrib int | |||
| 161 | 194 | ||||
| 162 | - | // MismatchError occurs when the Go struct definition doesn't match that of Objective-C | 195 | + | const ( |
| 163 | - | var MismatchError = errors.New("go struct doesn't match objective-c struct") | 196 | + | ReadOnly IvarAttrib = 1 << iota |
| 197 | + | ReadWrite | |||
| 198 | + | ) | |||
| 164 | 199 | ||||
| 165 | - | // RegisterClass takes a pointer to a struct that implements the Selector interface. | 200 | + | // FieldDef is a definition of a field to add to an Objective-C class. |
| 166 | - | // It will register the structs fields and pointer receiver methods in the Objective-C | 201 | + | // The name of the field is what will be used to access it through the Ivar. If the type is bool |
| 167 | - | // runtime using the SEL returned from Selector. Any errors that occur trying to add | 202 | + | // the name cannot start with `is` since a getter will be generated with the name `isBoolName`. |
| 168 | - | // a Method or Ivar is returned as an error. Such errors may occur in parsing or because | 203 | + | // The name also cannot contain any spaces. |
| 169 | - | // the size of the struct does not match the size in Objective-C. If no errors occur | 204 | + | // The type is the Go equivalent type of the Ivar. |
| 170 | - | // then the returned Class has been registered successfully. | 205 | + | // Attribute determines if a getter and or setter method is generated for this field. |
| 171 | - | // | 206 | + | type FieldDef struct { |
| 172 | - | // The struct's first field must be of type Class and have a tag that matches the format | 207 | + | Name string |
| 173 | - | // `objc:"ClassName : SuperClassName <Protocol, ...>`. This tag is equal to how the class | 208 | + | Type reflect.Type |
| 174 | - | // would be defined in Objective-C. | 209 | + | Attribute IvarAttrib |
| 175 | - | func RegisterClass(object Selector) (Class, error) { | 210 | + | } |
| 176 | - | ptr := reflect.TypeOf(object) | |||
| 177 | - | strct := ptr.Elem() | |||
| 178 | - | if strct.NumField() == 0 || strct.Field(0).Type != reflect.TypeOf(Class(0)) { | |||
| 179 | - | return 0, fmt.Errorf("objc: need objc.Class as first field: %w", MismatchError) | |||
| 180 | - | } | |||
| 181 | - | isa := strct.Field(0) | |||
| 182 | - | tag := isa.Tag.Get("objc") | |||
| 183 | - | if tag == "" { | |||
| 184 | - | return 0, fmt.Errorf("objc: missing objc tag: %w", TagFormatError) | |||
| 185 | - | } | |||
| 186 | - | // split contains the class name and super class name followed by all the Protocols | |||
| 187 | - | // start with two for ClassName : SuperClassName | |||
| 188 | - | var split = make([]string, 2) | |||
| 189 | - | { | |||
| 190 | - | // This is a simple parser for the objc tag that looks for the format | |||
| 191 | - | // "ClassName : SuperClassName <Protocol, ...>" | |||
| 192 | - | // It appends to the split variable with the [ClassName, SuperClassName, Protocol, ...] | |||
| 193 | - | ||||
| 194 | - | var i int // from tag[0:i] is whatever identifier is next | |||
| 195 | - | var r rune // r is the current rune | |||
| 196 | - | skipSpace := func() { | |||
| 197 | - | for _, c := range tag { | |||
| 198 | - | if c == ' ' { | |||
| 199 | - | tag = tag[1:] | |||
| 200 | - | continue | |||
| 201 | - | } | |||
| 202 | - | break | |||
| 203 | - | } | |||
| 204 | - | } | |||
| 205 | - | skipSpace() | |||
| 206 | - | // get ClassName | |||
| 207 | - | for i, r = range tag { | |||
| 208 | - | if r == ' ' || r == ':' { | |||
| 209 | - | break | |||
| 210 | - | } | |||
| 211 | - | } | |||
| 212 | - | split[0] = tag[0:i] // store ClassName | |||
| 213 | - | tag = tag[i:] | |||
| 214 | 211 | ||||
| 215 | - | skipSpace() | 212 | + | // ivarRegex checks to make sure the Ivar is correctly formatted |
| 213 | + | var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*") | |||
| 216 | 214 | ||||
| 217 | - | // check for ':' | 215 | + | // RegisterClass takes the name of the class to create, the superclass, a list of protocols this class |
| 218 | - | if len(tag) > 0 && tag[0] != ':' { | 216 | + | // implements, a list of fields this class has and a list of methods. It returns the created class or an error |
| 219 | - | return 0, fmt.Errorf("objc: missing ':': %w", TagFormatError) | 217 | + | // describing what went wrong. |
| 220 | - | } | 218 | + | func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) { |
| 221 | - | tag = tag[1:] // skip ':' | 219 | + | class := objc_allocateClassPair(superClass, name, 0) |
| 222 | - | skipSpace() | |||
| 223 | - | ||||
| 224 | - | // get SuperClassName | |||
| 225 | - | for i, r = range tag { | |||
| 226 | - | if r == ' ' { | |||
| 227 | - | break | |||
| 228 | - | } else if i+1 == len(tag) { | |||
| 229 | - | // if this is the last character in the string | |||
| 230 | - | // make sure to increment i so that tag[:i] | |||
| 231 | - | // includes the last character | |||
| 232 | - | i++ | |||
| 233 | - | break | |||
| 234 | - | } | |||
| 235 | - | } | |||
| 236 | - | if len(tag) < i { | |||
| 237 | - | return 0, fmt.Errorf("objc: missing SuperClassName: %w", TagFormatError) | |||
| 238 | - | } | |||
| 239 | - | split[1] = tag[:i] // store SuperClassName | |||
| 240 | - | tag = tag[i:] // drop SuperClassName | |||
| 241 | - | skipSpace() | |||
| 242 | - | if len(tag) > 0 { | |||
| 243 | - | if tag[0] != '<' { | |||
| 244 | - | return 0, fmt.Errorf("objc: expected '<': %w", TagFormatError) | |||
| 245 | - | } | |||
| 246 | - | tag = tag[1:] // drop '<' | |||
| 247 | - | // get Protocols | |||
| 248 | - | outer: | |||
| 249 | - | for { | |||
| 250 | - | skipSpace() | |||
| 251 | - | for i, r = range tag { | |||
| 252 | - | switch r { | |||
| 253 | - | case ' ': | |||
| 254 | - | split = append(split, tag[:i]) | |||
| 255 | - | tag = tag[i:] | |||
| 256 | - | continue outer | |||
| 257 | - | case ',': | |||
| 258 | - | // If there is actually an identifier - add it. | |||
| 259 | - | if i > 0 { | |||
| 260 | - | split = append(split, tag[:i]) | |||
| 261 | - | tag = tag[i:] | |||
| 262 | - | } else { | |||
| 263 | - | // Otherwise, drop ',' | |||
| 264 | - | tag = tag[1:] | |||
| 265 | - | } | |||
| 266 | - | continue outer | |||
| 267 | - | case '>': | |||
| 268 | - | // If there is actually an identifier - add it. | |||
| 269 | - | if i > 0 { | |||
| 270 | - | split = append(split, tag[:i]) | |||
| 271 | - | tag = tag[i:] | |||
| 272 | - | } | |||
| 273 | - | break outer | |||
| 274 | - | } | |||
| 275 | - | } | |||
| 276 | - | return 0, fmt.Errorf("objc: expected '>': %w", TagFormatError) | |||
| 277 | - | } | |||
| 278 | - | } | |||
| 279 | - | } | |||
| 280 | - | class := objc_allocateClassPair(GetClass(split[1]), split[0], 0) | |||
| 281 | if class == 0 { | 220 | if class == 0 { | ||
| 282 | - | return 0, fmt.Errorf("objc: failed to create class with name '%s'", split[0]) | 221 | + | return 0, fmt.Errorf("objc: failed to create class with name '%s'", name) |
| 283 | } | 222 | } | ||
| 284 | - | if len(split) > 2 { | 223 | + | // Add Protocols |
| 285 | - | // Add Protocols | 224 | + | for _, p := range protocols { |
| 286 | - | for _, n := range split[2:] { | 225 | + | if !class.AddProtocol(p) { |
| 287 | - | if !class.AddProtocol(GetProtocol(n)) { | 226 | + | return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p)) |
| 288 | - | return 0, fmt.Errorf("objc: couldn't add Protocol %s", n) | |||
| 289 | - | } | |||
| 290 | } | 227 | } | ||
| 291 | } | 228 | } | ||
| 292 | - | // Add exported methods based on the selectors returned from Selector(string) SEL | 229 | + | // Add exported methods based on the selectors returned from ClassDef(string) SEL |
| 293 | - | for i := 0; i < ptr.NumMethod(); i++ { | 230 | + | for idx, def := range methods { |
| 294 | - | met := ptr.Method(i) | |||
| 295 | - | // we know this method is the interface one since RegisterClass | |||
| 296 | - | // requires that the struct implement Selector. | |||
| 297 | - | if met.Name == "Selector" { | |||
| 298 | - | continue | |||
| 299 | - | } | |||
| 300 | - | sel := object.Selector(met.Name) | |||
| 301 | - | if sel == 0 { | |||
| 302 | - | continue | |||
| 303 | - | } | |||
| 304 | - | fn := met.Func.Interface() | |||
| 305 | imp, err := func() (imp IMP, err error) { | 231 | imp, err := func() (imp IMP, err error) { | ||
| 306 | defer func() { | 232 | defer func() { | ||
| 307 | if r := recover(); r != nil { | 233 | if r := recover(); r != nil { | ||
| 308 | err = fmt.Errorf("objc: failed to create IMP: %s", r) | 234 | err = fmt.Errorf("objc: failed to create IMP: %s", r) | ||
| 309 | } | 235 | } | ||
| 310 | }() | 236 | }() | ||
| 311 | - | return NewIMP(fn), nil | 237 | + | return NewIMP(def.Fn), nil |
| 312 | }() | 238 | }() | ||
| 313 | if err != nil { | 239 | if err != nil { | ||
| 314 | - | return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err) | 240 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err) |
| 315 | } | 241 | } | ||
| 316 | - | encoding, err := encodeFunc(fn) | 242 | + | encoding, err := encodeFunc(def.Fn) |
| 317 | if err != nil { | 243 | if err != nil { | ||
| 318 | - | return 0, fmt.Errorf("objc: couldn't add Method %s: %w", met.Name, err) | 244 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err) |
| 319 | } | 245 | } | ||
| 320 | - | if !class.AddMethod(sel, imp, encoding) { | 246 | + | if !class.AddMethod(def.Cmd, imp, encoding) { |
| 321 | - | return 0, fmt.Errorf("objc: couldn't add Method %s", met.Name) | 247 | + | return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx) |
| 322 | } | 248 | } | ||
| 323 | } | 249 | } | ||
| 324 | // Add Ivars | 250 | // Add Ivars | ||
| 325 | - | // Start at 1 because we skip the class object which is first | 251 | + | for _, instVar := range ivars { |
| 326 | - | for i := 1; i < strct.NumField(); i++ { | 252 | + | ivar := instVar |
| 327 | - | f := strct.Field(i) | 253 | + | if !ivarRegex.MatchString(ivar.Name) { |
| 328 | - | if f.Name == "_" { | 254 | + | return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name) |
| 329 | - | continue | |||
| 330 | } | 255 | } | ||
| 331 | - | size := f.Type.Size() | 256 | + | size := ivar.Type.Size() |
| 332 | - | alignment := uint8(math.Log2(float64(f.Type.Align()))) | 257 | + | alignment := uint8(math.Log2(float64(ivar.Type.Align()))) |
| 333 | - | enc, err := encodeType(f.Type, false) | 258 | + | enc, err := encodeType(ivar.Type, false) |
| 334 | if err != nil { | 259 | if err != nil { | ||
| 335 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", f.Name, err) | 260 | + | return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err) |
| 336 | } | 261 | } | ||
| 337 | - | if !class_addIvar(class, f.Name, size, alignment, enc) { | 262 | + | if !class_addIvar(class, ivar.Name, size, alignment, enc) { |
| 338 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s", f.Name) | 263 | + | return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name) |
| 339 | } | 264 | } | ||
| 340 | - | if offset := class.InstanceVariable(f.Name).Offset(); offset != f.Offset { | 265 | + | offset := class.InstanceVariable(ivar.Name).Offset() |
| 341 | - | return 0, fmt.Errorf("objc: couldn't add Ivar %s because offset (%d != %d)", f.Name, offset, f.Offset) | 266 | + | switch ivar.Attribute { |
| 267 | + | case ReadWrite: | |||
| 268 | + | ty := reflect.FuncOf( | |||
| 269 | + | []reflect.Type{ | |||
| 270 | + | reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type, | |||
| 271 | + | }, | |||
| 272 | + | nil, false, | |||
| 273 | + | ) | |||
| 274 | + | var encoding string | |||
| 275 | + | if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil { | |||
| 276 | + | return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err) | |||
| 277 | + | } | |||
| 278 | + | val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |||
| 279 | + | // on entry the first and second arguments are ID and SEL followed by the value | |||
| 280 | + | if len(args) != 3 { | |||
| 281 | + | panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args))) | |||
| 282 | + | } | |||
| 283 | + | // The following reflect code does the equivalent of this: | |||
| 284 | + | // | |||
| 285 | + | // ((*struct { | |||
| 286 | + | // Padding [offset]byte | |||
| 287 | + | // Value int | |||
| 288 | + | // })(unsafe.Pointer(args[0].Interface().(ID)))).v = 123 | |||
| 289 | + | // | |||
| 290 | + | // However, since the type of the variable is unknown reflection is used to actually assign the value | |||
| 291 | + | id := args[0].Interface().(ID) | |||
| 292 | + | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet | |||
| 293 | + | reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2]) | |||
| 294 | + | return nil | |||
| 295 | + | }).Interface() | |||
| 296 | + | // this code only works for ascii but that shouldn't be a problem | |||
| 297 | + | selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00" | |||
| 298 | + | class.AddMethod(RegisterName(selector), NewIMP(val), encoding) | |||
| 299 | + | fallthrough // also implement the read method | |||
| 300 | + | case ReadOnly: | |||
| 301 | + | ty := reflect.FuncOf( | |||
| 302 | + | []reflect.Type{ | |||
| 303 | + | reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), | |||
| 304 | + | }, | |||
| 305 | + | []reflect.Type{ivar.Type}, false, | |||
| 306 | + | ) | |||
| 307 | + | var encoding string | |||
| 308 | + | if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil { | |||
| 309 | + | return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err) | |||
| 310 | + | } | |||
| 311 | + | val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) { | |||
| 312 | + | // on entry the first and second arguments are ID and SEL | |||
| 313 | + | if len(args) != 2 { | |||
| 314 | + | panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args))) | |||
| 315 | + | } | |||
| 316 | + | id := args[0].Interface().(ID) | |||
| 317 | + | ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet | |||
| 318 | + | // the variable is located at an offset from the id | |||
| 319 | + | return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()} | |||
| 320 | + | }).Interface() | |||
| 321 | + | if ivar.Type.Kind() == reflect.Bool { | |||
| 322 | + | // this code only works for ascii but that shouldn't be a problem | |||
| 323 | + | ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] | |||
| 324 | + | } | |||
| 325 | + | class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding) | |||
| 326 | + | default: | |||
| 327 | + | return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute) | |||
| 342 | } | 328 | } | ||
| 343 | } | 329 | } | ||
| 344 | objc_registerClassPair(class) | 330 | objc_registerClassPair(class) | ||
| 345 | - | if size1, size2 := class.InstanceSize(), strct.Size(); size1 != size2 { | |||
| 346 | - | return 0, fmt.Errorf("objc: sizes don't match %d != %d: %w", size1, size2, MismatchError) | |||
| 347 | - | } | |||
| 348 | return class, nil | 331 | return class, nil | ||
| 349 | } | 332 | } | ||
| 350 | 333 | ||||
| @@ -420,7 +403,7 @@ func encodeType(typ reflect.Type, insidePtr bool) (string, error) { | |||||
| 420 | if insidePtr { | 403 | if insidePtr { | ||
| 421 | return encStructBegin + typ.Name() + encStructEnd, nil | 404 | return encStructBegin + typ.Name() + encStructEnd, nil | ||
| 422 | } | 405 | } | ||
| 423 | - | var encoding = encStructBegin | 406 | + | encoding := encStructBegin |
| 424 | encoding += typ.Name() | 407 | encoding += typ.Name() | ||
| 425 | encoding += "=" | 408 | encoding += "=" | ||
| 426 | for i := 0; i < typ.NumField(); i++ { | 409 | for i := 0; i < typ.NumField(); i++ { | ||
| @@ -543,20 +526,29 @@ func (i Ivar) Offset() uintptr { | |||||
| 543 | return ivar_getOffset(i) | 526 | return ivar_getOffset(i) | ||
| 544 | } | 527 | } | ||
| 545 | 528 | ||||
| 529 | + | func (i Ivar) Name() string { | |||
| 530 | + | return ivar_getName(i) | |||
| 531 | + | } | |||
| 532 | + | ||||
| 546 | // Protocol is a type that declares methods that can be implemented by any class. | 533 | // Protocol is a type that declares methods that can be implemented by any class. | ||
| 547 | - | type Protocol uintptr | 534 | + | type Protocol [0]func() |
| 548 | 535 | ||||
| 549 | // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name. | 536 | // GetProtocol returns the protocol for the given name or nil if there is no protocol by that name. | ||
| 550 | func GetProtocol(name string) *Protocol { | 537 | func GetProtocol(name string) *Protocol { | ||
| 551 | return objc_getProtocol(name) | 538 | return objc_getProtocol(name) | ||
| 552 | } | 539 | } | ||
| 553 | 540 | ||||
| 541 | + | // Equals return true if the two protocols are the same. | |||
| 542 | + | func (p *Protocol) Equals(p2 *Protocol) bool { | |||
| 543 | + | return protocol_isEqual(p, p2) | |||
| 544 | + | } | |||
| 545 | + | ||||
| 554 | // IMP is a function pointer that can be called by Objective-C code. | 546 | // IMP is a function pointer that can be called by Objective-C code. | ||
| 555 | type IMP uintptr | 547 | type IMP uintptr | ||
| 556 | 548 | ||||
| 557 | // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments. | 549 | // NewIMP takes a Go function that takes (ID, SEL) as its first two arguments. | ||
| 558 | - | // ID may instead be a pointer to a struct whose first field has type Class. | |||
| 559 | // It returns an IMP function pointer that can be called by Objective-C code. | 550 | // It returns an IMP function pointer that can be called by Objective-C code. | ||
| 551 | + | // The function panics if an error occurs. | |||
| 560 | // The function pointer is never deallocated. | 552 | // The function pointer is never deallocated. | ||
| 561 | func NewIMP(fn interface{}) IMP { | 553 | func NewIMP(fn interface{}) IMP { | ||
| 562 | ty := reflect.TypeOf(fn) | 554 | ty := reflect.TypeOf(fn) | ||
| @@ -568,13 +560,9 @@ func NewIMP(fn interface{}) IMP { | |||||
| 568 | switch { | 560 | switch { | ||
| 569 | case ty.NumIn() < 2: | 561 | case ty.NumIn() < 2: | ||
| 570 | fallthrough | 562 | fallthrough | ||
| 571 | - | case ty.In(0).Kind() != reflect.Uintptr && // checks if it's objc.ID | 563 | + | case ty.In(0) != reflect.TypeOf(ID(0)): |
| 572 | - | // or that it's a pointer to a struct | |||
| 573 | - | (ty.In(0).Kind() != reflect.Pointer || ty.In(0).Elem().Kind() != reflect.Struct || | |||
| 574 | - | // and that the structs first field is an objc.Class | |||
| 575 | - | ty.In(0).Elem().Field(0).Type != reflect.TypeOf(Class(0))): | |||
| 576 | fallthrough | 564 | fallthrough | ||
| 577 | - | case ty.In(1).Kind() != reflect.Uintptr: | 565 | + | case ty.In(1) != reflect.TypeOf(SEL(0)): |
| 578 | panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String()) | 566 | panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String()) | ||
| 579 | } | 567 | } | ||
| 580 | return IMP(purego.NewCallback(fn)) | 568 | return IMP(purego.NewCallback(fn)) | ||
M objc/objc_runtime_darwin_test.go +46 -19
| @@ -5,6 +5,7 @@ package objc_test | |||
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | 7 | "fmt" | |
| 8 | + | "reflect" | |
| 8 | 9 | "testing" | |
| 9 | 10 | ||
| 10 | 11 | "github.com/ebitengine/purego" | |
| @@ -12,35 +13,61 @@ import ( | |||
| 12 | 13 | ) | |
| 13 | 14 | ||
| 14 | 15 | func ExampleAllocateClassPair() { | |
| 15 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) | |
| 16 | + | class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) | |
| 16 | 17 | class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) { | |
| 17 | 18 | fmt.Println("Hello World!") | |
| 18 | 19 | }), "v@:") | |
| 19 | 20 | class.Register() | |
| 20 | 21 | ||
| 21 | - | var fooObject = objc.ID(class).Send(objc.RegisterName("new")) | |
| 22 | + | fooObject := objc.ID(class).Send(objc.RegisterName("new")) | |
| 22 | 23 | fooObject.Send(objc.RegisterName("run")) | |
| 23 | 24 | // Output: Hello World! | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | - | func ExampleClass_AddIvar() { | |
| 27 | - | type barObject struct { | |
| 28 | - | isa objc.Class | |
| 29 | - | bar int | |
| 27 | + | func ExampleRegisterClass() { | |
| 28 | + | var ( | |
| 29 | + | sel_new = objc.RegisterName("new") | |
| 30 | + | sel_init = objc.RegisterName("init") | |
| 31 | + | sel_setBar = objc.RegisterName("setBar:") | |
| 32 | + | sel_bar = objc.RegisterName("bar") | |
| 33 | + | ||
| 34 | + | BarInit = func(id objc.ID, cmd objc.SEL) objc.ID { | |
| 35 | + | return id.SendSuper(cmd) | |
| 36 | + | } | |
| 37 | + | ) | |
| 38 | + | ||
| 39 | + | class, err := objc.RegisterClass( | |
| 40 | + | "BarObject", | |
| 41 | + | objc.GetClass("NSObject"), | |
| 42 | + | []*objc.Protocol{ | |
| 43 | + | objc.GetProtocol("NSDelegateWindow"), | |
| 44 | + | }, | |
| 45 | + | []objc.FieldDef{ | |
| 46 | + | { | |
| 47 | + | Name: "bar", | |
| 48 | + | Type: reflect.TypeOf(int(0)), | |
| 49 | + | Attribute: objc.ReadWrite, | |
| 50 | + | }, | |
| 51 | + | { | |
| 52 | + | Name: "foo", | |
| 53 | + | Type: reflect.TypeOf(false), | |
| 54 | + | Attribute: objc.ReadWrite, | |
| 55 | + | }, | |
| 56 | + | }, | |
| 57 | + | []objc.MethodDef{ | |
| 58 | + | { | |
| 59 | + | Cmd: sel_init, | |
| 60 | + | Fn: BarInit, | |
| 61 | + | }, | |
| 62 | + | }, | |
| 63 | + | ) | |
| 64 | + | if err != nil { | |
| 65 | + | panic(err) | |
| 30 | 66 | } | |
| 31 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0) | |
| 32 | - | class.AddIvar("bar", int(0), "q") | |
| 33 | - | class.AddMethod(objc.RegisterName("bar"), objc.NewIMP(func(self *barObject, _cmd objc.SEL) int { | |
| 34 | - | return self.bar | |
| 35 | - | }), "q@:") | |
| 36 | - | class.AddMethod(objc.RegisterName("setBar:"), objc.NewIMP(func(self *barObject, _cmd objc.SEL, bar int) { | |
| 37 | - | self.bar = bar | |
| 38 | - | }), "v@:q") | |
| 39 | - | class.Register() | |
| 40 | 67 | ||
| 41 | - | var object = objc.ID(class).Send(objc.RegisterName("new")) | |
| 42 | - | object.Send(objc.RegisterName("setBar:"), 123) | |
| 43 | - | var bar = int(object.Send(objc.RegisterName("bar"))) | |
| 68 | + | object := objc.ID(class).Send(sel_new) | |
| 69 | + | object.Send(sel_setBar, 123) | |
| 70 | + | bar := int(object.Send(sel_bar)) | |
| 44 | 71 | fmt.Println(bar) | |
| 45 | 72 | // Output: 123 | |
| 46 | 73 | } | |
| @@ -85,7 +112,7 @@ func TestSend(t *testing.T) { | |||
| 85 | 112 | // Initialize a NSNumber | |
| 86 | 113 | NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double) | |
| 87 | 114 | // Then get that number back using the generic Send function. | |
| 88 | - | var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue")) | |
| 115 | + | number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue")) | |
| 89 | 116 | if double != number { | |
| 90 | 117 | t.Failed() | |
| 91 | 118 | } | |
| @@ -5,6 +5,7 @@ package objc_test | |||||
| 5 | 5 | ||||
| 6 | import ( | 6 | import ( | ||
| 7 | "fmt" | 7 | "fmt" | ||
| 8 | + | "reflect" | |||
| 8 | "testing" | 9 | "testing" | ||
| 9 | 10 | ||||
| 10 | "github.com/ebitengine/purego" | 11 | "github.com/ebitengine/purego" | ||
| @@ -12,35 +13,61 @@ import ( | |||||
| 12 | ) | 13 | ) | ||
| 13 | 14 | ||||
| 14 | func ExampleAllocateClassPair() { | 15 | func ExampleAllocateClassPair() { | ||
| 15 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) | 16 | + | class := objc.AllocateClassPair(objc.GetClass("NSObject"), "FooObject", 0) |
| 16 | class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) { | 17 | class.AddMethod(objc.RegisterName("run"), objc.NewIMP(func(self objc.ID, _cmd objc.SEL) { | ||
| 17 | fmt.Println("Hello World!") | 18 | fmt.Println("Hello World!") | ||
| 18 | }), "v@:") | 19 | }), "v@:") | ||
| 19 | class.Register() | 20 | class.Register() | ||
| 20 | 21 | ||||
| 21 | - | var fooObject = objc.ID(class).Send(objc.RegisterName("new")) | 22 | + | fooObject := objc.ID(class).Send(objc.RegisterName("new")) |
| 22 | fooObject.Send(objc.RegisterName("run")) | 23 | fooObject.Send(objc.RegisterName("run")) | ||
| 23 | // Output: Hello World! | 24 | // Output: Hello World! | ||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
| 26 | - | func ExampleClass_AddIvar() { | 27 | + | func ExampleRegisterClass() { |
| 27 | - | type barObject struct { | 28 | + | var ( |
| 28 | - | isa objc.Class | 29 | + | sel_new = objc.RegisterName("new") |
| 29 | - | bar int | 30 | + | sel_init = objc.RegisterName("init") |
| 31 | + | sel_setBar = objc.RegisterName("setBar:") | |||
| 32 | + | sel_bar = objc.RegisterName("bar") | |||
| 33 | + | ||||
| 34 | + | BarInit = func(id objc.ID, cmd objc.SEL) objc.ID { | |||
| 35 | + | return id.SendSuper(cmd) | |||
| 36 | + | } | |||
| 37 | + | ) | |||
| 38 | + | ||||
| 39 | + | class, err := objc.RegisterClass( | |||
| 40 | + | "BarObject", | |||
| 41 | + | objc.GetClass("NSObject"), | |||
| 42 | + | []*objc.Protocol{ | |||
| 43 | + | objc.GetProtocol("NSDelegateWindow"), | |||
| 44 | + | }, | |||
| 45 | + | []objc.FieldDef{ | |||
| 46 | + | { | |||
| 47 | + | Name: "bar", | |||
| 48 | + | Type: reflect.TypeOf(int(0)), | |||
| 49 | + | Attribute: objc.ReadWrite, | |||
| 50 | + | }, | |||
| 51 | + | { | |||
| 52 | + | Name: "foo", | |||
| 53 | + | Type: reflect.TypeOf(false), | |||
| 54 | + | Attribute: objc.ReadWrite, | |||
| 55 | + | }, | |||
| 56 | + | }, | |||
| 57 | + | []objc.MethodDef{ | |||
| 58 | + | { | |||
| 59 | + | Cmd: sel_init, | |||
| 60 | + | Fn: BarInit, | |||
| 61 | + | }, | |||
| 62 | + | }, | |||
| 63 | + | ) | |||
| 64 | + | if err != nil { | |||
| 65 | + | panic(err) | |||
| 30 | } | 66 | } | ||
| 31 | - | var class = objc.AllocateClassPair(objc.GetClass("NSObject"), "BarObject", 0) | |||
| 32 | - | class.AddIvar("bar", int(0), "q") | |||
| 33 | - | class.AddMethod(objc.RegisterName("bar"), objc.NewIMP(func(self *barObject, _cmd objc.SEL) int { | |||
| 34 | - | return self.bar | |||
| 35 | - | }), "q@:") | |||
| 36 | - | class.AddMethod(objc.RegisterName("setBar:"), objc.NewIMP(func(self *barObject, _cmd objc.SEL, bar int) { | |||
| 37 | - | self.bar = bar | |||
| 38 | - | }), "v@:q") | |||
| 39 | - | class.Register() | |||
| 40 | 67 | ||||
| 41 | - | var object = objc.ID(class).Send(objc.RegisterName("new")) | 68 | + | object := objc.ID(class).Send(sel_new) |
| 42 | - | object.Send(objc.RegisterName("setBar:"), 123) | 69 | + | object.Send(sel_setBar, 123) |
| 43 | - | var bar = int(object.Send(objc.RegisterName("bar"))) | 70 | + | bar := int(object.Send(sel_bar)) |
| 44 | fmt.Println(bar) | 71 | fmt.Println(bar) | ||
| 45 | // Output: 123 | 72 | // Output: 123 | ||
| 46 | } | 73 | } | ||
| @@ -85,7 +112,7 @@ func TestSend(t *testing.T) { | |||||
| 85 | // Initialize a NSNumber | 112 | // Initialize a NSNumber | ||
| 86 | NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double) | 113 | NSNumber := objc.ID(objc.GetClass("NSNumber")).Send(objc.RegisterName("numberWithDouble:"), double) | ||
| 87 | // Then get that number back using the generic Send function. | 114 | // Then get that number back using the generic Send function. | ||
| 88 | - | var number = objc.Send[float64](NSNumber, objc.RegisterName("doubleValue")) | 115 | + | number := objc.Send[float64](NSNumber, objc.RegisterName("doubleValue")) |
| 89 | if double != number { | 116 | if double != number { | ||
| 90 | t.Failed() | 117 | t.Failed() | ||
| 91 | } | 118 | } | ||