v0.3.0 96f3aeb

scanner.go RawHistoryFolder

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}

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 8

↑↓ moveenter openesc close