detached 1a51bed

objc: implement ability to create Protocols at runtime (#320)

Closes #321
commit
1a51bed53afae7417fa8f512a1e96b4cc43e938e
author
TotallyGamerJet · · 2025-06-08 18:18 UTC
committer
GitHub · · 2025-06-08 18:18 UTC
parent
660f0c6
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.9.0-alpha.7
3 files · +363 -25
A examples/protocol-dumper/main_darwin.go +176 -0
@@ -0,0 +1,176 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package main
5+
6+// #cgo CFLAGS: -x objective-c
7+// #cgo LDFLAGS: -framework AppKit
8+//
9+// #import <AppKit/AppKit.h>
10+//
11+// __attribute__((used))
12+// static void __force_protocol_load() {
13+// id o = NULL;
14+// o = @protocol(NSAccessibility);
15+// o = @protocol(NSApplicationDelegate);
16+// }
17+import "C"
18+
19+import (
20+ "fmt"
21+ "io"
22+ "os"
23+ "strconv"
24+ "strings"
25+ "text/template"
26+
27+ "github.com/ebitengine/purego/objc"
28+)
29+
30+var protocols = []string{
31+ "NSAccessibility",
32+ "NSApplicationDelegate",
33+}
34+
35+type ProtocolImpl struct {
36+ Name string
37+
38+ RequiredInstanceMethods []objc.MethodDescription
39+ RequiredClassMethods []objc.MethodDescription
40+ OptionalInstanceMethods []objc.MethodDescription
41+ OptionalClassMethods []objc.MethodDescription
42+
43+ AdoptedProtocols []*objc.Protocol
44+
45+ RequiredInstanceProperties []objc.Property
46+ RequiredClassProperties []objc.Property
47+ OptionalInstanceProperties []objc.Property
48+ OptionalClassProperties []objc.Property
49+}
50+
51+func readProtocols(names []string) (imps []ProtocolImpl, err error) {
52+ for _, name := range names {
53+ p := objc.GetProtocol(name)
54+ if p == nil {
55+ return nil, fmt.Errorf("protocol '%s' does not exist", name)
56+ }
57+ imp := ProtocolImpl{}
58+ imp.Name = name
59+ imp.RequiredInstanceMethods = p.CopyMethodDescriptionList(true, true)
60+ imp.RequiredClassMethods = p.CopyMethodDescriptionList(true, false)
61+ imp.OptionalInstanceMethods = p.CopyMethodDescriptionList(false, true)
62+ imp.OptionalClassMethods = p.CopyMethodDescriptionList(false, false)
63+
64+ imp.AdoptedProtocols = p.CopyProtocolList()
65+
66+ imp.RequiredInstanceProperties = p.CopyPropertyList(true, true)
67+ imp.RequiredClassProperties = p.CopyPropertyList(true, false)
68+ imp.OptionalInstanceProperties = p.CopyPropertyList(false, true)
69+ imp.OptionalClassProperties = p.CopyPropertyList(false, false)
70+ imps = append(imps, imp)
71+ }
72+ return imps, nil
73+}
74+
75+const templ = `// Code generated by protocol-dumper; DO NOT EDIT.
76+
77+package main
78+
79+import (
80+ "log"
81+
82+ "github.com/ebitengine/purego/objc"
83+)
84+
85+func init() {
86+ var p *objc.Protocol
87+ {{- range . }}
88+ {{- $protocolName := .Name }}
89+ if p = objc.AllocateProtocol("{{$protocolName}}"); p != nil {
90+ {{- range .RequiredInstanceMethods }}
91+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, true)
92+ {{- end }}
93+
94+ {{- range .RequiredClassMethods }}
95+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, false)
96+ {{- end }}
97+
98+ {{- range .OptionalInstanceMethods }}
99+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, true)
100+ {{- end }}
101+
102+ {{- range .OptionalClassMethods }}
103+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, false)
104+ {{- end }}
105+ var adoptedProtocol *objc.Protocol
106+ {{- range .AdoptedProtocols }}
107+ adoptedProtocol = objc.GetProtocol("{{ .Name }}")
108+ if adoptedProtocol == nil {
109+ log.Fatalln("protocol '{{ .Name }}' does not exist")
110+ }
111+ p.AddProtocol(adoptedProtocol)
112+ {{- end }}
113+
114+ {{- range .RequiredInstanceProperties }}
115+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, true)
116+ {{- end }}
117+
118+
119+ {{- range .RequiredClassProperties }}
120+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, false)
121+ {{- end }}
122+
123+ {{- range .OptionalInstanceProperties }}
124+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, true)
125+ {{- end }}
126+
127+ {{- range .OptionalClassProperties }}
128+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, false)
129+ {{- end }}
130+ p.Register()
131+ } // Finished protocol: {{$protocolName}}
132+ {{- end }}
133+}
134+`
135+
136+func printProtocols(impls []ProtocolImpl, w io.Writer) error {
137+ tmpl, err := template.New("protocol.tmpl").Funcs(template.FuncMap{
138+ "attributeToStructString": attributeToStructString,
139+ }).Parse(templ)
140+ if err != nil {
141+ return err
142+ }
143+ err = tmpl.Execute(w, impls)
144+ if err != nil {
145+ return err
146+ }
147+ return nil
148+}
149+
150+func attributeToStructString(p objc.Property) string {
151+ attribs := strings.Split(p.Attributes(), ",")
152+ var b strings.Builder
153+ b.WriteString("[]objc.PropertyAttribute{")
154+ for i, attrib := range attribs {
155+ b.WriteString(fmt.Sprintf(`{Name: &[]byte("%s\x00")[0], Value: &[]byte(%s)[0]}`, string(attrib[0]), strconv.Quote(attrib[1:]+"\x00")))
156+ if i != len(attribs)-1 {
157+ b.WriteString(", ")
158+ }
159+ }
160+ b.WriteString("}")
161+ return b.String()
162+}
163+
164+// This program prints out the Go code to generate the OBJC protocols list.
165+// To use it add the @protocol(name) to the C import section and then add it to the protocols list.
166+// You'll want to run it as amd64 and arm64 since the types are slightly different for each architecture.
167+func main() {
168+ var imps []ProtocolImpl
169+ var err error
170+ if imps, err = readProtocols(protocols); err != nil {
171+ panic(err)
172+ }
173+ if err = printProtocols(imps, os.Stdout); err != nil {
174+ panic(err)
175+ }
176+}
@@ -0,0 +1,176 @@
1+// SPDX-License-Identifier: Apache-2.0
2+// SPDX-FileCopyrightText: 2025 The Ebitengine Authors
3+
4+package main
5+
6+// #cgo CFLAGS: -x objective-c
7+// #cgo LDFLAGS: -framework AppKit
8+//
9+// #import <AppKit/AppKit.h>
10+//
11+// __attribute__((used))
12+// static void __force_protocol_load() {
13+// id o = NULL;
14+// o = @protocol(NSAccessibility);
15+// o = @protocol(NSApplicationDelegate);
16+// }
17+import "C"
18+
19+import (
20+ "fmt"
21+ "io"
22+ "os"
23+ "strconv"
24+ "strings"
25+ "text/template"
26+
27+ "github.com/ebitengine/purego/objc"
28+)
29+
30+var protocols = []string{
31+ "NSAccessibility",
32+ "NSApplicationDelegate",
33+}
34+
35+type ProtocolImpl struct {
36+ Name string
37+
38+ RequiredInstanceMethods []objc.MethodDescription
39+ RequiredClassMethods []objc.MethodDescription
40+ OptionalInstanceMethods []objc.MethodDescription
41+ OptionalClassMethods []objc.MethodDescription
42+
43+ AdoptedProtocols []*objc.Protocol
44+
45+ RequiredInstanceProperties []objc.Property
46+ RequiredClassProperties []objc.Property
47+ OptionalInstanceProperties []objc.Property
48+ OptionalClassProperties []objc.Property
49+}
50+
51+func readProtocols(names []string) (imps []ProtocolImpl, err error) {
52+ for _, name := range names {
53+ p := objc.GetProtocol(name)
54+ if p == nil {
55+ return nil, fmt.Errorf("protocol '%s' does not exist", name)
56+ }
57+ imp := ProtocolImpl{}
58+ imp.Name = name
59+ imp.RequiredInstanceMethods = p.CopyMethodDescriptionList(true, true)
60+ imp.RequiredClassMethods = p.CopyMethodDescriptionList(true, false)
61+ imp.OptionalInstanceMethods = p.CopyMethodDescriptionList(false, true)
62+ imp.OptionalClassMethods = p.CopyMethodDescriptionList(false, false)
63+
64+ imp.AdoptedProtocols = p.CopyProtocolList()
65+
66+ imp.RequiredInstanceProperties = p.CopyPropertyList(true, true)
67+ imp.RequiredClassProperties = p.CopyPropertyList(true, false)
68+ imp.OptionalInstanceProperties = p.CopyPropertyList(false, true)
69+ imp.OptionalClassProperties = p.CopyPropertyList(false, false)
70+ imps = append(imps, imp)
71+ }
72+ return imps, nil
73+}
74+
75+const templ = `// Code generated by protocol-dumper; DO NOT EDIT.
76+
77+package main
78+
79+import (
80+ "log"
81+
82+ "github.com/ebitengine/purego/objc"
83+)
84+
85+func init() {
86+ var p *objc.Protocol
87+ {{- range . }}
88+ {{- $protocolName := .Name }}
89+ if p = objc.AllocateProtocol("{{$protocolName}}"); p != nil {
90+ {{- range .RequiredInstanceMethods }}
91+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, true)
92+ {{- end }}
93+
94+ {{- range .RequiredClassMethods }}
95+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, false)
96+ {{- end }}
97+
98+ {{- range .OptionalInstanceMethods }}
99+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, true)
100+ {{- end }}
101+
102+ {{- range .OptionalClassMethods }}
103+ p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, false)
104+ {{- end }}
105+ var adoptedProtocol *objc.Protocol
106+ {{- range .AdoptedProtocols }}
107+ adoptedProtocol = objc.GetProtocol("{{ .Name }}")
108+ if adoptedProtocol == nil {
109+ log.Fatalln("protocol '{{ .Name }}' does not exist")
110+ }
111+ p.AddProtocol(adoptedProtocol)
112+ {{- end }}
113+
114+ {{- range .RequiredInstanceProperties }}
115+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, true)
116+ {{- end }}
117+
118+
119+ {{- range .RequiredClassProperties }}
120+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, false)
121+ {{- end }}
122+
123+ {{- range .OptionalInstanceProperties }}
124+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, true)
125+ {{- end }}
126+
127+ {{- range .OptionalClassProperties }}
128+ p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, false)
129+ {{- end }}
130+ p.Register()
131+ } // Finished protocol: {{$protocolName}}
132+ {{- end }}
133+}
134+`
135+
136+func printProtocols(impls []ProtocolImpl, w io.Writer) error {
137+ tmpl, err := template.New("protocol.tmpl").Funcs(template.FuncMap{
138+ "attributeToStructString": attributeToStructString,
139+ }).Parse(templ)
140+ if err != nil {
141+ return err
142+ }
143+ err = tmpl.Execute(w, impls)
144+ if err != nil {
145+ return err
146+ }
147+ return nil
148+}
149+
150+func attributeToStructString(p objc.Property) string {
151+ attribs := strings.Split(p.Attributes(), ",")
152+ var b strings.Builder
153+ b.WriteString("[]objc.PropertyAttribute{")
154+ for i, attrib := range attribs {
155+ b.WriteString(fmt.Sprintf(`{Name: &[]byte("%s\x00")[0], Value: &[]byte(%s)[0]}`, string(attrib[0]), strconv.Quote(attrib[1:]+"\x00")))
156+ if i != len(attribs)-1 {
157+ b.WriteString(", ")
158+ }
159+ }
160+ b.WriteString("}")
161+ return b.String()
162+}
163+
164+// This program prints out the Go code to generate the OBJC protocols list.
165+// To use it add the @protocol(name) to the C import section and then add it to the protocols list.
166+// You'll want to run it as amd64 and arm64 since the types are slightly different for each architecture.
167+func main() {
168+ var imps []ProtocolImpl
169+ var err error
170+ if imps, err = readProtocols(protocols); err != nil {
171+ panic(err)
172+ }
173+ if err = printProtocols(imps, os.Stdout); err != nil {
174+ panic(err)
175+ }
176+}
M objc/objc_runtime_darwin.go +151 -25
@@ -16,39 +16,52 @@ import (
1616 "unsafe"
1717
1818 "github.com/ebitengine/purego"
19+ "github.com/ebitengine/purego/internal/strings"
1920 )
2021
2122 // TODO: support try/catch?
2223 // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
2324 var (
24- objc_msgSend_fn uintptr
25- objc_msgSend_stret_fn uintptr
26- objc_msgSend func(obj ID, cmd SEL, args ...any) ID
27- objc_msgSendSuper2_fn uintptr
28- objc_msgSendSuper2_stret_fn uintptr
29- objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) 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
25+ objc_msgSend_fn uintptr
26+ objc_msgSend_stret_fn uintptr
27+ objc_msgSend func(obj ID, cmd SEL, args ...any) ID
28+ objc_msgSendSuper2_fn uintptr
29+ objc_msgSendSuper2_stret_fn uintptr
30+ objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID
31+ objc_getClass func(name string) Class
32+ objc_getProtocol func(name string) *Protocol
33+ objc_allocateProtocol func(name string) *Protocol
34+ objc_registerProtocol func(protocol *Protocol)
35+ objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
36+ objc_registerClassPair func(class Class)
37+ sel_registerName func(name string) SEL
38+ class_getSuperclass func(class Class) Class
39+ class_getInstanceVariable func(class Class, name string) Ivar
40+ class_getInstanceSize func(class Class) uintptr
41+ class_addMethod func(class Class, name SEL, imp IMP, types string) bool
42+ class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
43+ class_addProtocol func(class Class, protocol *Protocol) bool
44+ ivar_getOffset func(ivar Ivar) uintptr
45+ ivar_getName func(ivar Ivar) string
46+ object_getClass func(obj ID) Class
47+ object_getIvar func(obj ID, ivar Ivar) ID
48+ object_setIvar func(obj ID, ivar Ivar, value ID)
49+ protocol_getName func(protocol *Protocol) string
50+ protocol_isEqual func(p *Protocol, p2 *Protocol) bool
51+ protocol_addMethodDescription func(p *Protocol, name SEL, types string, isRequiredMethod bool, isInstanceMethod bool)
52+ protocol_copyMethodDescriptionList func(p *Protocol, isRequiredMethod bool, isInstanceMethod bool, outCount *uint32) *MethodDescription
53+ protocol_copyProtocolList func(p *Protocol, outCount *uint32) **Protocol
54+ protocol_copyPropertyList2 func(p *Protocol, outCount *uint32, isRequiredProperty, isInstanceProperty bool) *Property
55+ protocol_addProtocol func(p *Protocol, p2 *Protocol)
56+ protocol_addProperty func(p *Protocol, name string, attributes []PropertyAttribute, attributeCount uint32, isRequiredProperty bool, isInstanceProperty bool)
57+ property_getName func(p Property) string
58+ property_getAttributes func(p Property) string
59+
60+ free func(ptr unsafe.Pointer)
4861 )
4962
5063 func init() {
51- objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)
64+ objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL|purego.RTLD_NOW)
5265 if err != nil {
5366 panic(fmt.Errorf("objc: %w", err))
5467 }
@@ -75,6 +88,8 @@ func init() {
7588 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
7689 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
7790 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
91+ purego.RegisterLibFunc(&objc_allocateProtocol, objc, "objc_allocateProtocol")
92+ purego.RegisterLibFunc(&objc_registerProtocol, objc, "objc_registerProtocol")
7893 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
7994 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
8095 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
@@ -88,8 +103,17 @@ func init() {
88103 purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
89104 purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
90105 purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
106+ purego.RegisterLibFunc(&protocol_addMethodDescription, objc, "protocol_addMethodDescription")
107+ purego.RegisterLibFunc(&protocol_copyMethodDescriptionList, objc, "protocol_copyMethodDescriptionList")
108+ purego.RegisterLibFunc(&protocol_copyProtocolList, objc, "protocol_copyProtocolList")
109+ purego.RegisterLibFunc(&protocol_addProtocol, objc, "protocol_addProtocol")
110+ purego.RegisterLibFunc(&protocol_addProperty, objc, "protocol_addProperty")
111+ purego.RegisterLibFunc(&protocol_copyPropertyList2, objc, "protocol_copyPropertyList2")
112+ purego.RegisterLibFunc(&property_getName, objc, "property_getName")
113+ purego.RegisterLibFunc(&property_getAttributes, objc, "property_getAttributes")
91114 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
92115 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
116+ purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")
93117 }
94118
95119 // ID is an opaque pointer to some Objective-C object
@@ -531,6 +555,39 @@ func (i Ivar) Name() string {
531555 return ivar_getName(i)
532556 }
533557
558+// MethodDescription holds the name and type definition of a method.
559+type MethodDescription struct {
560+ name, types uintptr
561+}
562+
563+// Name returns the name of this method.
564+func (m MethodDescription) Name() string {
565+ return strings.GoString(m.name)
566+}
567+
568+// Types returns the OBJC runtime encoded type description.
569+func (m MethodDescription) Types() string {
570+ return strings.GoString(m.types)
571+}
572+
573+// PropertyAttribute contains the null-terminated Name and Value pair of a Properties internal description.
574+type PropertyAttribute struct {
575+ Name, Value *byte
576+}
577+
578+// Property is an opaque type for Objective-C property metadata.
579+type Property uintptr
580+
581+// Name returns the name of this property.
582+func (p Property) Name() string {
583+ return property_getName(p)
584+}
585+
586+// Attributes returns a comma separated list of PropertyAttribute
587+func (p Property) Attributes() string {
588+ return property_getAttributes(p)
589+}
590+
534591 // Protocol is a type that declares methods that can be implemented by any class.
535592 type Protocol [0]func()
536593
@@ -539,11 +596,69 @@ func GetProtocol(name string) *Protocol {
539596 return objc_getProtocol(name)
540597 }
541598
599+// AllocateProtocol creates a new protocol in the OBJC runtime or nil if the protocol already exists.
600+func AllocateProtocol(name string) *Protocol {
601+ return objc_allocateProtocol(name)
602+}
603+
604+// Register registers the protocol created using AllocateProtocol with the runtime. This must be done
605+// before it is used anywhere and can only be called once.
606+func (p *Protocol) Register() {
607+ objc_registerProtocol(p)
608+}
609+
610+// CopyMethodDescriptionList returns a list of methods that this protocol has given it isRequiredMethod and isInstanceMethod.
611+func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod bool) []MethodDescription {
612+ count := uint32(0)
613+ desc := protocol_copyMethodDescriptionList(p, isRequiredMethod, isInstanceMethod, &count)
614+ methods := clone(unsafe.Slice(desc, count))
615+ free(unsafe.Pointer(desc))
616+ return methods
617+}
618+
619+// CopyProtocolList returns a list of the protocols that this protocol inherits from.
620+func (p *Protocol) CopyProtocolList() []*Protocol {
621+ count := uint32(0)
622+ desc := protocol_copyProtocolList(p, &count)
623+ protocols := clone(unsafe.Slice(desc, count))
624+ free(unsafe.Pointer(desc))
625+ return protocols
626+}
627+
628+// CopyPropertyList returns a list of properties that this protocol has given it isRequiredProperty and isInstanceProperty.
629+func (p *Protocol) CopyPropertyList(isRequiredProperty, isInstanceProperty bool) []Property {
630+ count := uint32(0)
631+ desc := protocol_copyPropertyList2(p, &count, isRequiredProperty, isInstanceProperty)
632+ protocols := clone(unsafe.Slice(desc, count))
633+ free(unsafe.Pointer(desc))
634+ return protocols
635+}
636+
637+// Name returns the name of this protocol.
638+func (p *Protocol) Name() string {
639+ return protocol_getName(p)
640+}
641+
542642 // Equals return true if the two protocols are the same.
543643 func (p *Protocol) Equals(p2 *Protocol) bool {
544644 return protocol_isEqual(p, p2)
545645 }
546646
647+// AddMethodDescription adds a method to a protocol. This can only be called between AllocateProtocol and Protocol.Register.
648+func (p *Protocol) AddMethodDescription(name SEL, types string, isRequiredMethod, isInstanceMethod bool) {
649+ protocol_addMethodDescription(p, name, types, isRequiredMethod, isInstanceMethod)
650+}
651+
652+// AddProtocol marks the protocol as inheriting from another. This can only be called between AllocateProtocol and Protocol.Register.
653+func (p *Protocol) AddProtocol(protocol *Protocol) {
654+ protocol_addProtocol(p, protocol)
655+}
656+
657+// AddProperty adds a property to the protocol. This can only be called between AllocateProtocol and Protocol.Register.
658+func (p *Protocol) AddProperty(name string, attributes []PropertyAttribute, isRequiredProperty, isInstanceProperty bool) {
659+ protocol_addProperty(p, name, attributes, uint32(len(attributes)), isRequiredProperty, isInstanceProperty)
660+}
661+
547662 // IMP is a function pointer that can be called by Objective-C code.
548663 type IMP uintptr
549664
@@ -568,3 +683,14 @@ func NewIMP(fn any) IMP {
568683 }
569684 return IMP(purego.NewCallback(fn))
570685 }
686+
687+// TODO: remove and use slices.Clone when minimum version for purego is 1.21
688+func clone[S ~[]E, E any](s S) S {
689+ // Preserve nilness in case it matters.
690+ if s == nil {
691+ return nil
692+ }
693+ // Avoid s[:0:0] as it leads to unwanted liveness when cloning a
694+ // zero-length slice of a large array; see https://go.dev/issue/68488.
695+ return append(S{}, s...)
696+}
@@ -16,39 +16,52 @@ import (
16 "unsafe"16 "unsafe"
17 17
18 "github.com/ebitengine/purego"18 "github.com/ebitengine/purego"
19+ "github.com/ebitengine/purego/internal/strings"
19 )20 )
20 21
21 // TODO: support try/catch?22 // TODO: support try/catch?
22 // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime23 // https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
23 var (24 var (
24- objc_msgSend_fn uintptr25+ objc_msgSend_fn uintptr
25- objc_msgSend_stret_fn uintptr26+ objc_msgSend_stret_fn uintptr
26- objc_msgSend func(obj ID, cmd SEL, args ...any) ID27+ objc_msgSend func(obj ID, cmd SEL, args ...any) ID
27- objc_msgSendSuper2_fn uintptr28+ objc_msgSendSuper2_fn uintptr
28- objc_msgSendSuper2_stret_fn uintptr29+ objc_msgSendSuper2_stret_fn uintptr
29- objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID30+ objc_msgSendSuper2 func(super *objc_super, cmd SEL, args ...any) ID
30- objc_getClass func(name string) Class31+ objc_getClass func(name string) Class
31- objc_getProtocol func(name string) *Protocol32+ objc_getProtocol func(name string) *Protocol
32- objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class33+ objc_allocateProtocol func(name string) *Protocol
33- objc_registerClassPair func(class Class)34+ objc_registerProtocol func(protocol *Protocol)
34- sel_registerName func(name string) SEL35+ objc_allocateClassPair func(super Class, name string, extraBytes uintptr) Class
35- class_getSuperclass func(class Class) Class36+ objc_registerClassPair func(class Class)
36- class_getInstanceVariable func(class Class, name string) Ivar37+ sel_registerName func(name string) SEL
37- class_getInstanceSize func(class Class) uintptr38+ class_getSuperclass func(class Class) Class
38- class_addMethod func(class Class, name SEL, imp IMP, types string) bool39+ class_getInstanceVariable func(class Class, name string) Ivar
39- class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool40+ class_getInstanceSize func(class Class) uintptr
40- class_addProtocol func(class Class, protocol *Protocol) bool41+ class_addMethod func(class Class, name SEL, imp IMP, types string) bool
41- ivar_getOffset func(ivar Ivar) uintptr42+ class_addIvar func(class Class, name string, size uintptr, alignment uint8, types string) bool
42- ivar_getName func(ivar Ivar) string43+ class_addProtocol func(class Class, protocol *Protocol) bool
43- object_getClass func(obj ID) Class44+ ivar_getOffset func(ivar Ivar) uintptr
44- object_getIvar func(obj ID, ivar Ivar) ID45+ ivar_getName func(ivar Ivar) string
45- object_setIvar func(obj ID, ivar Ivar, value ID)46+ object_getClass func(obj ID) Class
46- protocol_getName func(protocol *Protocol) string47+ object_getIvar func(obj ID, ivar Ivar) ID
47- protocol_isEqual func(p *Protocol, p2 *Protocol) bool48+ object_setIvar func(obj ID, ivar Ivar, value ID)
49+ protocol_getName func(protocol *Protocol) string
50+ protocol_isEqual func(p *Protocol, p2 *Protocol) bool
51+ protocol_addMethodDescription func(p *Protocol, name SEL, types string, isRequiredMethod bool, isInstanceMethod bool)
52+ protocol_copyMethodDescriptionList func(p *Protocol, isRequiredMethod bool, isInstanceMethod bool, outCount *uint32) *MethodDescription
53+ protocol_copyProtocolList func(p *Protocol, outCount *uint32) **Protocol
54+ protocol_copyPropertyList2 func(p *Protocol, outCount *uint32, isRequiredProperty, isInstanceProperty bool) *Property
55+ protocol_addProtocol func(p *Protocol, p2 *Protocol)
56+ protocol_addProperty func(p *Protocol, name string, attributes []PropertyAttribute, attributeCount uint32, isRequiredProperty bool, isInstanceProperty bool)
57+ property_getName func(p Property) string
58+ property_getAttributes func(p Property) string
59+
60+ free func(ptr unsafe.Pointer)
48 )61 )
49 62
50 func init() {63 func init() {
51- objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL)64+ objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL|purego.RTLD_NOW)
52 if err != nil {65 if err != nil {
53 panic(fmt.Errorf("objc: %w", err))66 panic(fmt.Errorf("objc: %w", err))
54 }67 }
@@ -75,6 +88,8 @@ func init() {
75 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")88 purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
76 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")89 purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
77 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")90 purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
91+ purego.RegisterLibFunc(&objc_allocateProtocol, objc, "objc_allocateProtocol")
92+ purego.RegisterLibFunc(&objc_registerProtocol, objc, "objc_registerProtocol")
78 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")93 purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
79 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")94 purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
80 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")95 purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
@@ -88,8 +103,17 @@ func init() {
88 purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")103 purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
89 purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")104 purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
90 purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")105 purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
106+ purego.RegisterLibFunc(&protocol_addMethodDescription, objc, "protocol_addMethodDescription")
107+ purego.RegisterLibFunc(&protocol_copyMethodDescriptionList, objc, "protocol_copyMethodDescriptionList")
108+ purego.RegisterLibFunc(&protocol_copyProtocolList, objc, "protocol_copyProtocolList")
109+ purego.RegisterLibFunc(&protocol_addProtocol, objc, "protocol_addProtocol")
110+ purego.RegisterLibFunc(&protocol_addProperty, objc, "protocol_addProperty")
111+ purego.RegisterLibFunc(&protocol_copyPropertyList2, objc, "protocol_copyPropertyList2")
112+ purego.RegisterLibFunc(&property_getName, objc, "property_getName")
113+ purego.RegisterLibFunc(&property_getAttributes, objc, "property_getAttributes")
91 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")114 purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
92 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")115 purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
116+ purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")
93 }117 }
94 118
95 // ID is an opaque pointer to some Objective-C object119 // ID is an opaque pointer to some Objective-C object
@@ -531,6 +555,39 @@ func (i Ivar) Name() string {
531 return ivar_getName(i)555 return ivar_getName(i)
532 }556 }
533 557
558+// MethodDescription holds the name and type definition of a method.
559+type MethodDescription struct {
560+ name, types uintptr
561+}
562+
563+// Name returns the name of this method.
564+func (m MethodDescription) Name() string {
565+ return strings.GoString(m.name)
566+}
567+
568+// Types returns the OBJC runtime encoded type description.
569+func (m MethodDescription) Types() string {
570+ return strings.GoString(m.types)
571+}
572+
573+// PropertyAttribute contains the null-terminated Name and Value pair of a Properties internal description.
574+type PropertyAttribute struct {
575+ Name, Value *byte
576+}
577+
578+// Property is an opaque type for Objective-C property metadata.
579+type Property uintptr
580+
581+// Name returns the name of this property.
582+func (p Property) Name() string {
583+ return property_getName(p)
584+}
585+
586+// Attributes returns a comma separated list of PropertyAttribute
587+func (p Property) Attributes() string {
588+ return property_getAttributes(p)
589+}
590+
534 // Protocol is a type that declares methods that can be implemented by any class.591 // Protocol is a type that declares methods that can be implemented by any class.
535 type Protocol [0]func()592 type Protocol [0]func()
536 593
@@ -539,11 +596,69 @@ func GetProtocol(name string) *Protocol {
539 return objc_getProtocol(name)596 return objc_getProtocol(name)
540 }597 }
541 598
599+// AllocateProtocol creates a new protocol in the OBJC runtime or nil if the protocol already exists.
600+func AllocateProtocol(name string) *Protocol {
601+ return objc_allocateProtocol(name)
602+}
603+
604+// Register registers the protocol created using AllocateProtocol with the runtime. This must be done
605+// before it is used anywhere and can only be called once.
606+func (p *Protocol) Register() {
607+ objc_registerProtocol(p)
608+}
609+
610+// CopyMethodDescriptionList returns a list of methods that this protocol has given it isRequiredMethod and isInstanceMethod.
611+func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod bool) []MethodDescription {
612+ count := uint32(0)
613+ desc := protocol_copyMethodDescriptionList(p, isRequiredMethod, isInstanceMethod, &count)
614+ methods := clone(unsafe.Slice(desc, count))
615+ free(unsafe.Pointer(desc))
616+ return methods
617+}
618+
619+// CopyProtocolList returns a list of the protocols that this protocol inherits from.
620+func (p *Protocol) CopyProtocolList() []*Protocol {
621+ count := uint32(0)
622+ desc := protocol_copyProtocolList(p, &count)
623+ protocols := clone(unsafe.Slice(desc, count))
624+ free(unsafe.Pointer(desc))
625+ return protocols
626+}
627+
628+// CopyPropertyList returns a list of properties that this protocol has given it isRequiredProperty and isInstanceProperty.
629+func (p *Protocol) CopyPropertyList(isRequiredProperty, isInstanceProperty bool) []Property {
630+ count := uint32(0)
631+ desc := protocol_copyPropertyList2(p, &count, isRequiredProperty, isInstanceProperty)
632+ protocols := clone(unsafe.Slice(desc, count))
633+ free(unsafe.Pointer(desc))
634+ return protocols
635+}
636+
637+// Name returns the name of this protocol.
638+func (p *Protocol) Name() string {
639+ return protocol_getName(p)
640+}
641+
542 // Equals return true if the two protocols are the same.642 // Equals return true if the two protocols are the same.
543 func (p *Protocol) Equals(p2 *Protocol) bool {643 func (p *Protocol) Equals(p2 *Protocol) bool {
544 return protocol_isEqual(p, p2)644 return protocol_isEqual(p, p2)
545 }645 }
546 646
647+// AddMethodDescription adds a method to a protocol. This can only be called between AllocateProtocol and Protocol.Register.
648+func (p *Protocol) AddMethodDescription(name SEL, types string, isRequiredMethod, isInstanceMethod bool) {
649+ protocol_addMethodDescription(p, name, types, isRequiredMethod, isInstanceMethod)
650+}
651+
652+// AddProtocol marks the protocol as inheriting from another. This can only be called between AllocateProtocol and Protocol.Register.
653+func (p *Protocol) AddProtocol(protocol *Protocol) {
654+ protocol_addProtocol(p, protocol)
655+}
656+
657+// AddProperty adds a property to the protocol. This can only be called between AllocateProtocol and Protocol.Register.
658+func (p *Protocol) AddProperty(name string, attributes []PropertyAttribute, isRequiredProperty, isInstanceProperty bool) {
659+ protocol_addProperty(p, name, attributes, uint32(len(attributes)), isRequiredProperty, isInstanceProperty)
660+}
661+
547 // IMP is a function pointer that can be called by Objective-C code.662 // IMP is a function pointer that can be called by Objective-C code.
548 type IMP uintptr663 type IMP uintptr
549 664
@@ -568,3 +683,14 @@ func NewIMP(fn any) IMP {
568 }683 }
569 return IMP(purego.NewCallback(fn))684 return IMP(purego.NewCallback(fn))
570 }685 }
686+
687+// TODO: remove and use slices.Clone when minimum version for purego is 1.21
688+func clone[S ~[]E, E any](s S) S {
689+ // Preserve nilness in case it matters.
690+ if s == nil {
691+ return nil
692+ }
693+ // Avoid s[:0:0] as it leads to unwanted liveness when cloning a
694+ // zero-length slice of a large array; see https://go.dev/issue/68488.
695+ return append(S{}, s...)
696+}
M objc/objc_runtime_darwin_test.go +36 -0
@@ -5,6 +5,7 @@ package objc_test
55
66 import (
77 "fmt"
8+ "log"
89 "reflect"
910 "testing"
1011
@@ -208,3 +209,38 @@ func ExampleSendSuper() {
208209 fmt.Println(res)
209210 // Output: 16
210211 }
212+
213+func ExampleAllocateProtocol() {
214+ var p *objc.Protocol
215+ if p = objc.AllocateProtocol("MyCustomProtocol"); p != nil {
216+ p.AddMethodDescription(objc.RegisterName("isFoo"), "B16@0:8", true, true)
217+ var adoptedProtocol *objc.Protocol
218+ adoptedProtocol = objc.GetProtocol("NSObject")
219+ if adoptedProtocol == nil {
220+ log.Fatalln("protocol 'NSObject' does not exist")
221+ }
222+ p.AddProtocol(adoptedProtocol)
223+ p.AddProperty("accessibilityElement", []objc.PropertyAttribute{
224+ {Name: &[]byte("T\x00")[0], Value: &[]byte("B\x00")[0]},
225+ {Name: &[]byte("G\x00")[0], Value: &[]byte("isBar\x00")[0]},
226+ }, true, true)
227+ p.Register()
228+ }
229+
230+ p = objc.GetProtocol("MyCustomProtocol")
231+
232+ for _, protocol := range p.CopyProtocolList() {
233+ fmt.Println(protocol.Name())
234+ }
235+ for _, property := range p.CopyPropertyList(true, true) {
236+ fmt.Println(property.Name(), property.Attributes())
237+ }
238+ for _, method := range p.CopyMethodDescriptionList(true, true) {
239+ fmt.Println(method.Name(), method.Types())
240+ }
241+
242+ // Output:
243+ // NSObject
244+ // accessibilityElement TB,GisBar
245+ // isFoo B16@0:8
246+}
@@ -5,6 +5,7 @@ package objc_test
5 5
6 import (6 import (
7 "fmt"7 "fmt"
8+ "log"
8 "reflect"9 "reflect"
9 "testing"10 "testing"
10 11
@@ -208,3 +209,38 @@ func ExampleSendSuper() {
208 fmt.Println(res)209 fmt.Println(res)
209 // Output: 16210 // Output: 16
210 }211 }
212+
213+func ExampleAllocateProtocol() {
214+ var p *objc.Protocol
215+ if p = objc.AllocateProtocol("MyCustomProtocol"); p != nil {
216+ p.AddMethodDescription(objc.RegisterName("isFoo"), "B16@0:8", true, true)
217+ var adoptedProtocol *objc.Protocol
218+ adoptedProtocol = objc.GetProtocol("NSObject")
219+ if adoptedProtocol == nil {
220+ log.Fatalln("protocol 'NSObject' does not exist")
221+ }
222+ p.AddProtocol(adoptedProtocol)
223+ p.AddProperty("accessibilityElement", []objc.PropertyAttribute{
224+ {Name: &[]byte("T\x00")[0], Value: &[]byte("B\x00")[0]},
225+ {Name: &[]byte("G\x00")[0], Value: &[]byte("isBar\x00")[0]},
226+ }, true, true)
227+ p.Register()
228+ }
229+
230+ p = objc.GetProtocol("MyCustomProtocol")
231+
232+ for _, protocol := range p.CopyProtocolList() {
233+ fmt.Println(protocol.Name())
234+ }
235+ for _, property := range p.CopyPropertyList(true, true) {
236+ fmt.Println(property.Name(), property.Attributes())
237+ }
238+ for _, method := range p.CopyMethodDescriptionList(true, true) {
239+ fmt.Println(method.Name(), method.Types())
240+ }
241+
242+ // Output:
243+ // NSObject
244+ // accessibilityElement TB,GisBar
245+ // isFoo B16@0:8
246+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 102

↑↓ moveenter openesc close