package main import ( "bytes" "encoding/xml" "flag" "fmt" "go/format" "os" "sort" "strings" "unicode" ) type protocol struct { Interfaces []iface `xml:"interface"` } type iface struct { Name string `xml:"name,attr"` Version int32 `xml:"version,attr"` Description description `xml:"description"` Requests []message `xml:"request"` Events []message `xml:"event"` Enums []enum `xml:"enum"` } type description struct { Summary string `xml:"summary,attr"` } type message struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` Since int `xml:"since,attr"` Description description `xml:"description"` Args []arg `xml:"arg"` } type arg struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` Interface string `xml:"interface,attr"` Nullable bool `xml:"allow-null,attr"` } type enum struct { Name string `xml:"name,attr"` Bitfield bool `xml:"bitfield,attr"` Description description `xml:"description"` Entries []entry `xml:"entry"` } type entry struct { Name string `xml:"name,attr"` Value string `xml:"value,attr"` } type flags []string func (f *flags) String() string { return strings.Join(*f, ",") } func (f *flags) Set(s string) error { *f = append(*f, s); return nil } func parse(path string) (protocol, error) { var p protocol b, e := os.ReadFile(path) if e == nil { e = xml.Unmarshal(b, &p) } return p, e } func camel(s string) string { var b strings.Builder for _, part := range strings.Split(s, "_") { if part == "" { continue } r := []rune(part) b.WriteRune(unicode.ToUpper(r[0])) b.WriteString(string(r[1:])) } return b.String() } func name(s string) string { for _, prefix := range []string{"wl_", "xdg_"} { if strings.HasPrefix(s, prefix) { s = strings.TrimPrefix(s, prefix) break } } return camel(s) } func signature(m message) string { var b strings.Builder if m.Since > 1 { fmt.Fprint(&b, m.Since) } for _, a := range m.Args { if a.Type == "new_id" && a.Interface == "" { b.WriteString("sun") continue } if a.Nullable { b.WriteByte('?') } b.WriteString(map[string]string{"int": "i", "uint": "u", "fixed": "f", "string": "s", "array": "a", "object": "o", "new_id": "n", "fd": "h"}[a.Type]) } return b.String() } type gen struct { bytes.Buffer owners map[string]string imports map[string]string } func (g *gen) p(f string, a ...any) { fmt.Fprintf(&g.Buffer, f, a...) } func (g *gen) ref(s string) (string, error) { if s == "" { return "nil", nil } alias, ok := g.owners[s] if !ok { return "", fmt.Errorf("unknown interface %s", s) } if alias != "" { alias += "." } return alias + name(s) + "Interface", nil } func (g *gen) wrapper(s string) string { alias := g.owners[s] if alias != "" { alias += "." } return alias + name(s) } func (g *gen) table(m message) error { var ts []string for _, a := range m.Args { if a.Type == "new_id" && a.Interface == "" { ts = append(ts, "nil", "nil", "nil") continue } t := "nil" if a.Type == "object" || a.Type == "new_id" { var e error t, e = g.ref(a.Interface) if e != nil { return e } } ts = append(ts, t) } g.p("{Name:%q,Signature:%q,Types:[]*server.Interface{%s}},\n", m.Name, signature(m), strings.Join(ts, ",")) return nil } func (g *gen) typ(a arg, event bool) string { switch a.Type { case "uint": return "uint32" case "int": return "int32" case "fixed": return "server.Fixed" case "string": return "string" case "array": return "[]byte" case "fd": return "int" case "new_id": if !event { return "uint32" } fallthrough case "object": if a.Interface == "" { return "*server.Resource" } return "*" + g.wrapper(a.Interface) } return "server.Arg" } func param(a arg) string { s := a.Name if parts := strings.Split(s, "_"); len(parts) > 1 { s = parts[0] for _, part := range parts[1:] { s += camel(part) } } switch s { case "interface", "type", "map", "range", "func", "var", "package", "select", "default", "go", "defer", "return", "chan", "import": return s + "Arg" } return s } func (g *gen) params(m message, event bool) { for _, a := range m.Args { if a.Type == "new_id" && a.Interface == "" && !event { g.p(", interfaceName string, version uint32, id uint32") continue } g.p(", %s %s", param(a), g.typ(a, event)) } } func (g *gen) generate(pr protocol, pkg string) ([]byte, error) { g.p("// Code generated by wlgen; DO NOT EDIT.\npackage %s\nimport (\"github.com/bnema/purego-libwayland/server\"\n", pkg) pin := false for _, it := range pr.Interfaces { for _, m := range it.Events { for _, a := range m.Args { if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") { pin = true } } } } if pin { g.p("\"runtime\"\n") } for _, it := range pr.Interfaces { if len(it.Requests) > 0 { g.p("\"fmt\"\n") break } } aliases := make([]string, 0, len(g.imports)) for alias := range g.imports { aliases = append(aliases, alias) } sort.Strings(aliases) for _, alias := range aliases { g.p("%s %q\n", alias, g.imports[alias]) } g.p(")\n") for _, it := range pr.Interfaces { g.p("var %sInterface *server.Interface\n", name(it.Name)) } g.p("func init(){\n") for _, it := range pr.Interfaces { g.p("%sInterface=&server.Interface{Name:%q,Version:%d}\n", name(it.Name), it.Name, it.Version) } for _, it := range pr.Interfaces { n := name(it.Name) g.p("%sInterface.Requests=[]server.Message{\n", n) for _, m := range it.Requests { if e := g.table(m); e != nil { return nil, e } } g.p("}\n%sInterface.Events=[]server.Message{\n", n) for _, m := range it.Events { if e := g.table(m); e != nil { return nil, e } } g.p("}\n") } g.p("if err:=server.NewInterfaces(") for _, it := range pr.Interfaces { g.p("%sInterface,", name(it.Name)) } g.p(");err!=nil{panic(err)}}\n") for _, it := range pr.Interfaces { n := name(it.Name) g.p("// %s %s.\ntype %s struct{*server.Resource}\nfunc Wrap%s(r *server.Resource)*%s{if r==nil{return nil};return &%s{r}}\n", n, it.Description.Summary, n, n, n, n) for j, m := range it.Requests { g.p("const %sRequest%s uint32=%d\n", n, camel(m.Name), j) } for j, m := range it.Events { g.p("const %sEvent%s uint32=%d\n", n, camel(m.Name), j) } for _, en := range it.Enums { t := n + camel(en.Name) g.p("// %s %s", t, en.Description.Summary) if en.Bitfield { g.p(" (bitfield)") } g.p(".\ntype %s uint32\nconst(\n", t) for _, v := range en.Entries { g.p("%s%s %s=%s\n", t, camel(v.Name), t, v.Value) } g.p(")\n") } g.p("// %sHandler handles %s requests.\ntype %sHandler interface{\n", n, it.Name, n) for _, m := range it.Requests { g.p("%s(self *%s", camel(m.Name), n) g.params(m, false) g.p(")\n") } g.p("}\nfunc New%s(c server.Client,version int32,id uint32,h %sHandler)(*%s,error){", n, n, n) if len(it.Requests) > 0 { g.p("if h==nil{return nil,fmt.Errorf(%q)}\n", "purego-libwayland: nil handler for "+it.Name) } g.p("var self *%s\nr,err:=c.CreateResource(%sInterface,version,id,func(_ *server.Resource,opcode uint32,args []server.Arg){switch opcode{\n", n, n) for j, m := range it.Requests { g.p("case %d:h.%s(self", j, camel(m.Name)) idx := 0 for _, a := range m.Args { if a.Type == "new_id" && a.Interface == "" { g.p(",args[%d].String(),args[%d].Uint(),args[%d].NewID()", idx, idx+1, idx+2) idx += 3 continue } x := fmt.Sprintf("args[%d]", idx) idx++ switch a.Type { case "uint", "int", "fixed", "string", "array", "fd", "new_id": accessor := camel(a.Type) if a.Type == "new_id" { accessor = "NewID" } g.p(",%s.%s()", x, accessor) case "object": if a.Interface == "" { g.p(",%s.Resource()", x) } else { g.p(",%sWrap%s(%s.Resource())", func() string { if v := g.owners[a.Interface]; v != "" { return v + "." } return "" }(), name(a.Interface), x) } } } g.p(")") if m.Type == "destructor" { g.p(";self.Destroy()") } g.p("\n") } g.p("}});if err!=nil{return nil,err};self=Wrap%s(r);r.Data=self;return self,nil}\n", n) for _, m := range it.Events { g.p("// Send%s %s", camel(m.Name), m.Description.Summary) if m.Since > 1 { g.p(" (since version %d)", m.Since) } g.p(".\nfunc(self *%s)Send%s(", n, camel(m.Name)) for i, a := range m.Args { if i > 0 { g.p(",") } g.p("%s %s", param(a), g.typ(a, true)) } g.p("){\n") needsPin := false for _, a := range m.Args { if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") { needsPin = true } } if needsPin { g.p("var p runtime.Pinner;defer p.Unpin()\n") } g.p("self.PostEvent(%sEvent%s", n, camel(m.Name)) for _, a := range m.Args { v := param(a) if a.Type == "new_id" && a.Interface == "" { g.p(",server.String(&p,%s.Iface().Name,false),server.Uint(uint32(%s.Version())),server.NewID(%s)", v, v, v) continue } switch a.Type { case "uint", "int", "fd": g.p(",server.%s(%s)", map[string]string{"uint": "Uint", "int": "Int", "fd": "Fd"}[a.Type], v) case "fixed": g.p(",server.FixedArg(%s)", v) case "string": g.p(",server.String(&p,%s,%t)", v, a.Nullable) case "array": g.p(",server.Array(&p,%s)", v) case "object", "new_id": constructor := "Object" if a.Type == "new_id" { constructor = "NewID" } if a.Interface == "" { g.p(",server.%s(%s)", constructor, v) } else { g.p(",server.%s(resourceOf(%s))", constructor, v) } } } g.p(")\n}\n") } g.p("func New%sGlobal(d *server.Display,version int32,bind server.BindFunc)error{return d.CreateGlobal(%sInterface,version,bind)}\n", n, n) } g.p("func resourceOf(v interface{GetResource()*server.Resource})*server.Resource{if v==nil{return nil};return v.GetResource()}\n") for _, it := range pr.Interfaces { n := name(it.Name) g.p("func(self *%s)GetResource()*server.Resource{if self==nil{return nil};return self.Resource}\n", n) } return format.Source(g.Bytes()) } func main() { if e := run(); e != nil { fmt.Fprintln(os.Stderr, e) os.Exit(1) } } func run() error { var im, ix flags flag.Var(&im, "import", "alias=Go package path") flag.Var(&ix, "import-xml", "alias=protocol XML") pkg := flag.String("package", "", "Go package") out := flag.String("out", "", "output file") flag.Parse() if *pkg == "" || *out == "" || flag.NArg() != 1 { return fmt.Errorf("usage: wlgen -package name -out file [-import alias=path -import-xml alias=xml] protocol.xml") } g := &gen{owners: map[string]string{}, imports: map[string]string{}} for _, s := range im { a, v, ok := strings.Cut(s, "=") if !ok { return fmt.Errorf("invalid import %q", s) } g.imports[a] = v } for _, s := range ix { a, v, ok := strings.Cut(s, "=") if !ok || g.imports[a] == "" { return fmt.Errorf("invalid import XML %q", s) } p, e := parse(v) if e != nil { return e } for _, it := range p.Interfaces { g.owners[it.Name] = a } } p, e := parse(flag.Arg(0)) if e != nil { return e } for _, it := range p.Interfaces { g.owners[it.Name] = "" } b, e := g.generate(p, *pkg) if e != nil { return e } return os.WriteFile(*out, b, 0644) }