main 908d8c0

gen.go RawHistoryFolder

1// Package gen generates the deliberately bounded libxkbcommon ABI subset.
2package gen
3
4import (
5	"fmt"
6	"go/format"
7	"os"
8	"regexp"
9	"strconv"
10	"strings"
11)
12
13var comments = regexp.MustCompile(`(?s)/\*.*?\*/|//[^\n]*`)
14
15func clean(s string) string { return comments.ReplaceAllString(s, "") }
16func declared(h, name string) bool {
17	re := regexp.MustCompile(`(?m)\b` + regexp.QuoteMeta(name) + `\s*\([^;{}]*\)\s*;`)
18	return re.MatchString(clean(h))
19}
20
21var def = regexp.MustCompile(`(?m)^\s*#define\s+(XKB_(?:KEY_|MOD_NAME_|VMOD_NAME_|LED_NAME_)[A-Za-z0-9_]+)\s+("[^"]*"|0x[0-9a-fA-F]+|[0-9]+)\b?`)
22var en = regexp.MustCompile(`(?s)enum\s+xkb_[a-z_]+\s*\{([^}]*)\}`)
23var item = regexp.MustCompile(`^\s*(XKB_[A-Z][A-Z0-9_]*)(?:\s*=\s*(.+))?$`)
24
25func constants(h string) string {
26	h = clean(h)
27	var b strings.Builder
28	for _, e := range en.FindAllStringSubmatch(h, -1) {
29		last := int64(-1)
30		for _, part := range strings.Split(e[1], ",") {
31			m := item.FindStringSubmatch(strings.TrimSpace(part))
32			if m == nil {
33				continue
34			}
35			expr := strings.TrimSpace(m[2])
36			if expr == "" {
37				last++
38				expr = strconv.FormatInt(last, 10)
39			} else if n, err := strconv.ParseInt(expr, 0, 64); err == nil {
40				last = n
41			} else if v := regexp.MustCompile(`^\(1 << (\d+)\)$`).FindStringSubmatch(expr); v != nil {
42				shift, _ := strconv.Atoi(v[1])
43				last = 1 << shift
44			} else {
45				continue
46			}
47			fmt.Fprintf(&b, "%s = %s\n", m[1], expr)
48		}
49	}
50	for _, m := range def.FindAllStringSubmatch(h, -1) {
51		fmt.Fprintf(&b, "%s = %s\n", m[1], m[2])
52	}
53	return b.String()
54}
55
56// Functions describes the bound ABI; each name is checked against the pinned headers.
57// Go memory is passed as unsafe.Pointer and converted inside the Syscall call
58// expression; native handles (contexts, keymaps, states) are uintptr.
59var Functions = []struct{ Name, Signature string }{
60	{"xkb_context_new", "func(uint32) uintptr"}, {"xkb_context_ref", "func(uintptr) uintptr"}, {"xkb_context_unref", "func(uintptr)"},
61	{"xkb_keymap_new_from_names", "func(uintptr, unsafe.Pointer, uint32) uintptr"}, {"xkb_keymap_new_from_string", "func(uintptr, unsafe.Pointer, uint32, uint32) uintptr"}, {"xkb_keymap_new_from_buffer", "func(uintptr, unsafe.Pointer, uintptr, uint32, uint32) uintptr"},
62	{"xkb_keymap_ref", "func(uintptr) uintptr"}, {"xkb_keymap_unref", "func(uintptr)"}, {"xkb_keymap_get_as_string", "func(uintptr,uint32) uintptr"},
63	{"xkb_keymap_mod_get_index", "func(uintptr,unsafe.Pointer) uint32"}, {"xkb_keymap_key_repeats", "func(uintptr,uint32) int32"}, {"xkb_keymap_num_layouts", "func(uintptr) uint32"},
64	{"xkb_state_new", "func(uintptr) uintptr"}, {"xkb_state_ref", "func(uintptr) uintptr"}, {"xkb_state_unref", "func(uintptr)"},
65	{"xkb_state_update_mask", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32) uint32"},
66	{"xkb_state_serialize_layout", "func(uintptr,uint32) uint32"}, {"xkb_state_serialize_mods", "func(uintptr,uint32) uint32"},
67	{"xkb_state_mod_name_is_active", "func(uintptr,unsafe.Pointer,uint32) int32"}, {"xkb_state_mod_index_is_active", "func(uintptr,uint32,uint32) int32"},
68	{"xkb_state_key_get_one_sym", "func(uintptr,uint32) uint32"}, {"xkb_state_key_get_utf8", "func(uintptr,uint32,unsafe.Pointer,uintptr) int32"},
69	{"xkb_keysym_get_name", "func(uint32,unsafe.Pointer,uintptr) int32"}, {"xkb_keysym_from_name", "func(unsafe.Pointer,uint32) uint32"},
70	{"xkb_compose_table_new_from_locale", "func(uintptr,unsafe.Pointer,uint32) uintptr"}, {"xkb_compose_table_ref", "func(uintptr) uintptr"}, {"xkb_compose_table_unref", "func(uintptr)"},
71	{"xkb_compose_state_new", "func(uintptr,uint32) uintptr"}, {"xkb_compose_state_ref", "func(uintptr) uintptr"}, {"xkb_compose_state_unref", "func(uintptr)"},
72	{"xkb_compose_state_feed", "func(uintptr,uint32) uint32"}, {"xkb_compose_state_get_status", "func(uintptr) uint32"},
73	{"xkb_compose_state_reset", "func(uintptr)"}, {"xkb_compose_state_get_utf8", "func(uintptr,unsafe.Pointer,uintptr) int32"}, {"xkb_compose_state_get_one_sym", "func(uintptr) uint32"},
74}
75
76func field(name string) string { return strings.ToUpper(name[:1]) + name[1:] }
77
78// Only fixed-width integer-class values reach Syscall6/Syscall15: floats, bools,
79// varargs and structs would be silently mis-passed, so the generator rejects them.
80// unsafe.Pointer is accepted for parameters only (Go memory handed to C);
81// uintptr is for C-owned handles and sizes.
82var allowedParams = map[string]bool{"uintptr": true, "uint32": true, "int32": true, "unsafe.Pointer": true}
83var allowedReturns = map[string]bool{"": true, "uintptr": true, "uint32": true, "int32": true}
84
85// method renders an exported, allocation-free wrapper calling the resolved
86// address through purego.Syscall6 (up to 6 arguments) or Syscall15.
87func method(name, sig string) (string, error) {
88	rest, ok := strings.CutPrefix(sig, "func(")
89	if !ok {
90		return "", fmt.Errorf("bad signature for %s: %s", name, sig)
91	}
92	params, ret, _ := strings.Cut(rest, ")")
93	ret = strings.TrimSpace(ret)
94	var types []string
95	if strings.TrimSpace(params) != "" {
96		for _, p := range strings.Split(params, ",") {
97			types = append(types, strings.TrimSpace(p))
98		}
99	}
100	if len(types) > 15 {
101		return "", fmt.Errorf("%s: more than 15 arguments", name)
102	}
103	if !allowedReturns[ret] {
104		return "", fmt.Errorf("%s: unsupported return type %q", name, ret)
105	}
106	var decl, args []string
107	for i, t := range types {
108		if !allowedParams[t] {
109			return "", fmt.Errorf("%s: unsupported parameter type %q", name, t)
110		}
111		decl = append(decl, fmt.Sprintf("a%d %s", i+1, t))
112		// The uintptr conversion must stay inside the Syscall call expression so
113		// //go:uintptrescapes keeps unsafe.Pointer arguments alive and unmoved.
114		if t == "uintptr" {
115			args = append(args, fmt.Sprintf("a%d", i+1))
116		} else {
117			args = append(args, fmt.Sprintf("uintptr(a%d)", i+1))
118		}
119	}
120	call, size := "purego.Syscall6", 6
121	if len(types) > 6 {
122		call, size = "purego.Syscall15", 15
123	}
124	for len(args) < size {
125		args = append(args, "0")
126	}
127	var b strings.Builder
128	fmt.Fprintf(&b, "\n// %s calls %s without allocating.\nfunc (f *Funcs) %s(%s) %s {\n", field(name), name, field(name), strings.Join(decl, ", "), ret)
129	switch ret {
130	case "":
131		fmt.Fprintf(&b, "%s(f.%s, %s)\n", call, name, strings.Join(args, ", "))
132	case "uintptr":
133		fmt.Fprintf(&b, "r, _, _ := %s(f.%s, %s)\nreturn r\n", call, name, strings.Join(args, ", "))
134	default:
135		fmt.Fprintf(&b, "r, _, _ := %s(f.%s, %s)\nreturn %s(r)\n", call, name, strings.Join(args, ", "), ret)
136	}
137	b.WriteString("}\n")
138	return b.String(), nil
139}
140func Generate(root string) error {
141	h1, err := os.ReadFile(root + "/upstream/xkbcommon.h")
142	if err != nil {
143		return err
144	}
145	h2, err := os.ReadFile(root + "/upstream/xkbcommon-compose.h")
146	if err != nil {
147		return err
148	}
149	hdr := string(h1) + "\n" + string(h2)
150	var b strings.Builder
151	b.WriteString("// Code generated by go generate; DO NOT EDIT.\npackage raw\n\nimport (\n\"unsafe\"\n\n\"github.com/bnema/purego\"\n)\n\n")
152	b.WriteString("// Opaque C handles and scalar typedefs.\ntype Context uintptr\ntype Keymap uintptr\ntype State uintptr\ntype ComposeTable uintptr\ntype ComposeState uintptr\ntype Keycode uint32\ntype Keysym uint32\ntype LayoutIndex uint32\ntype ModIndex uint32\ntype ModMask uint32\ntype LayoutMask uint32\n")
153	b.WriteString("// RuleNames matches struct xkb_rule_names (five const char pointers). The\n// fields are unsafe.Pointer so the GC sees the NUL-terminated strings they reference.\ntype RuleNames struct { Rules, Model, Layout, Variant, Options unsafe.Pointer }\n")
154	b.WriteString("const (\n" + constants(hdr))
155	for _, f := range []string{"xkbcommon-keysyms.h", "xkbcommon-names.h"} {
156		data, e := os.ReadFile(root + "/upstream/" + f)
157		if e != nil {
158			return e
159		}
160		b.WriteString(constants(string(data)))
161	}
162	b.WriteString(")\n\n// Funcs holds resolved library function addresses. Each function is invoked\n// through purego.Syscall6/Syscall15 (fixed arity, no reflection, no allocation).\ntype Funcs struct {\n")
163	for _, f := range Functions {
164		if !declared(hdr, f.Name) {
165			return fmt.Errorf("missing pinned declaration: %s", f.Name)
166		}
167		fmt.Fprintf(&b, "%s uintptr\n", f.Name)
168	}
169	b.WriteString("}\n\n// Symbols is the complete required symbol set.\nvar Symbols = []string{\n")
170	for _, f := range Functions {
171		fmt.Fprintf(&b, "%q,\n", f.Name)
172	}
173	b.WriteString("}\n\n// Register stores prevalidated symbol addresses in a Funcs value.\nfunc Register(f *Funcs, addresses map[string]uintptr) {\n")
174	for _, f := range Functions {
175		fmt.Fprintf(&b, "f.%s = addresses[%q]\n", f.Name, f.Name)
176	}
177	b.WriteString("}\n")
178	for _, f := range Functions {
179		m, err := method(f.Name, f.Signature)
180		if err != nil {
181			return err
182		}
183		b.WriteString(m)
184	}
185	s := b.String()
186	formatted, err := format.Source([]byte(s))
187	if err != nil {
188		return fmt.Errorf("format generated file: %w", err)
189	}
190	return os.WriteFile(root+"/raw/generated.go", formatted, 0644)
191}

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 2

↑↓ moveenter openesc close