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 ImportBlock string
166 Constants []constantData
167 Interfaces []interfaceData
168}
169
170type interfaceData struct {
171 Name string
172 GoName string
173 Version int
174 HasEvents bool
175 SignatureCases []signatureCase
176 Requests []requestData
177 Events []eventData
178}
179
180type signatureCase struct {
181 Opcode int
182 Signature string
183}
184type requestData struct {
185 Name string
186 GoName string
187 Doc string
188 Opcode int
189 Destructor bool
190 Since int
191 Params string
192 Results string
193 ArgPreparations []string
194 CreatesChild bool
195 ChildVar string
196 ChildType string
197 ChildFactory string
198 ErrorReturn string
199 HasFDs bool
200 FDList string
201 ArgList string
202}
203
204// argSlot is one positional argument of a request, in declaration order. A
205// new_id slot is filled in once the child object has been created.
206type argSlot struct {
207 newID bool
208 expr string
209}
210
211type eventData struct {
212 HasFD bool
213 ServerDestroy bool
214 FDNames []string
215 Name string
216 GoName string
217 Opcode int
218 HandlerField string
219 HandlerAccessor string
220 HandlerParams string
221 HandlerArgs string
222 DecodeLines []string
223}
224
225type constantData struct {
226 Name string
227 Value string
228 Type string
229 Enum string
230 Entry string
231}
232
233// Only display and registry have handwritten bootstrap bindings.
234var bootstrapTypes = map[string]bool{
235 "Display": true,
236 "Registry": true,
237}
238
239// goKeywords are avoided as parameter names because generated identifiers
240// must not collide with the language.
241var goKeywords = map[string]bool{
242 "break": true, "case": true, "chan": true, "const": true, "continue": true,
243 "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
244 "func": true, "go": true, "goto": true, "if": true, "import": true,
245 "interface": true, "map": true, "package": true, "range": true, "return": true,
246 "select": true, "struct": true, "switch": true, "type": true, "var": true,
247}
248
249func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
250 data := templateData{
251 Package: packageName,
252 Protocol: s.protocol.Name,
253 Source: s.source,
254 }
255
256 imports := []string{TransportImport}
257 usedCross := make(map[string]bool)
258 for _, candidate := range s.protocol.Interfaces {
259 for _, req := range candidate.Requests {
260 for _, arg := range req.Args {
261 if arg.Type == "object" || arg.Type == "new_id" {
262 usedCross[arg.Interface] = true
263 }
264 }
265 }
266 for _, ev := range candidate.Events {
267 for _, arg := range ev.Args {
268 // Event object references are decoded as IDs, not imported proxies.
269 if arg.Type == "new_id" {
270 usedCross[arg.Interface] = true
271 }
272 }
273 }
274 }
275 for iface, path := range s.CrossPackage {
276 local := false
277 for _, candidate := range s.protocol.Interfaces {
278 if candidate.Name == iface {
279 local = true
280 break
281 }
282 }
283 if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
284 continue
285 }
286 alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
287 imports = append(imports, alias+" "+path)
288 }
289 sort.Strings(imports)
290 stdImports := []string{}
291 needsSync := false
292
293 // Enum constants are package-level declarations, so two interfaces that
294 // declare the same enum entry (for example xdg_surface.error.invalid_size
295 // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
296 // occurs in more than one interface are qualified with their interface
297 // name below; unique names keep their historical, unprefixed form.
298 enumEntryCounts := map[string]int{}
299 for _, iface := range s.protocol.Interfaces {
300 for _, enum := range iface.Enums {
301 for _, entry := range enum.Entries {
302 enumEntryCounts[constantName(enum.Name, entry.Name)]++
303 }
304 }
305 }
306
307 for _, iface := range s.protocol.Interfaces {
308 if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
309 continue
310 }
311 ifaceData, err := s.processInterface(iface)
312 if err != nil {
313 return data, err
314 }
315 data.Interfaces = append(data.Interfaces, ifaceData)
316 if len(iface.Events) > 0 {
317 needsSync = true
318 }
319
320 for _, enum := range iface.Enums {
321 for _, entry := range enum.Entries {
322 name := constantName(enum.Name, entry.Name)
323 if enumEntryCounts[name] > 1 {
324 name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
325 }
326 // Enum values are emitted untyped: the same enum travels as a uint
327 // in one protocol and as an int in another, and an untyped constant
328 // assigns to either without casts at the call site.
329 data.Constants = append(data.Constants, constantData{
330 Name: name,
331 Value: entry.Value,
332 Enum: enum.Name,
333 Entry: entry.Name,
334 })
335 }
336 }
337 }
338
339 // Handlers are registered and dispatched from different goroutines, so an
340 // interface with events needs a mutex.
341 if needsSync {
342 stdImports = append(stdImports, "sync")
343 }
344
345 var block strings.Builder
346 block.WriteString("import (\n")
347 for _, imp := range stdImports {
348 fmt.Fprintf(&block, "\t%q\n", imp)
349 }
350 if len(stdImports) > 0 && len(imports) > 0 {
351 block.WriteString("\n")
352 }
353 for _, imp := range imports {
354 if strings.Contains(imp, " ") {
355 parts := strings.SplitN(imp, " ", 2)
356 fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
357 } else {
358 fmt.Fprintf(&block, "\t%q\n", imp)
359 }
360 }
361 block.WriteString(")")
362 data.ImportBlock = block.String()
363
364 return data, nil
365}
366
367func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
368 data := interfaceData{
369 Name: iface.Name,
370 GoName: s.toGoName(iface.Name),
371 Version: iface.Version,
372 HasEvents: len(iface.Events) > 0,
373 }
374
375 for i, req := range iface.Requests {
376 request, err := s.processRequest(iface, req, i)
377 if err != nil {
378 return data, err
379 }
380 data.Requests = append(data.Requests, request)
381 }
382
383 for i, event := range iface.Events {
384 var sig strings.Builder
385 for _, arg := range event.Args {
386 sig.WriteString(arg.Type)
387 sig.WriteByte(',')
388 }
389 data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
390 eventData, err := s.processEvent(event, i)
391 if iface.Name == "wl_callback" && event.Name == "done" {
392 eventData.ServerDestroy = true
393 }
394 if err != nil {
395 return data, err
396 }
397 data.Events = append(data.Events, eventData)
398 }
399
400 return data, nil
401}
402
403func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
404 data := requestData{
405 Name: req.Name,
406 GoName: s.toGoName(req.Name),
407 Doc: s.formatDescription(req.Description),
408 Opcode: opcode,
409 Destructor: req.Type == "destructor",
410 Since: req.Since,
411 ChildVar: "child",
412 }
413
414 var (
415 params []string
416 argSlots []argSlot
417 argExprs []string
418 fdExprs []string
419 newIDArg *Arg
420 )
421
422 for i, arg := range req.Args {
423 if arg.Type == "new_id" {
424 if newIDArg != nil {
425 return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
426 }
427 newIDArg = &arg
428 // The child object is created later, but it keeps its declared
429 // position: the wire order is the argument order.
430 argSlots = append(argSlots, argSlot{newID: true})
431 continue
432 }
433
434 name := s.paramName(arg.Name, i)
435 goType, err := s.goTypeForArg(arg)
436 if err != nil {
437 return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
438 }
439
440 expr := name
441 switch arg.Type {
442 case "object":
443 // Objects are pointer types, and travel as wl.Object so a nil
444 // pointer is never sent as a non-nil interface holding a typed nil.
445 params = append(params, name+" *"+goType)
446 holder := fmt.Sprintf("arg%d", i)
447 data.ArgPreparations = append(data.ArgPreparations,
448 fmt.Sprintf("var %s wl.Object", holder),
449 fmt.Sprintf("if %s != nil {", name),
450 fmt.Sprintf("\t%s = %s", holder, name),
451 "}")
452 expr = holder
453
454 case "fd":
455 // Descriptors travel out of band: the body carries no value for
456 // them, and the descriptor list is attached to the request.
457 params = append(params, name+" int")
458 fdExprs = append(fdExprs, name)
459 expr = "uintptr(" + name + ")"
460
461 default:
462 params = append(params, name+" "+goType)
463 }
464
465 argSlots = append(argSlots, argSlot{expr: expr})
466 }
467
468 if newIDArg != nil {
469 childType, err := s.goTypeForArg(*newIDArg)
470 if err != nil {
471 return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
472 }
473 data.CreatesChild = true
474 data.ChildType = childType
475 if strings.Contains(childType, ".") {
476 data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
477 }
478 }
479
480 for _, slot := range argSlots {
481 if slot.newID {
482 argExprs = append(argExprs, data.ChildVar)
483 continue
484 }
485 argExprs = append(argExprs, slot.expr)
486 }
487
488 data.Params = strings.Join(params, ", ")
489 args := ""
490 if len(argExprs) > 0 {
491 args = ", " + strings.Join(argExprs, ", ")
492 }
493 // Context.Request owns the lifecycle: destructors claim the proxy
494 // atomically, children are registered before the send and descriptors are
495 // closed only after a successful send.
496 data.ArgList = args
497 data.FDList = strings.Join(fdExprs, ", ")
498 data.HasFDs = len(fdExprs) > 0
499 if data.CreatesChild {
500 data.Results = "(*" + data.ChildType + ", error)"
501 data.ErrorReturn = "nil, err"
502 } else {
503 data.Results = "error"
504 data.ErrorReturn = "err"
505 }
506
507 return data, nil
508}
509
510func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
511 goName := s.toGoName(event.Name)
512 data := eventData{
513 ServerDestroy: event.Type == "destructor",
514 Name: event.Name,
515 GoName: goName,
516 Opcode: opcode,
517 HandlerField: "on" + goName,
518 HandlerAccessor: "handlersFor" + goName,
519 }
520
521 var params, args []string
522
523 for i, arg := range event.Args {
524 name := s.paramName(arg.Name, i)
525
526 switch arg.Type {
527 case "new_id":
528 childType, err := s.goTypeForArg(arg)
529 if err != nil {
530 return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
531 }
532 child := name + "Object"
533 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
534 if strings.Contains(childType, ".") {
535 factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
536 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
537 } else {
538 data.DecodeLines = append(data.DecodeLines,
539 fmt.Sprintf("%s := &%s{}", child, childType),
540 fmt.Sprintf("%s.SetContext(o.Context())", child),
541 )
542 }
543 data.DecodeLines = append(data.DecodeLines,
544 fmt.Sprintf("%s.SetID(%sID)", child, name),
545 fmt.Sprintf("%s.SetVersion(o.Version())", child),
546 fmt.Sprintf("o.Context().Register(%s)", child),
547 )
548 params = append(params, child+" *"+childType)
549 args = append(args, child)
550
551 case "object":
552 // Object references are reported as raw object IDs: the generated
553 // layer does not own a registry of foreign proxies.
554 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
555 params = append(params, name+"ID uint32")
556 args = append(args, name+"ID")
557
558 default:
559 if arg.Type == "fd" {
560 data.HasFD = true
561 data.FDNames = append(data.FDNames, name)
562 }
563 goType, decoder, err := s.scalarDecoder(arg)
564 if err != nil {
565 return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
566 }
567 data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
568 params = append(params, name+" "+goType)
569 args = append(args, name)
570 }
571 }
572
573 data.HandlerParams = strings.Join(params, ", ")
574 data.HandlerArgs = strings.Join(args, ", ")
575 return data, nil
576}
577
578// scalarDecoder maps a simple argument to its Go type and event decoder.
579func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
580 switch arg.Type {
581 case "int":
582 return "int32", "Int32", nil
583 case "uint":
584 return "uint32", "Uint32", nil
585 case "fixed":
586 return "wl.Fixed", "Fixed", nil
587 case "string":
588 return "string", "String", nil
589 case "array":
590 return "[]byte", "Array", nil
591 case "fd":
592 return "*wl.OwnedFD", "FD", nil
593 }
594 return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
595}
596
597// goTypeForArg returns the Go type of an object-like argument. Protocol-local
598// interfaces map to generated types, explicit external references to their
599// mapped package, and bootstrap display/registry to transport wrappers.
600func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
601 switch arg.Type {
602 case "int":
603 return "int32", nil
604 case "uint":
605 return "uint32", nil
606 case "fixed":
607 return "wl.Fixed", nil
608 case "string":
609 return "string", nil
610 case "array":
611 return "[]byte", nil
612 case "fd":
613 return "int", nil
614 case "object", "new_id":
615 return s.goTypeForInterface(arg.Interface)
616 }
617 return "", fmt.Errorf("unsupported argument type %q", arg.Type)
618}
619
620func (s *Scanner) goTypeForInterface(iface string) (string, error) {
621 if iface == "" {
622 return "wl.BaseProxy", nil
623 }
624 // The XML package owns its own definitions even when the name starts wl_.
625 for _, local := range s.protocol.Interfaces {
626 if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
627 return s.toGoName(iface), nil
628 }
629 }
630 if bootstrapTypes[s.toGoName(iface)] {
631 return "wl." + s.toGoName(iface), nil
632 }
633 if _, ok := s.CrossPackage[iface]; ok {
634 return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
635 }
636 return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
637}
638
639// paramName turns a protocol argument name into a Go parameter name.
640func (s *Scanner) paramName(name string, index int) string {
641 if name == "" {
642 return fmt.Sprintf("arg%d", index)
643 }
644
645 parts := strings.Split(name, "_")
646 for i, part := range parts {
647 if part == "" {
648 continue
649 }
650 if i == 0 {
651 parts[i] = strings.ToLower(part[:1]) + part[1:]
652 continue
653 }
654 parts[i] = strings.ToUpper(part[:1]) + part[1:]
655 }
656 result := strings.Join(parts, "")
657 if result == "" {
658 return fmt.Sprintf("arg%d", index)
659 }
660 if goKeywords[result] {
661 return result + "Arg"
662 }
663 return result
664}
665
666func (s *Scanner) toGoName(name string) string {
667 name = strings.TrimPrefix(name, "zwlr_")
668 name = strings.TrimPrefix(name, "zwp_")
669 name = strings.TrimPrefix(name, "wl_")
670 name = strings.TrimSuffix(name, "_v1")
671 name = strings.TrimSuffix(name, "_v2")
672
673 parts := strings.Split(name, "_")
674 for i, part := range parts {
675 if part != "" {
676 parts[i] = strings.ToUpper(part[:1]) + part[1:]
677 }
678 }
679 return strings.Join(parts, "")
680}
681
682// constantName renders an enum entry as the unprefixed constant name.
683func constantName(enum, entry string) string {
684 return strings.Join(append(joinName(enum), joinName(entry)...), "_")
685}
686
687// qualifiedConstantName prefixes an enum entry constant with its interface
688// name. It is used for entries that several interfaces declare, which would
689// otherwise collide in the generated package.
690func qualifiedConstantName(iface, enum, entry string) string {
691 return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
692}
693
694// joinName upper-cases the underscore-separated parts of a protocol name.
695func joinName(value string) []string {
696 parts := strings.Split(value, "_")
697 out := make([]string, 0, len(parts))
698 for _, part := range parts {
699 if part != "" {
700 out = append(out, strings.ToUpper(part))
701 }
702 }
703 return out
704}
705
706// formatDescription renders an argument or request description as a single
707// documentation line.
708func (s *Scanner) formatDescription(desc *Description) string {
709 if desc == nil {
710 return ""
711 }
712
713 text := strings.TrimSpace(desc.Text)
714 if text == "" {
715 return desc.Summary
716 }
717
718 lines := strings.Split(text, "\n")
719 formatted := make([]string, 0, len(lines))
720 for _, line := range lines {
721 if line = strings.TrimSpace(line); line != "" {
722 formatted = append(formatted, line)
723 }
724 }
725 return strings.Join(formatted, " ")
726}