detached 1a6c6c9

scanner.go RawHistoryFolder

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}

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