1// Package scanner generates WLTurbo-backed Go bindings from Wayland protocol
2// XML files.
3//
4// The generated code owns protocol semantics only: it embeds wl.BaseProxy,
5// allocates and registers child objects through the wl.Context, sends requests
6// through that context, and dispatches typed events to registered handlers.
7// Message framing, descriptor passing and connection lifetime stay in the
8// transport, so a generated binding never opens a socket or writes a header.
9package scanner
10
11import (
12 "bytes"
13 "encoding/xml"
14 "fmt"
15 "go/format"
16 "io"
17 "os"
18 "path/filepath"
19 "sort"
20 "strings"
21 "text/template"
22)
23
24// TransportImport is the import path generated bindings use for the Wayland
25// transport.
26const TransportImport = "github.com/bnema/wlturbo/wl"
27
28// Protocol represents a Wayland protocol specification
29type Protocol struct {
30 XMLName xml.Name `xml:"protocol"`
31 Name string `xml:"name,attr"`
32 Copyright string `xml:"copyright"`
33 Description *Description `xml:"description"`
34 Interfaces []Interface `xml:"interface"`
35}
36
37// Interface represents a Wayland interface in the protocol
38type Interface struct {
39 Name string `xml:"name,attr"`
40 Version int `xml:"version,attr"`
41 Description *Description `xml:"description"`
42 Requests []Request `xml:"request"`
43 Events []Event `xml:"event"`
44 Enums []Enum `xml:"enum"`
45}
46
47// Request represents a client-to-server message
48type Request struct {
49 Name string `xml:"name,attr"`
50 Type string `xml:"type,attr"` // "destructor" or empty
51 Since int `xml:"since,attr"`
52 Description *Description `xml:"description"`
53 Args []Arg `xml:"arg"`
54}
55
56// Event represents a server-to-client message
57type Event struct {
58 Type string `xml:"type,attr"`
59 Name string `xml:"name,attr"`
60 Since int `xml:"since,attr"`
61 Description *Description `xml:"description"`
62 Args []Arg `xml:"arg"`
63}
64
65// Arg represents a message argument
66type Arg struct {
67 Name string `xml:"name,attr"`
68 Type string `xml:"type,attr"`
69 Summary string `xml:"summary,attr"`
70 Interface string `xml:"interface,attr"`
71 AllowNull bool `xml:"allow-null,attr"`
72 Enum string `xml:"enum,attr"`
73}
74
75// Enum represents an enumeration type
76type Enum struct {
77 Name string `xml:"name,attr"`
78 Since int `xml:"since,attr"`
79 Bitfield bool `xml:"bitfield,attr"`
80 Description *Description `xml:"description"`
81 Entries []Entry `xml:"entry"`
82}
83
84// Entry represents an enum value
85type Entry struct {
86 Name string `xml:"name,attr"`
87 Value string `xml:"value,attr"`
88 Summary string `xml:"summary,attr"`
89 Since int `xml:"since,attr"`
90}
91
92// Description represents documentation
93type Description struct {
94 Summary string `xml:"summary,attr"`
95 Text string `xml:",chardata"`
96}
97
98// Scanner generates Go bindings from Wayland protocol XML
99type Scanner struct {
100 protocol *Protocol
101 source string
102 // CrossPackage maps interface names to Go package import paths. Local wins.
103 CrossPackage map[string]string
104}
105
106// NewScanner creates a new protocol scanner
107func NewScanner() *Scanner {
108 return &Scanner{}
109}
110
111// ParseXML parses a Wayland protocol XML file
112func (s *Scanner) ParseXML(path string) error {
113 file, err := os.Open(path)
114 if err != nil {
115 return fmt.Errorf("failed to open XML file: %w", err)
116 }
117 defer func() { _ = file.Close() }()
118
119 data, err := io.ReadAll(file)
120 if err != nil {
121 return fmt.Errorf("failed to read XML file: %w", err)
122 }
123
124 var protocol Protocol
125 if err := xml.Unmarshal(data, &protocol); err != nil {
126 return fmt.Errorf("failed to parse XML: %w", err)
127 }
128
129 s.protocol = &protocol
130 // Only the base name is recorded: generated output must not depend on the
131 // path the XML happened to be read from.
132 s.source = filepath.Base(path)
133 return nil
134}
135
136// Generate creates Go source code for the parsed protocol
137func (s *Scanner) Generate(packageName string) ([]byte, error) {
138 if s.protocol == nil {
139 return nil, fmt.Errorf("no protocol parsed")
140 }
141
142 data, err := s.prepareTemplateData(packageName)
143 if err != nil {
144 return nil, err
145 }
146
147 var buf bytes.Buffer
148 tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
149 if err := tmpl.Execute(&buf, data); err != nil {
150 return nil, fmt.Errorf("failed to execute template: %w", err)
151 }
152
153 formatted, err := format.Source(buf.Bytes())
154 if err != nil {
155 return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
156 }
157 return formatted, nil
158}
159
160// templateData holds data for template generation
161type templateData struct {
162 Package string
163 Protocol string
164 Source string
165 Copyright string
166 ImportBlock string
167 Constants []constantData
168 Interfaces []interfaceData
169}
170
171type interfaceData struct {
172 Name string
173 GoName string
174 Version int
175 HasEvents bool
176 SignatureCases []signatureCase
177 Requests []requestData
178 Events []eventData
179}
180
181type signatureCase struct {
182 Opcode int
183 Signature string
184}
185type requestData struct {
186 Name string
187 GoName string
188 Doc string
189 Opcode int
190 Destructor bool
191 Since int
192 Params string
193 Results string
194 ArgPreparations []string
195 CreatesChild bool
196 ChildVar string
197 ChildType string
198 ChildFactory string
199 ErrorReturn string
200 HasFDs bool
201 FDList string
202 ArgList string
203}
204
205// argSlot is one positional argument of a request, in declaration order. A
206// new_id slot is filled in once the child object has been created.
207type argSlot struct {
208 newID bool
209 expr string
210}
211
212type eventData struct {
213 HasFD bool
214 ServerDestroy bool
215 FDNames []string
216 Name string
217 GoName string
218 Opcode int
219 HandlerField string
220 HandlerAccessor string
221 HandlerParams string
222 HandlerArgs string
223 DecodeLines []string
224}
225
226type constantData struct {
227 Name string
228 Value string
229 Type string
230 Enum string
231 Entry string
232}
233
234// Only display and registry have handwritten bootstrap bindings.
235var bootstrapTypes = map[string]bool{
236 "Display": true,
237 "Registry": true,
238}
239
240// goKeywords are avoided as parameter names because generated identifiers
241// must not collide with the language.
242var goKeywords = map[string]bool{
243 "break": true, "case": true, "chan": true, "const": true, "continue": true,
244 "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
245 "func": true, "go": true, "goto": true, "if": true, "import": true,
246 "interface": true, "map": true, "package": true, "range": true, "return": true,
247 "select": true, "struct": true, "switch": true, "type": true, "var": true,
248}
249
250func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
251 data := templateData{
252 Package: packageName,
253 Protocol: s.protocol.Name,
254 Source: s.source,
255 }
256 if notice := strings.TrimSpace(s.protocol.Copyright); notice != "" {
257 var comment strings.Builder
258 for _, line := range strings.Split(notice, "\n") {
259 comment.WriteString("// ")
260 comment.WriteString(strings.TrimSpace(line))
261 comment.WriteByte('\n')
262 }
263 data.Copyright = comment.String()
264 }
265
266 imports := []string{TransportImport}
267 usedCross := make(map[string]bool)
268 for _, candidate := range s.protocol.Interfaces {
269 for _, req := range candidate.Requests {
270 for _, arg := range req.Args {
271 if arg.Type == "object" || arg.Type == "new_id" {
272 usedCross[arg.Interface] = true
273 }
274 }
275 }
276 for _, ev := range candidate.Events {
277 for _, arg := range ev.Args {
278 // Event object references are decoded as IDs, not imported proxies.
279 if arg.Type == "new_id" {
280 usedCross[arg.Interface] = true
281 }
282 }
283 }
284 }
285 for iface, path := range s.CrossPackage {
286 local := false
287 for _, candidate := range s.protocol.Interfaces {
288 if candidate.Name == iface {
289 local = true
290 break
291 }
292 }
293 if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
294 continue
295 }
296 alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
297 imports = append(imports, alias+" "+path)
298 }
299 sort.Strings(imports)
300 stdImports := []string{}
301 needsSync := false
302
303 // Enum constants are package-level declarations, so two interfaces that
304 // declare the same enum entry (for example xdg_surface.error.invalid_size
305 // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
306 // occurs in more than one interface are qualified with their interface
307 // name below; unique names keep their historical, unprefixed form.
308 enumEntryCounts := map[string]int{}
309 for _, iface := range s.protocol.Interfaces {
310 for _, enum := range iface.Enums {
311 for _, entry := range enum.Entries {
312 enumEntryCounts[constantName(enum.Name, entry.Name)]++
313 }
314 }
315 }
316
317 for _, iface := range s.protocol.Interfaces {
318 if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
319 continue
320 }
321 ifaceData, err := s.processInterface(iface)
322 if err != nil {
323 return data, err
324 }
325 data.Interfaces = append(data.Interfaces, ifaceData)
326 if len(iface.Events) > 0 {
327 needsSync = true
328 }
329
330 for _, enum := range iface.Enums {
331 for _, entry := range enum.Entries {
332 name := constantName(enum.Name, entry.Name)
333 if enumEntryCounts[name] > 1 {
334 name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
335 }
336 // Enum values are emitted untyped: the same enum travels as a uint
337 // in one protocol and as an int in another, and an untyped constant
338 // assigns to either without casts at the call site.
339 data.Constants = append(data.Constants, constantData{
340 Name: name,
341 Value: entry.Value,
342 Enum: enum.Name,
343 Entry: entry.Name,
344 })
345 }
346 }
347 }
348
349 // Handlers are registered and dispatched from different goroutines, so an
350 // interface with events needs a mutex.
351 if needsSync {
352 stdImports = append(stdImports, "sync")
353 }
354
355 var block strings.Builder
356 block.WriteString("import (\n")
357 for _, imp := range stdImports {
358 fmt.Fprintf(&block, "\t%q\n", imp)
359 }
360 if len(stdImports) > 0 && len(imports) > 0 {
361 block.WriteString("\n")
362 }
363 for _, imp := range imports {
364 if strings.Contains(imp, " ") {
365 parts := strings.SplitN(imp, " ", 2)
366 fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
367 } else {
368 fmt.Fprintf(&block, "\t%q\n", imp)
369 }
370 }
371 block.WriteString(")")
372 data.ImportBlock = block.String()
373
374 return data, nil
375}
376
377func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
378 data := interfaceData{
379 Name: iface.Name,
380 GoName: s.toGoName(iface.Name),
381 Version: iface.Version,
382 HasEvents: len(iface.Events) > 0,
383 }
384
385 for i, req := range iface.Requests {
386 request, err := s.processRequest(iface, req, i)
387 if err != nil {
388 return data, err
389 }
390 data.Requests = append(data.Requests, request)
391 }
392
393 for i, event := range iface.Events {
394 var sig strings.Builder
395 for _, arg := range event.Args {
396 sig.WriteString(arg.Type)
397 sig.WriteByte(',')
398 }
399 data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
400 eventData, err := s.processEvent(event, i)
401 if iface.Name == "wl_callback" && event.Name == "done" {
402 eventData.ServerDestroy = true
403 }
404 if err != nil {
405 return data, err
406 }
407 data.Events = append(data.Events, eventData)
408 }
409
410 return data, nil
411}
412
413func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
414 data := requestData{
415 Name: req.Name,
416 GoName: s.toGoName(req.Name),
417 Doc: s.formatDescription(req.Description),
418 Opcode: opcode,
419 Destructor: req.Type == "destructor",
420 Since: req.Since,
421 ChildVar: "child",
422 }
423
424 var (
425 params []string
426 argSlots []argSlot
427 argExprs []string
428 fdExprs []string
429 newIDArg *Arg
430 )
431
432 for i, arg := range req.Args {
433 if arg.Type == "new_id" {
434 if newIDArg != nil {
435 return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
436 }
437 newIDArg = &arg
438 // The child object is created later, but it keeps its declared
439 // position: the wire order is the argument order.
440 argSlots = append(argSlots, argSlot{newID: true})
441 continue
442 }
443
444 name := s.paramName(arg.Name, i)
445 goType, err := s.goTypeForArg(arg)
446 if err != nil {
447 return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
448 }
449
450 expr := name
451 switch arg.Type {
452 case "object":
453 // Objects are pointer types, and travel as wl.Object so a nil
454 // pointer is never sent as a non-nil interface holding a typed nil.
455 params = append(params, name+" *"+goType)
456 holder := fmt.Sprintf("arg%d", i)
457 data.ArgPreparations = append(data.ArgPreparations,
458 fmt.Sprintf("var %s wl.Object", holder),
459 fmt.Sprintf("if %s != nil {", name),
460 fmt.Sprintf("\t%s = %s", holder, name),
461 "}")
462 expr = "wl.ArgObject(" + holder + ")"
463
464 case "fd":
465 // Descriptors travel out of band: the body carries no value for
466 // them, and the descriptor list is attached to the request.
467 params = append(params, name+" int")
468 fdExprs = append(fdExprs, name)
469 expr = "wl.ArgFD()"
470
471 default:
472 params = append(params, name+" "+goType)
473 switch arg.Type {
474 case "int":
475 expr = "wl.ArgInt(" + name + ")"
476 case "uint":
477 expr = "wl.ArgUint(" + name + ")"
478 case "fixed":
479 expr = "wl.ArgFixed(" + name + ")"
480 case "string":
481 expr = "wl.ArgString(" + name + ")"
482 case "array":
483 expr = "wl.ArgArray(" + name + ")"
484 }
485 }
486
487 argSlots = append(argSlots, argSlot{expr: expr})
488 }
489
490 if newIDArg != nil {
491 childType, err := s.goTypeForArg(*newIDArg)
492 if err != nil {
493 return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
494 }
495 data.CreatesChild = true
496 data.ChildType = childType
497 if strings.Contains(childType, ".") {
498 data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
499 }
500 }
501
502 for _, slot := range argSlots {
503 if slot.newID {
504 argExprs = append(argExprs, "wl.ArgObject("+data.ChildVar+")")
505 continue
506 }
507 argExprs = append(argExprs, slot.expr)
508 }
509
510 data.Params = strings.Join(params, ", ")
511 args := ""
512 if len(argExprs) > 0 {
513 args = ", " + strings.Join(argExprs, ", ")
514 }
515 // Context.RequestArgs owns the lifecycle: destructors claim the proxy
516 // atomically, children are registered before the send and descriptors are
517 // closed only after a successful send.
518 data.ArgList = args
519 data.FDList = strings.Join(fdExprs, ", ")
520 data.HasFDs = len(fdExprs) > 0
521 if data.CreatesChild {
522 data.Results = "(*" + data.ChildType + ", error)"
523 data.ErrorReturn = "nil, err"
524 } else {
525 data.Results = "error"
526 data.ErrorReturn = "err"
527 }
528
529 return data, nil
530}
531
532func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
533 goName := s.toGoName(event.Name)
534 data := eventData{
535 ServerDestroy: event.Type == "destructor",
536 Name: event.Name,
537 GoName: goName,
538 Opcode: opcode,
539 HandlerField: "on" + goName,
540 HandlerAccessor: "handlersFor" + goName,
541 }
542
543 var params, args []string
544
545 for i, arg := range event.Args {
546 name := s.paramName(arg.Name, i)
547
548 switch arg.Type {
549 case "new_id":
550 childType, err := s.goTypeForArg(arg)
551 if err != nil {
552 return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
553 }
554 child := name + "Object"
555 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
556 if strings.Contains(childType, ".") {
557 factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
558 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
559 } else {
560 data.DecodeLines = append(data.DecodeLines,
561 fmt.Sprintf("%s := &%s{}", child, childType),
562 fmt.Sprintf("%s.SetContext(o.Context())", child),
563 )
564 }
565 data.DecodeLines = append(data.DecodeLines,
566 fmt.Sprintf("%s.SetID(%sID)", child, name),
567 fmt.Sprintf("%s.SetVersion(o.Version())", child),
568 fmt.Sprintf("o.Context().Register(%s)", child),
569 )
570 params = append(params, child+" *"+childType)
571 args = append(args, child)
572
573 case "object":
574 // Object references are reported as raw object IDs: the generated
575 // layer does not own a registry of foreign proxies.
576 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
577 params = append(params, name+"ID uint32")
578 args = append(args, name+"ID")
579
580 default:
581 if arg.Type == "fd" {
582 data.HasFD = true
583 data.FDNames = append(data.FDNames, name)
584 }
585 goType, decoder, err := s.scalarDecoder(arg)
586 if err != nil {
587 return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
588 }
589 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
590 params = append(params, name+" "+goType)
591 args = append(args, name)
592 }
593 }
594
595 data.HandlerParams = strings.Join(params, ", ")
596 data.HandlerArgs = strings.Join(args, ", ")
597 return data, nil
598}
599
600// scalarDecoder maps a simple argument to its Go type and event decoder.
601func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
602 switch arg.Type {
603 case "int":
604 return "int32", "Int32", nil
605 case "uint":
606 return "uint32", "Uint32", nil
607 case "fixed":
608 return "wl.Fixed", "Fixed", nil
609 case "string":
610 return "string", "String", nil
611 case "array":
612 return "[]byte", "Array", nil
613 case "fd":
614 return "*wl.OwnedFD", "FD", nil
615 }
616 return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
617}
618
619// goTypeForArg returns the Go type of an object-like argument. Protocol-local
620// interfaces map to generated types, explicit external references to their
621// mapped package, and bootstrap display/registry to transport wrappers.
622func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
623 switch arg.Type {
624 case "int":
625 return "int32", nil
626 case "uint":
627 return "uint32", nil
628 case "fixed":
629 return "wl.Fixed", nil
630 case "string":
631 return "string", nil
632 case "array":
633 return "[]byte", nil
634 case "fd":
635 return "int", nil
636 case "object", "new_id":
637 return s.goTypeForInterface(arg.Interface)
638 }
639 return "", fmt.Errorf("unsupported argument type %q", arg.Type)
640}
641
642func (s *Scanner) goTypeForInterface(iface string) (string, error) {
643 if iface == "" {
644 return "wl.BaseProxy", nil
645 }
646 // The XML package owns its own definitions even when the name starts wl_.
647 for _, local := range s.protocol.Interfaces {
648 if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
649 return s.toGoName(iface), nil
650 }
651 }
652 if bootstrapTypes[s.toGoName(iface)] {
653 return "wl." + s.toGoName(iface), nil
654 }
655 if _, ok := s.CrossPackage[iface]; ok {
656 return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
657 }
658 return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
659}
660
661// paramName turns a protocol argument name into a Go parameter name.
662func (s *Scanner) paramName(name string, index int) string {
663 if name == "" {
664 return fmt.Sprintf("arg%d", index)
665 }
666
667 parts := strings.Split(name, "_")
668 for i, part := range parts {
669 if part == "" {
670 continue
671 }
672 if i == 0 {
673 parts[i] = strings.ToLower(part[:1]) + part[1:]
674 continue
675 }
676 parts[i] = strings.ToUpper(part[:1]) + part[1:]
677 }
678 result := strings.Join(parts, "")
679 if result == "" {
680 return fmt.Sprintf("arg%d", index)
681 }
682 if goKeywords[result] {
683 return result + "Arg"
684 }
685 return result
686}
687
688func (s *Scanner) toGoName(name string) string {
689 name = strings.TrimPrefix(name, "zwlr_")
690 name = strings.TrimPrefix(name, "zwp_")
691 name = strings.TrimPrefix(name, "wl_")
692 name = strings.TrimSuffix(name, "_v1")
693 name = strings.TrimSuffix(name, "_v2")
694
695 parts := strings.Split(name, "_")
696 for i, part := range parts {
697 if part != "" {
698 parts[i] = strings.ToUpper(part[:1]) + part[1:]
699 }
700 }
701 return strings.Join(parts, "")
702}
703
704// constantName renders an enum entry as the unprefixed constant name.
705func constantName(enum, entry string) string {
706 return strings.Join(append(joinName(enum), joinName(entry)...), "_")
707}
708
709// qualifiedConstantName prefixes an enum entry constant with its interface
710// name. It is used for entries that several interfaces declare, which would
711// otherwise collide in the generated package.
712func qualifiedConstantName(iface, enum, entry string) string {
713 return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
714}
715
716// joinName upper-cases the underscore-separated parts of a protocol name.
717func joinName(value string) []string {
718 parts := strings.Split(value, "_")
719 out := make([]string, 0, len(parts))
720 for _, part := range parts {
721 if part != "" {
722 out = append(out, strings.ToUpper(part))
723 }
724 }
725 return out
726}
727
728// formatDescription renders an argument or request description as a single
729// documentation line.
730func (s *Scanner) formatDescription(desc *Description) string {
731 if desc == nil {
732 return ""
733 }
734
735 text := strings.TrimSpace(desc.Text)
736 if text == "" {
737 return desc.Summary
738 }
739
740 lines := strings.Split(text, "\n")
741 formatted := make([]string, 0, len(lines))
742 for _, line := range lines {
743 if line = strings.TrimSpace(line); line != "" {
744 formatted = append(formatted, line)
745 }
746 }
747 return strings.Join(formatted, " ")
748}