Skip to content

objc: implement ability to create Protocols at runtime #320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Jun 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions examples/protocol-dumper/main_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2025 The Ebitengine Authors

package main

// #cgo CFLAGS: -x objective-c
// #cgo LDFLAGS: -framework AppKit
//
// #import <AppKit/AppKit.h>
//
// __attribute__((used))
// static void __force_protocol_load() {
// id o = NULL;
// o = @protocol(NSAccessibility);
// o = @protocol(NSApplicationDelegate);
// }
import "C"

import (
"fmt"
"io"
"os"
"strconv"
"strings"
"text/template"

"github.com/ebitengine/purego/objc"
)

var protocols = []string{
"NSAccessibility",
"NSApplicationDelegate",
}

type ProtocolImpl struct {
Name string

RequiredInstanceMethods []objc.MethodDescription
RequiredClassMethods []objc.MethodDescription
OptionalInstanceMethods []objc.MethodDescription
OptionalClassMethods []objc.MethodDescription

AdoptedProtocols []*objc.Protocol

RequiredInstanceProperties []objc.Property
RequiredClassProperties []objc.Property
OptionalInstanceProperties []objc.Property
OptionalClassProperties []objc.Property
}

func readProtocols(names []string) (imps []ProtocolImpl, err error) {
for _, name := range names {
p := objc.GetProtocol(name)
if p == nil {
return nil, fmt.Errorf("protocol '%s' does not exist", name)
}
imp := ProtocolImpl{}
imp.Name = name
imp.RequiredInstanceMethods = p.CopyMethodDescriptionList(true, true)
imp.RequiredClassMethods = p.CopyMethodDescriptionList(true, false)
imp.OptionalInstanceMethods = p.CopyMethodDescriptionList(false, true)
imp.OptionalClassMethods = p.CopyMethodDescriptionList(false, false)

imp.AdoptedProtocols = p.CopyProtocolList()

imp.RequiredInstanceProperties = p.CopyPropertyList(true, true)
imp.RequiredClassProperties = p.CopyPropertyList(true, false)
imp.OptionalInstanceProperties = p.CopyPropertyList(false, true)
imp.OptionalClassProperties = p.CopyPropertyList(false, false)
imps = append(imps, imp)
}
return imps, nil
}

const templ = `// Code generated by protocol-dumper; DO NOT EDIT.

package main

import (
"log"

"github.com/ebitengine/purego/objc"
)

func init() {
var p *objc.Protocol
{{- range . }}
{{- $protocolName := .Name }}
if p = objc.AllocateProtocol("{{$protocolName}}"); p != nil {
{{- range .RequiredInstanceMethods }}
p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, true)
{{- end }}

{{- range .RequiredClassMethods }}
p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", true, false)
{{- end }}

{{- range .OptionalInstanceMethods }}
p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, true)
{{- end }}

{{- range .OptionalClassMethods }}
p.AddMethodDescription(objc.RegisterName("{{ .Name }}"), "{{ .Types }}", false, false)
{{- end }}
var adoptedProtocol *objc.Protocol
{{- range .AdoptedProtocols }}
adoptedProtocol = objc.GetProtocol("{{ .Name }}")
if adoptedProtocol == nil {
log.Fatalln("protocol '{{ .Name }}' does not exist")
}
p.AddProtocol(adoptedProtocol)
{{- end }}

{{- range .RequiredInstanceProperties }}
p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, true)
{{- end }}


{{- range .RequiredClassProperties }}
p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, true, false)
{{- end }}

{{- range .OptionalInstanceProperties }}
p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, true)
{{- end }}

{{- range .OptionalClassProperties }}
p.AddProperty("{{ .Name }}", {{ attributeToStructString . }}, false, false)
{{- end }}
p.Register()
} // Finished protocol: {{$protocolName}}
{{- end }}
}
`

func printProtocols(impls []ProtocolImpl, w io.Writer) error {
tmpl, err := template.New("protocol.tmpl").Funcs(template.FuncMap{
"attributeToStructString": attributeToStructString,
}).Parse(templ)
if err != nil {
return err
}
err = tmpl.Execute(w, impls)
if err != nil {
return err
}
return nil
}

func attributeToStructString(p objc.Property) string {
attribs := strings.Split(p.Attributes(), ",")
var b strings.Builder
b.WriteString("[]objc.PropertyAttribute{")
for i, attrib := range attribs {
b.WriteString(fmt.Sprintf(`{Name: &[]byte("%s\x00")[0], Value: &[]byte(%s)[0]}`, string(attrib[0]), strconv.Quote(attrib[1:]+"\x00")))
if i != len(attribs)-1 {
b.WriteString(", ")
}
}
b.WriteString("}")
return b.String()
}

// This program prints out the Go code to generate the OBJC protocols list.
// To use it add the @protocol(name) to the C import section and then add it to the protocols list.
// You'll want to run it as amd64 and arm64 since the types are slightly different for each architecture.
func main() {
var imps []ProtocolImpl
var err error
if imps, err = readProtocols(protocols); err != nil {
panic(err)
}
if err = printProtocols(imps, os.Stdout); err != nil {
panic(err)
}
}
Loading