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