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}