detached 7ad88b7

feat(gen): port server generator from purego-libwayland wlgen

commit
7ad88b7bbf04b9ae7fc43e12e958921cf8318f58
author
brice · · 2026-10-01 03:26 UTC
parent
4a364ab
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +558 -0
A internal/gen/server/equivalence_test.go +115 -0
@@ -0,0 +1,115 @@
1+package server
2+
3+import (
4+ "bufio"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bnema/go-wayland-bindings/spec"
11+)
12+
13+// TestEquivalence regenerates every protocol listed in the //go:generate lines
14+// of the baseline purego-libwayland protocol/gen.go and requires byte-identical
15+// output. It runs only when WGB_OLD_PUREGO points at a baseline checkout.
16+func TestEquivalence(t *testing.T) {
17+ root := os.Getenv("WGB_OLD_PUREGO")
18+ if root == "" {
19+ t.Skip("WGB_OLD_PUREGO not set")
20+ }
21+ protoDir := filepath.Join(root, "protocol")
22+ f, err := os.Open(filepath.Join(protoDir, "gen.go"))
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ defer f.Close()
27+
28+ const prefix = "//go:generate go run ../cmd/wlgen "
29+ n := 0
30+ sc := bufio.NewScanner(f)
31+ sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
32+ for sc.Scan() {
33+ line := sc.Text()
34+ if !strings.HasPrefix(line, prefix) {
35+ continue
36+ }
37+ args := strings.Fields(strings.TrimPrefix(line, prefix))
38+ n++
39+ t.Run(line[len(prefix):], func(t *testing.T) { checkLine(t, protoDir, args) })
40+ }
41+ if err := sc.Err(); err != nil {
42+ t.Fatal(err)
43+ }
44+ if n == 0 {
45+ t.Fatal("no //go:generate lines found")
46+ }
47+}
48+
49+func loadXML(t *testing.T, path string) *spec.Protocol {
50+ t.Helper()
51+ b, err := os.ReadFile(path)
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ p, err := spec.Parse(b)
56+ if err != nil {
57+ t.Fatalf("%s: %v", path, err)
58+ }
59+ return p
60+}
61+
62+func checkLine(t *testing.T, protoDir string, args []string) {
63+ opts := Options{Imports: map[string]string{}, Owners: map[string]string{}, Tool: "wlgen"}
64+ var out, xmlPath string
65+ var importXML []string
66+ for i := 0; i < len(args); i++ {
67+ switch args[i] {
68+ case "-package", "-out", "-import", "-import-xml":
69+ if i+1 >= len(args) {
70+ t.Fatalf("flag %s missing value", args[i])
71+ }
72+ v := args[i+1]
73+ i++
74+ switch args[i-1] {
75+ case "-package":
76+ opts.Package = v
77+ case "-out":
78+ out = v
79+ case "-import":
80+ a, path, ok := strings.Cut(v, "=")
81+ if !ok {
82+ t.Fatalf("invalid -import %q", v)
83+ }
84+ opts.Imports[a] = path
85+ case "-import-xml":
86+ importXML = append(importXML, v)
87+ }
88+ default:
89+ xmlPath = args[i]
90+ }
91+ }
92+ if opts.Package == "" || out == "" || xmlPath == "" {
93+ t.Fatalf("incomplete generate line: %v", args)
94+ }
95+ for _, s := range importXML {
96+ a, path, ok := strings.Cut(s, "=")
97+ if !ok || opts.Imports[a] == "" {
98+ t.Fatalf("invalid -import-xml %q", s)
99+ }
100+ for _, it := range loadXML(t, filepath.Join(protoDir, path)).Interfaces {
101+ opts.Owners[it.Name] = a
102+ }
103+ }
104+ got, err := Generate(loadXML(t, filepath.Join(protoDir, xmlPath)), opts)
105+ if err != nil {
106+ t.Fatal(err)
107+ }
108+ want, err := os.ReadFile(filepath.Join(protoDir, out))
109+ if err != nil {
110+ t.Fatal(err)
111+ }
112+ if string(got) != string(want) {
113+ t.Errorf("generated output differs from %s (got %d bytes, want %d)", out, len(got), len(want))
114+ }
115+}
@@ -0,0 +1,115 @@
1+package server
2+
3+import (
4+ "bufio"
5+ "os"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+
10+ "github.com/bnema/go-wayland-bindings/spec"
11+)
12+
13+// TestEquivalence regenerates every protocol listed in the //go:generate lines
14+// of the baseline purego-libwayland protocol/gen.go and requires byte-identical
15+// output. It runs only when WGB_OLD_PUREGO points at a baseline checkout.
16+func TestEquivalence(t *testing.T) {
17+ root := os.Getenv("WGB_OLD_PUREGO")
18+ if root == "" {
19+ t.Skip("WGB_OLD_PUREGO not set")
20+ }
21+ protoDir := filepath.Join(root, "protocol")
22+ f, err := os.Open(filepath.Join(protoDir, "gen.go"))
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ defer f.Close()
27+
28+ const prefix = "//go:generate go run ../cmd/wlgen "
29+ n := 0
30+ sc := bufio.NewScanner(f)
31+ sc.Buffer(make([]byte, 0, 64*1024), 1024*1024)
32+ for sc.Scan() {
33+ line := sc.Text()
34+ if !strings.HasPrefix(line, prefix) {
35+ continue
36+ }
37+ args := strings.Fields(strings.TrimPrefix(line, prefix))
38+ n++
39+ t.Run(line[len(prefix):], func(t *testing.T) { checkLine(t, protoDir, args) })
40+ }
41+ if err := sc.Err(); err != nil {
42+ t.Fatal(err)
43+ }
44+ if n == 0 {
45+ t.Fatal("no //go:generate lines found")
46+ }
47+}
48+
49+func loadXML(t *testing.T, path string) *spec.Protocol {
50+ t.Helper()
51+ b, err := os.ReadFile(path)
52+ if err != nil {
53+ t.Fatal(err)
54+ }
55+ p, err := spec.Parse(b)
56+ if err != nil {
57+ t.Fatalf("%s: %v", path, err)
58+ }
59+ return p
60+}
61+
62+func checkLine(t *testing.T, protoDir string, args []string) {
63+ opts := Options{Imports: map[string]string{}, Owners: map[string]string{}, Tool: "wlgen"}
64+ var out, xmlPath string
65+ var importXML []string
66+ for i := 0; i < len(args); i++ {
67+ switch args[i] {
68+ case "-package", "-out", "-import", "-import-xml":
69+ if i+1 >= len(args) {
70+ t.Fatalf("flag %s missing value", args[i])
71+ }
72+ v := args[i+1]
73+ i++
74+ switch args[i-1] {
75+ case "-package":
76+ opts.Package = v
77+ case "-out":
78+ out = v
79+ case "-import":
80+ a, path, ok := strings.Cut(v, "=")
81+ if !ok {
82+ t.Fatalf("invalid -import %q", v)
83+ }
84+ opts.Imports[a] = path
85+ case "-import-xml":
86+ importXML = append(importXML, v)
87+ }
88+ default:
89+ xmlPath = args[i]
90+ }
91+ }
92+ if opts.Package == "" || out == "" || xmlPath == "" {
93+ t.Fatalf("incomplete generate line: %v", args)
94+ }
95+ for _, s := range importXML {
96+ a, path, ok := strings.Cut(s, "=")
97+ if !ok || opts.Imports[a] == "" {
98+ t.Fatalf("invalid -import-xml %q", s)
99+ }
100+ for _, it := range loadXML(t, filepath.Join(protoDir, path)).Interfaces {
101+ opts.Owners[it.Name] = a
102+ }
103+ }
104+ got, err := Generate(loadXML(t, filepath.Join(protoDir, xmlPath)), opts)
105+ if err != nil {
106+ t.Fatal(err)
107+ }
108+ want, err := os.ReadFile(filepath.Join(protoDir, out))
109+ if err != nil {
110+ t.Fatal(err)
111+ }
112+ if string(got) != string(want) {
113+ t.Errorf("generated output differs from %s (got %d bytes, want %d)", out, len(got), len(want))
114+ }
115+}
A internal/gen/server/server.go +386 -0
@@ -0,0 +1,386 @@
1+// Package server generates Go server-side bindings (for
2+// github.com/bnema/purego-libwayland/server) from Wayland protocol XML.
3+package server
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "go/format"
9+ "sort"
10+ "strings"
11+ "unicode"
12+
13+ "github.com/bnema/go-wayland-bindings/spec"
14+)
15+
16+// Options configures Generate.
17+type Options struct {
18+ // Package is the Go package name of the generated file.
19+ Package string
20+ // Imports maps an import alias to a Go import path.
21+ Imports map[string]string
22+ // Owners maps an interface name to the import alias of the package that
23+ // defines it; "" means the generated package itself. Interfaces defined
24+ // by the protocol being generated are always local.
25+ Owners map[string]string
26+ // Tool is the tool name in the generated-code header (default "wlbgen").
27+ Tool string
28+}
29+
30+// Generate returns the gofmt-formatted server bindings for p.
31+func Generate(p *spec.Protocol, opts Options) ([]byte, error) {
32+ if p == nil {
33+ return nil, fmt.Errorf("server: nil protocol")
34+ }
35+ if opts.Package == "" {
36+ return nil, fmt.Errorf("server: empty package name")
37+ }
38+ tool := opts.Tool
39+ if tool == "" {
40+ tool = "wlbgen"
41+ }
42+ g := &gen{owners: map[string]string{}, imports: map[string]string{}}
43+ for k, v := range opts.Imports {
44+ g.imports[k] = v
45+ }
46+ for k, v := range opts.Owners {
47+ g.owners[k] = v
48+ }
49+ for _, it := range p.Interfaces {
50+ g.owners[it.Name] = ""
51+ }
52+ return g.generate(p, opts.Package, tool)
53+}
54+
55+func summary(d *spec.Description) string {
56+ if d == nil {
57+ return ""
58+ }
59+ return d.Summary
60+}
61+
62+func camel(s string) string {
63+ var b strings.Builder
64+ for _, part := range strings.Split(s, "_") {
65+ if part == "" {
66+ continue
67+ }
68+ r := []rune(part)
69+ b.WriteRune(unicode.ToUpper(r[0]))
70+ b.WriteString(string(r[1:]))
71+ }
72+ return b.String()
73+}
74+func name(s string) string {
75+ for _, prefix := range []string{"wl_", "xdg_"} {
76+ if strings.HasPrefix(s, prefix) {
77+ s = strings.TrimPrefix(s, prefix)
78+ break
79+ }
80+ }
81+ return camel(s)
82+}
83+func signature(m spec.Message) string {
84+ var b strings.Builder
85+ if m.Since > 1 {
86+ fmt.Fprint(&b, m.Since)
87+ }
88+ for _, a := range m.Args {
89+ if a.Type == "new_id" && a.Interface == "" {
90+ b.WriteString("sun")
91+ continue
92+ }
93+ if a.AllowNull {
94+ b.WriteByte('?')
95+ }
96+ b.WriteString(map[string]string{"int": "i", "uint": "u", "fixed": "f", "string": "s", "array": "a", "object": "o", "new_id": "n", "fd": "h"}[a.Type])
97+ }
98+ return b.String()
99+}
100+
101+type gen struct {
102+ bytes.Buffer
103+ owners map[string]string
104+ imports map[string]string
105+}
106+
107+func (g *gen) p(f string, a ...any) { fmt.Fprintf(&g.Buffer, f, a...) }
108+func (g *gen) ref(s string) (string, error) {
109+ if s == "" {
110+ return "nil", nil
111+ }
112+ alias, ok := g.owners[s]
113+ if !ok {
114+ return "", fmt.Errorf("unknown interface %s", s)
115+ }
116+ if alias != "" {
117+ alias += "."
118+ }
119+ return alias + name(s) + "Interface", nil
120+}
121+func (g *gen) wrapper(s string) string {
122+ alias := g.owners[s]
123+ if alias != "" {
124+ alias += "."
125+ }
126+ return alias + name(s)
127+}
128+func (g *gen) table(m spec.Message) error {
129+ var ts []string
130+ for _, a := range m.Args {
131+ if a.Type == "new_id" && a.Interface == "" {
132+ ts = append(ts, "nil", "nil", "nil")
133+ continue
134+ }
135+ t := "nil"
136+ if a.Type == "object" || a.Type == "new_id" {
137+ var e error
138+ t, e = g.ref(a.Interface)
139+ if e != nil {
140+ return e
141+ }
142+ }
143+ ts = append(ts, t)
144+ }
145+ g.p("{Name:%q,Signature:%q,Types:[]*server.Interface{%s}},\n", m.Name, signature(m), strings.Join(ts, ","))
146+ return nil
147+}
148+func (g *gen) typ(a spec.Arg, event bool) string {
149+ switch a.Type {
150+ case "uint":
151+ return "uint32"
152+ case "int":
153+ return "int32"
154+ case "fixed":
155+ return "server.Fixed"
156+ case "string":
157+ return "string"
158+ case "array":
159+ return "[]byte"
160+ case "fd":
161+ return "int"
162+ case "new_id":
163+ if !event {
164+ return "uint32"
165+ }
166+ fallthrough
167+ case "object":
168+ if a.Interface == "" {
169+ return "*server.Resource"
170+ }
171+ return "*" + g.wrapper(a.Interface)
172+ }
173+ return "server.Arg"
174+}
175+func param(a spec.Arg) string {
176+ s := a.Name
177+ if parts := strings.Split(s, "_"); len(parts) > 1 {
178+ s = parts[0]
179+ for _, part := range parts[1:] {
180+ s += camel(part)
181+ }
182+ }
183+ switch s {
184+ case "interface", "type", "map", "range", "func", "var", "package", "select", "default", "go", "defer", "return", "chan", "import":
185+ return s + "Arg"
186+ }
187+ return s
188+}
189+func (g *gen) params(m spec.Message, event bool) {
190+ for _, a := range m.Args {
191+ if a.Type == "new_id" && a.Interface == "" && !event {
192+ g.p(", interfaceName string, version uint32, id uint32")
193+ continue
194+ }
195+ g.p(", %s %s", param(a), g.typ(a, event))
196+ }
197+}
198+func (g *gen) generate(pr *spec.Protocol, pkg, tool string) ([]byte, error) {
199+ g.p("// Code generated by %s; DO NOT EDIT.\npackage %s\nimport (\"github.com/bnema/purego-libwayland/server\"\n", tool, pkg)
200+ pin := false
201+ for _, it := range pr.Interfaces {
202+ for _, m := range it.Events {
203+ for _, a := range m.Args {
204+ if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") {
205+ pin = true
206+ }
207+ }
208+ }
209+ }
210+ if pin {
211+ g.p("\"runtime\"\n")
212+ }
213+ for _, it := range pr.Interfaces {
214+ if len(it.Requests) > 0 {
215+ g.p("\"fmt\"\n")
216+ break
217+ }
218+ }
219+ aliases := make([]string, 0, len(g.imports))
220+ for alias := range g.imports {
221+ aliases = append(aliases, alias)
222+ }
223+ sort.Strings(aliases)
224+ for _, alias := range aliases {
225+ g.p("%s %q\n", alias, g.imports[alias])
226+ }
227+ g.p(")\n")
228+ for _, it := range pr.Interfaces {
229+ g.p("var %sInterface *server.Interface\n", name(it.Name))
230+ }
231+ g.p("func init(){\n")
232+ for _, it := range pr.Interfaces {
233+ g.p("%sInterface=&server.Interface{Name:%q,Version:%d}\n", name(it.Name), it.Name, it.Version)
234+ }
235+ for _, it := range pr.Interfaces {
236+ n := name(it.Name)
237+ g.p("%sInterface.Requests=[]server.Message{\n", n)
238+ for _, m := range it.Requests {
239+ if e := g.table(m); e != nil {
240+ return nil, e
241+ }
242+ }
243+ g.p("}\n%sInterface.Events=[]server.Message{\n", n)
244+ for _, m := range it.Events {
245+ if e := g.table(m); e != nil {
246+ return nil, e
247+ }
248+ }
249+ g.p("}\n")
250+ }
251+ g.p("if err:=server.NewInterfaces(")
252+ for _, it := range pr.Interfaces {
253+ g.p("%sInterface,", name(it.Name))
254+ }
255+ g.p(");err!=nil{panic(err)}}\n")
256+ for _, it := range pr.Interfaces {
257+ n := name(it.Name)
258+ 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, summary(it.Description), n, n, n, n)
259+ for j, m := range it.Requests {
260+ g.p("const %sRequest%s uint32=%d\n", n, camel(m.Name), j)
261+ }
262+ for j, m := range it.Events {
263+ g.p("const %sEvent%s uint32=%d\n", n, camel(m.Name), j)
264+ }
265+ for _, en := range it.Enums {
266+ t := n + camel(en.Name)
267+ g.p("// %s %s", t, summary(en.Description))
268+ if en.Bitfield {
269+ g.p(" (bitfield)")
270+ }
271+ g.p(".\ntype %s uint32\nconst(\n", t)
272+ for _, v := range en.Entries {
273+ g.p("%s%s %s=%s\n", t, camel(v.Name), t, v.Value)
274+ }
275+ g.p(")\n")
276+ }
277+ g.p("// %sHandler handles %s requests.\ntype %sHandler interface{\n", n, it.Name, n)
278+ for _, m := range it.Requests {
279+ g.p("%s(self *%s", camel(m.Name), n)
280+ g.params(m, false)
281+ g.p(")\n")
282+ }
283+ g.p("}\nfunc New%s(c server.Client,version int32,id uint32,h %sHandler)(*%s,error){", n, n, n)
284+ if len(it.Requests) > 0 {
285+ g.p("if h==nil{return nil,fmt.Errorf(%q)}\n", "purego-libwayland: nil handler for "+it.Name)
286+ }
287+ 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)
288+ for j, m := range it.Requests {
289+ g.p("case %d:h.%s(self", j, camel(m.Name))
290+ idx := 0
291+ for _, a := range m.Args {
292+ if a.Type == "new_id" && a.Interface == "" {
293+ g.p(",args[%d].String(),args[%d].Uint(),args[%d].NewID()", idx, idx+1, idx+2)
294+ idx += 3
295+ continue
296+ }
297+ x := fmt.Sprintf("args[%d]", idx)
298+ idx++
299+ switch a.Type {
300+ case "uint", "int", "fixed", "string", "array", "fd", "new_id":
301+ accessor := camel(a.Type)
302+ if a.Type == "new_id" {
303+ accessor = "NewID"
304+ }
305+ g.p(",%s.%s()", x, accessor)
306+ case "object":
307+ if a.Interface == "" {
308+ g.p(",%s.Resource()", x)
309+ } else {
310+ g.p(",%sWrap%s(%s.Resource())", func() string {
311+ if v := g.owners[a.Interface]; v != "" {
312+ return v + "."
313+ }
314+ return ""
315+ }(), name(a.Interface), x)
316+ }
317+ }
318+ }
319+ g.p(")")
320+ if m.Type == "destructor" {
321+ g.p(";self.Destroy()")
322+ }
323+ g.p("\n")
324+ }
325+ g.p("}});if err!=nil{return nil,err};self=Wrap%s(r);r.Data=self;return self,nil}\n", n)
326+ for _, m := range it.Events {
327+ g.p("// Send%s %s", camel(m.Name), summary(m.Description))
328+ if m.Since > 1 {
329+ g.p(" (since version %d)", m.Since)
330+ }
331+ g.p(".\nfunc(self *%s)Send%s(", n, camel(m.Name))
332+ for i, a := range m.Args {
333+ if i > 0 {
334+ g.p(",")
335+ }
336+ g.p("%s %s", param(a), g.typ(a, true))
337+ }
338+ g.p("){\n")
339+ needsPin := false
340+ for _, a := range m.Args {
341+ if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") {
342+ needsPin = true
343+ }
344+ }
345+ if needsPin {
346+ g.p("var p runtime.Pinner;defer p.Unpin()\n")
347+ }
348+ g.p("self.PostEvent(%sEvent%s", n, camel(m.Name))
349+ for _, a := range m.Args {
350+ v := param(a)
351+ if a.Type == "new_id" && a.Interface == "" {
352+ g.p(",server.String(&p,%s.Iface().Name,false),server.Uint(uint32(%s.Version())),server.NewID(%s)", v, v, v)
353+ continue
354+ }
355+ switch a.Type {
356+ case "uint", "int", "fd":
357+ g.p(",server.%s(%s)", map[string]string{"uint": "Uint", "int": "Int", "fd": "Fd"}[a.Type], v)
358+ case "fixed":
359+ g.p(",server.FixedArg(%s)", v)
360+ case "string":
361+ g.p(",server.String(&p,%s,%t)", v, a.AllowNull)
362+ case "array":
363+ g.p(",server.Array(&p,%s)", v)
364+ case "object", "new_id":
365+ constructor := "Object"
366+ if a.Type == "new_id" {
367+ constructor = "NewID"
368+ }
369+ if a.Interface == "" {
370+ g.p(",server.%s(%s)", constructor, v)
371+ } else {
372+ g.p(",server.%s(resourceOf(%s))", constructor, v)
373+ }
374+ }
375+ }
376+ g.p(")\n}\n")
377+ }
378+ g.p("func New%sGlobal(d *server.Display,version int32,bind server.BindFunc)error{return d.CreateGlobal(%sInterface,version,bind)}\n", n, n)
379+ }
380+ g.p("func resourceOf(v interface{GetResource()*server.Resource})*server.Resource{if v==nil{return nil};return v.GetResource()}\n")
381+ for _, it := range pr.Interfaces {
382+ n := name(it.Name)
383+ g.p("func(self *%s)GetResource()*server.Resource{if self==nil{return nil};return self.Resource}\n", n)
384+ }
385+ return format.Source(g.Bytes())
386+}
@@ -0,0 +1,386 @@
1+// Package server generates Go server-side bindings (for
2+// github.com/bnema/purego-libwayland/server) from Wayland protocol XML.
3+package server
4+
5+import (
6+ "bytes"
7+ "fmt"
8+ "go/format"
9+ "sort"
10+ "strings"
11+ "unicode"
12+
13+ "github.com/bnema/go-wayland-bindings/spec"
14+)
15+
16+// Options configures Generate.
17+type Options struct {
18+ // Package is the Go package name of the generated file.
19+ Package string
20+ // Imports maps an import alias to a Go import path.
21+ Imports map[string]string
22+ // Owners maps an interface name to the import alias of the package that
23+ // defines it; "" means the generated package itself. Interfaces defined
24+ // by the protocol being generated are always local.
25+ Owners map[string]string
26+ // Tool is the tool name in the generated-code header (default "wlbgen").
27+ Tool string
28+}
29+
30+// Generate returns the gofmt-formatted server bindings for p.
31+func Generate(p *spec.Protocol, opts Options) ([]byte, error) {
32+ if p == nil {
33+ return nil, fmt.Errorf("server: nil protocol")
34+ }
35+ if opts.Package == "" {
36+ return nil, fmt.Errorf("server: empty package name")
37+ }
38+ tool := opts.Tool
39+ if tool == "" {
40+ tool = "wlbgen"
41+ }
42+ g := &gen{owners: map[string]string{}, imports: map[string]string{}}
43+ for k, v := range opts.Imports {
44+ g.imports[k] = v
45+ }
46+ for k, v := range opts.Owners {
47+ g.owners[k] = v
48+ }
49+ for _, it := range p.Interfaces {
50+ g.owners[it.Name] = ""
51+ }
52+ return g.generate(p, opts.Package, tool)
53+}
54+
55+func summary(d *spec.Description) string {
56+ if d == nil {
57+ return ""
58+ }
59+ return d.Summary
60+}
61+
62+func camel(s string) string {
63+ var b strings.Builder
64+ for _, part := range strings.Split(s, "_") {
65+ if part == "" {
66+ continue
67+ }
68+ r := []rune(part)
69+ b.WriteRune(unicode.ToUpper(r[0]))
70+ b.WriteString(string(r[1:]))
71+ }
72+ return b.String()
73+}
74+func name(s string) string {
75+ for _, prefix := range []string{"wl_", "xdg_"} {
76+ if strings.HasPrefix(s, prefix) {
77+ s = strings.TrimPrefix(s, prefix)
78+ break
79+ }
80+ }
81+ return camel(s)
82+}
83+func signature(m spec.Message) string {
84+ var b strings.Builder
85+ if m.Since > 1 {
86+ fmt.Fprint(&b, m.Since)
87+ }
88+ for _, a := range m.Args {
89+ if a.Type == "new_id" && a.Interface == "" {
90+ b.WriteString("sun")
91+ continue
92+ }
93+ if a.AllowNull {
94+ b.WriteByte('?')
95+ }
96+ b.WriteString(map[string]string{"int": "i", "uint": "u", "fixed": "f", "string": "s", "array": "a", "object": "o", "new_id": "n", "fd": "h"}[a.Type])
97+ }
98+ return b.String()
99+}
100+
101+type gen struct {
102+ bytes.Buffer
103+ owners map[string]string
104+ imports map[string]string
105+}
106+
107+func (g *gen) p(f string, a ...any) { fmt.Fprintf(&g.Buffer, f, a...) }
108+func (g *gen) ref(s string) (string, error) {
109+ if s == "" {
110+ return "nil", nil
111+ }
112+ alias, ok := g.owners[s]
113+ if !ok {
114+ return "", fmt.Errorf("unknown interface %s", s)
115+ }
116+ if alias != "" {
117+ alias += "."
118+ }
119+ return alias + name(s) + "Interface", nil
120+}
121+func (g *gen) wrapper(s string) string {
122+ alias := g.owners[s]
123+ if alias != "" {
124+ alias += "."
125+ }
126+ return alias + name(s)
127+}
128+func (g *gen) table(m spec.Message) error {
129+ var ts []string
130+ for _, a := range m.Args {
131+ if a.Type == "new_id" && a.Interface == "" {
132+ ts = append(ts, "nil", "nil", "nil")
133+ continue
134+ }
135+ t := "nil"
136+ if a.Type == "object" || a.Type == "new_id" {
137+ var e error
138+ t, e = g.ref(a.Interface)
139+ if e != nil {
140+ return e
141+ }
142+ }
143+ ts = append(ts, t)
144+ }
145+ g.p("{Name:%q,Signature:%q,Types:[]*server.Interface{%s}},\n", m.Name, signature(m), strings.Join(ts, ","))
146+ return nil
147+}
148+func (g *gen) typ(a spec.Arg, event bool) string {
149+ switch a.Type {
150+ case "uint":
151+ return "uint32"
152+ case "int":
153+ return "int32"
154+ case "fixed":
155+ return "server.Fixed"
156+ case "string":
157+ return "string"
158+ case "array":
159+ return "[]byte"
160+ case "fd":
161+ return "int"
162+ case "new_id":
163+ if !event {
164+ return "uint32"
165+ }
166+ fallthrough
167+ case "object":
168+ if a.Interface == "" {
169+ return "*server.Resource"
170+ }
171+ return "*" + g.wrapper(a.Interface)
172+ }
173+ return "server.Arg"
174+}
175+func param(a spec.Arg) string {
176+ s := a.Name
177+ if parts := strings.Split(s, "_"); len(parts) > 1 {
178+ s = parts[0]
179+ for _, part := range parts[1:] {
180+ s += camel(part)
181+ }
182+ }
183+ switch s {
184+ case "interface", "type", "map", "range", "func", "var", "package", "select", "default", "go", "defer", "return", "chan", "import":
185+ return s + "Arg"
186+ }
187+ return s
188+}
189+func (g *gen) params(m spec.Message, event bool) {
190+ for _, a := range m.Args {
191+ if a.Type == "new_id" && a.Interface == "" && !event {
192+ g.p(", interfaceName string, version uint32, id uint32")
193+ continue
194+ }
195+ g.p(", %s %s", param(a), g.typ(a, event))
196+ }
197+}
198+func (g *gen) generate(pr *spec.Protocol, pkg, tool string) ([]byte, error) {
199+ g.p("// Code generated by %s; DO NOT EDIT.\npackage %s\nimport (\"github.com/bnema/purego-libwayland/server\"\n", tool, pkg)
200+ pin := false
201+ for _, it := range pr.Interfaces {
202+ for _, m := range it.Events {
203+ for _, a := range m.Args {
204+ if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") {
205+ pin = true
206+ }
207+ }
208+ }
209+ }
210+ if pin {
211+ g.p("\"runtime\"\n")
212+ }
213+ for _, it := range pr.Interfaces {
214+ if len(it.Requests) > 0 {
215+ g.p("\"fmt\"\n")
216+ break
217+ }
218+ }
219+ aliases := make([]string, 0, len(g.imports))
220+ for alias := range g.imports {
221+ aliases = append(aliases, alias)
222+ }
223+ sort.Strings(aliases)
224+ for _, alias := range aliases {
225+ g.p("%s %q\n", alias, g.imports[alias])
226+ }
227+ g.p(")\n")
228+ for _, it := range pr.Interfaces {
229+ g.p("var %sInterface *server.Interface\n", name(it.Name))
230+ }
231+ g.p("func init(){\n")
232+ for _, it := range pr.Interfaces {
233+ g.p("%sInterface=&server.Interface{Name:%q,Version:%d}\n", name(it.Name), it.Name, it.Version)
234+ }
235+ for _, it := range pr.Interfaces {
236+ n := name(it.Name)
237+ g.p("%sInterface.Requests=[]server.Message{\n", n)
238+ for _, m := range it.Requests {
239+ if e := g.table(m); e != nil {
240+ return nil, e
241+ }
242+ }
243+ g.p("}\n%sInterface.Events=[]server.Message{\n", n)
244+ for _, m := range it.Events {
245+ if e := g.table(m); e != nil {
246+ return nil, e
247+ }
248+ }
249+ g.p("}\n")
250+ }
251+ g.p("if err:=server.NewInterfaces(")
252+ for _, it := range pr.Interfaces {
253+ g.p("%sInterface,", name(it.Name))
254+ }
255+ g.p(");err!=nil{panic(err)}}\n")
256+ for _, it := range pr.Interfaces {
257+ n := name(it.Name)
258+ 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, summary(it.Description), n, n, n, n)
259+ for j, m := range it.Requests {
260+ g.p("const %sRequest%s uint32=%d\n", n, camel(m.Name), j)
261+ }
262+ for j, m := range it.Events {
263+ g.p("const %sEvent%s uint32=%d\n", n, camel(m.Name), j)
264+ }
265+ for _, en := range it.Enums {
266+ t := n + camel(en.Name)
267+ g.p("// %s %s", t, summary(en.Description))
268+ if en.Bitfield {
269+ g.p(" (bitfield)")
270+ }
271+ g.p(".\ntype %s uint32\nconst(\n", t)
272+ for _, v := range en.Entries {
273+ g.p("%s%s %s=%s\n", t, camel(v.Name), t, v.Value)
274+ }
275+ g.p(")\n")
276+ }
277+ g.p("// %sHandler handles %s requests.\ntype %sHandler interface{\n", n, it.Name, n)
278+ for _, m := range it.Requests {
279+ g.p("%s(self *%s", camel(m.Name), n)
280+ g.params(m, false)
281+ g.p(")\n")
282+ }
283+ g.p("}\nfunc New%s(c server.Client,version int32,id uint32,h %sHandler)(*%s,error){", n, n, n)
284+ if len(it.Requests) > 0 {
285+ g.p("if h==nil{return nil,fmt.Errorf(%q)}\n", "purego-libwayland: nil handler for "+it.Name)
286+ }
287+ 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)
288+ for j, m := range it.Requests {
289+ g.p("case %d:h.%s(self", j, camel(m.Name))
290+ idx := 0
291+ for _, a := range m.Args {
292+ if a.Type == "new_id" && a.Interface == "" {
293+ g.p(",args[%d].String(),args[%d].Uint(),args[%d].NewID()", idx, idx+1, idx+2)
294+ idx += 3
295+ continue
296+ }
297+ x := fmt.Sprintf("args[%d]", idx)
298+ idx++
299+ switch a.Type {
300+ case "uint", "int", "fixed", "string", "array", "fd", "new_id":
301+ accessor := camel(a.Type)
302+ if a.Type == "new_id" {
303+ accessor = "NewID"
304+ }
305+ g.p(",%s.%s()", x, accessor)
306+ case "object":
307+ if a.Interface == "" {
308+ g.p(",%s.Resource()", x)
309+ } else {
310+ g.p(",%sWrap%s(%s.Resource())", func() string {
311+ if v := g.owners[a.Interface]; v != "" {
312+ return v + "."
313+ }
314+ return ""
315+ }(), name(a.Interface), x)
316+ }
317+ }
318+ }
319+ g.p(")")
320+ if m.Type == "destructor" {
321+ g.p(";self.Destroy()")
322+ }
323+ g.p("\n")
324+ }
325+ g.p("}});if err!=nil{return nil,err};self=Wrap%s(r);r.Data=self;return self,nil}\n", n)
326+ for _, m := range it.Events {
327+ g.p("// Send%s %s", camel(m.Name), summary(m.Description))
328+ if m.Since > 1 {
329+ g.p(" (since version %d)", m.Since)
330+ }
331+ g.p(".\nfunc(self *%s)Send%s(", n, camel(m.Name))
332+ for i, a := range m.Args {
333+ if i > 0 {
334+ g.p(",")
335+ }
336+ g.p("%s %s", param(a), g.typ(a, true))
337+ }
338+ g.p("){\n")
339+ needsPin := false
340+ for _, a := range m.Args {
341+ if a.Type == "string" || a.Type == "array" || (a.Type == "new_id" && a.Interface == "") {
342+ needsPin = true
343+ }
344+ }
345+ if needsPin {
346+ g.p("var p runtime.Pinner;defer p.Unpin()\n")
347+ }
348+ g.p("self.PostEvent(%sEvent%s", n, camel(m.Name))
349+ for _, a := range m.Args {
350+ v := param(a)
351+ if a.Type == "new_id" && a.Interface == "" {
352+ g.p(",server.String(&p,%s.Iface().Name,false),server.Uint(uint32(%s.Version())),server.NewID(%s)", v, v, v)
353+ continue
354+ }
355+ switch a.Type {
356+ case "uint", "int", "fd":
357+ g.p(",server.%s(%s)", map[string]string{"uint": "Uint", "int": "Int", "fd": "Fd"}[a.Type], v)
358+ case "fixed":
359+ g.p(",server.FixedArg(%s)", v)
360+ case "string":
361+ g.p(",server.String(&p,%s,%t)", v, a.AllowNull)
362+ case "array":
363+ g.p(",server.Array(&p,%s)", v)
364+ case "object", "new_id":
365+ constructor := "Object"
366+ if a.Type == "new_id" {
367+ constructor = "NewID"
368+ }
369+ if a.Interface == "" {
370+ g.p(",server.%s(%s)", constructor, v)
371+ } else {
372+ g.p(",server.%s(resourceOf(%s))", constructor, v)
373+ }
374+ }
375+ }
376+ g.p(")\n}\n")
377+ }
378+ g.p("func New%sGlobal(d *server.Display,version int32,bind server.BindFunc)error{return d.CreateGlobal(%sInterface,version,bind)}\n", n, n)
379+ }
380+ g.p("func resourceOf(v interface{GetResource()*server.Resource})*server.Resource{if v==nil{return nil};return v.GetResource()}\n")
381+ for _, it := range pr.Interfaces {
382+ n := name(it.Name)
383+ g.p("func(self *%s)GetResource()*server.Resource{if self==nil{return nil};return self.Resource}\n", n)
384+ }
385+ return format.Source(g.Bytes())
386+}
A internal/gen/server/server_test.go +57 -0
@@ -0,0 +1,57 @@
1+package server
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/spec"
8+)
9+
10+func TestUntypedNewIDEvent(t *testing.T) {
11+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "test_device", Version: 1, Events: []spec.Message{{Name: "created", Args: []spec.Arg{{Name: "id", Type: "new_id"}}}}}}}
12+ b, err := Generate(p, Options{Package: "test"})
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ s := string(b)
17+ if !strings.HasPrefix(s, "// Code generated by wlbgen; DO NOT EDIT.\n") {
18+ t.Fatalf("unexpected header:\n%s", s)
19+ }
20+ if !strings.Contains(s, `Signature: "sun"`) || !strings.Contains(s, `server.String(&p, id.Iface().Name, false), server.Uint(uint32(id.Version())), server.NewID(id)`) {
21+ t.Fatalf("untyped new_id event missing wire args:\n%s", s)
22+ }
23+}
24+
25+func TestToolHeader(t *testing.T) {
26+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "test_device", Version: 1}}}
27+ b, err := Generate(p, Options{Package: "test", Tool: "wlgen"})
28+ if err != nil {
29+ t.Fatal(err)
30+ }
31+ if !strings.HasPrefix(string(b), "// Code generated by wlgen; DO NOT EDIT.\n") {
32+ t.Fatalf("unexpected header:\n%s", b)
33+ }
34+}
35+
36+func TestUnknownInterface(t *testing.T) {
37+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "a", Version: 1, Events: []spec.Message{{Name: "e", Args: []spec.Arg{{Name: "o", Type: "object", Interface: "missing"}}}}}}}
38+ if _, err := Generate(p, Options{Package: "test"}); err == nil {
39+ t.Fatal("expected error for unknown interface")
40+ }
41+}
42+
43+func TestSignature(t *testing.T) {
44+ cases := []struct {
45+ m spec.Message
46+ want string
47+ }{
48+ {spec.Message{Name: "bind", Args: []spec.Arg{{Type: "uint"}, {Type: "new_id"}}}, "usun"},
49+ {spec.Message{Since: 2, Args: []spec.Arg{{Type: "object", AllowNull: true}, {Type: "fd"}, {Type: "fixed"}}}, "2?ohf"},
50+ {spec.Message{Args: []spec.Arg{{Type: "new_id", Interface: "wl_surface"}}}, "n"},
51+ }
52+ for _, c := range cases {
53+ if got := signature(c.m); got != c.want {
54+ t.Errorf("signature(%+v)=%q, want %q", c.m, got, c.want)
55+ }
56+ }
57+}
@@ -0,0 +1,57 @@
1+package server
2+
3+import (
4+ "strings"
5+ "testing"
6+
7+ "github.com/bnema/go-wayland-bindings/spec"
8+)
9+
10+func TestUntypedNewIDEvent(t *testing.T) {
11+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "test_device", Version: 1, Events: []spec.Message{{Name: "created", Args: []spec.Arg{{Name: "id", Type: "new_id"}}}}}}}
12+ b, err := Generate(p, Options{Package: "test"})
13+ if err != nil {
14+ t.Fatal(err)
15+ }
16+ s := string(b)
17+ if !strings.HasPrefix(s, "// Code generated by wlbgen; DO NOT EDIT.\n") {
18+ t.Fatalf("unexpected header:\n%s", s)
19+ }
20+ if !strings.Contains(s, `Signature: "sun"`) || !strings.Contains(s, `server.String(&p, id.Iface().Name, false), server.Uint(uint32(id.Version())), server.NewID(id)`) {
21+ t.Fatalf("untyped new_id event missing wire args:\n%s", s)
22+ }
23+}
24+
25+func TestToolHeader(t *testing.T) {
26+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "test_device", Version: 1}}}
27+ b, err := Generate(p, Options{Package: "test", Tool: "wlgen"})
28+ if err != nil {
29+ t.Fatal(err)
30+ }
31+ if !strings.HasPrefix(string(b), "// Code generated by wlgen; DO NOT EDIT.\n") {
32+ t.Fatalf("unexpected header:\n%s", b)
33+ }
34+}
35+
36+func TestUnknownInterface(t *testing.T) {
37+ p := &spec.Protocol{Interfaces: []spec.Interface{{Name: "a", Version: 1, Events: []spec.Message{{Name: "e", Args: []spec.Arg{{Name: "o", Type: "object", Interface: "missing"}}}}}}}
38+ if _, err := Generate(p, Options{Package: "test"}); err == nil {
39+ t.Fatal("expected error for unknown interface")
40+ }
41+}
42+
43+func TestSignature(t *testing.T) {
44+ cases := []struct {
45+ m spec.Message
46+ want string
47+ }{
48+ {spec.Message{Name: "bind", Args: []spec.Arg{{Type: "uint"}, {Type: "new_id"}}}, "usun"},
49+ {spec.Message{Since: 2, Args: []spec.Arg{{Type: "object", AllowNull: true}, {Type: "fd"}, {Type: "fixed"}}}, "2?ohf"},
50+ {spec.Message{Args: []spec.Arg{{Type: "new_id", Interface: "wl_surface"}}}, "n"},
51+ }
52+ for _, c := range cases {
53+ if got := signature(c.m); got != c.want {
54+ t.Errorf("signature(%+v)=%q, want %q", c.m, got, c.want)
55+ }
56+ }
57+}

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 1

↑↓ moveenter openesc close