detached 7790fe5

Merge pull request #3 from bnema/perf/syscall-hotpath

perf: allocation-free libxkbcommon dispatch through fixed-arity syscalls
commit
7790fe57643e3bd6d5ce81eee893cf8eee33d3a3
author
bnema · · 2026-10-01 18:53 UTC
committer
GitHub · · 2026-10-01 18:53 UTC
merge of
0e37717 + ac65b63 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.2.0
9 files · +765 -110
A .github/workflows/ci.yml +36 -0
@@ -0,0 +1,36 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+concurrency:
7+ group: ci-${{ github.ref }}
8+ cancel-in-progress: true
9+jobs:
10+ # Arch ships the libxkbcommon the bindings are pinned against; the live
11+ # tests load it through purego. gcc is for the race detector.
12+ check:
13+ runs-on: ubuntu-latest
14+ container:
15+ image: archlinux:latest
16+ options: --init
17+ env:
18+ LANG: en_US.UTF-8
19+ steps:
20+ # Compose tests need the en_US.UTF-8 locale and its Compose file (libx11).
21+ - run: |
22+ pacman -Syu --noconfirm --needed git go gcc glibc-locales libxkbcommon libx11 xkeyboard-config
23+ echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen
24+ locale-gen
25+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26+ with:
27+ persist-credentials: false
28+ - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
29+ - run: CGO_ENABLED=0 go vet ./...
30+ - run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...
31+ - run: CGO_ENABLED=0 go test -count=1 ./...
32+ - run: CGO_ENABLED=1 go test -race -count=1 ./...
33+ - name: generated code is current
34+ run: |
35+ GOWORK=off go generate ./...
36+ git diff --exit-code
@@ -0,0 +1,36 @@
1+name: CI
2+on:
3+ push:
4+ branches: [main]
5+ pull_request:
6+concurrency:
7+ group: ci-${{ github.ref }}
8+ cancel-in-progress: true
9+jobs:
10+ # Arch ships the libxkbcommon the bindings are pinned against; the live
11+ # tests load it through purego. gcc is for the race detector.
12+ check:
13+ runs-on: ubuntu-latest
14+ container:
15+ image: archlinux:latest
16+ options: --init
17+ env:
18+ LANG: en_US.UTF-8
19+ steps:
20+ # Compose tests need the en_US.UTF-8 locale and its Compose file (libx11).
21+ - run: |
22+ pacman -Syu --noconfirm --needed git go gcc glibc-locales libxkbcommon libx11 xkeyboard-config
23+ echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen
24+ locale-gen
25+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26+ with:
27+ persist-credentials: false
28+ - run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
29+ - run: CGO_ENABLED=0 go vet ./...
30+ - run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...
31+ - run: CGO_ENABLED=0 go test -count=1 ./...
32+ - run: CGO_ENABLED=1 go test -race -count=1 ./...
33+ - name: generated code is current
34+ run: |
35+ GOWORK=off go generate ./...
36+ git diff --exit-code
M README.md +17 -2
@@ -1,6 +1,6 @@
11 # purego-xkbcommon
22
3-Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function registrations. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`.
3+Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function bindings. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`.
44
55 ```go
66 ctx, err := xkbcommon.NewContext()
@@ -19,4 +19,19 @@ Use `NewKeymapFD(fd, size)` for Wayland `xkb_v1` keymaps; it copies exactly `siz
1919
2020 `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use.
2121
22-Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and `RegisterFunc` with cgo disabled.
22+Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and the fixed-arity `Syscall6`/`Syscall15` with cgo disabled.
23+
24+## Allocation-free hot path
25+
26+`raw.Funcs` holds symbol addresses resolved once by `Available`; every generated method calls `purego.Syscall6` (or `Syscall15` for more than six arguments) directly, so no reflection or return boxing happens per call. Per-key calls — `State.KeySym`, `State.UTF8Into`, `State.UpdateMask`, `State.Layout`, `State.Mods`, `State.ModIndexActive`, `State.ModNameActive`, `Keymap.KeyRepeats`, `KeysymNameInto` and the `ComposeState` `Feed`/`Status`/`KeySym`/`UTF8Into`/`Reset` methods — perform 0 allocations (asserted by `TestHotPathAllocs`). The `*Into` methods stay allocation-free only with a long-lived `dst` reused across calls: a buffer passed to C escapes, so a fresh local array costs one allocation per call. The string-returning `UTF8` and `KeysymName` still allocate their result.
27+
28+
29+## v0.2.0: `raw` API change
30+
31+Only the `raw` package changed incompatibly; the root `xkbcommon` package is source-compatible (it gains `State.ModNameActive`, `State.ModIndexActive`, `Keymap.KeyRepeats` and `KeysymNameInto`).
32+
33+- `raw.Funcs` fields are no longer `func` values. Each function is now a method with the same exported name (`f.Xkb_state_key_get_one_sym(...)`), and the struct holds unexported symbol addresses.
34+- Pointer parameters (strings, buffers, `*RuleNames`) are `unsafe.Pointer` instead of `uintptr`, and `RuleNames` fields are `unsafe.Pointer`. Pass the pointer directly; never pre-convert it to `uintptr`, which would not keep the memory alive.
35+- `raw.Register(f, addresses, register)` is now `raw.Register(f, addresses)`; there is no `purego.RegisterFunc` hook.
36+- A zero `raw.Funcs` is unusable: calling any method panics (`purego: fn is nil`) until `Register` has filled it. `xkbcommon.Available` does this for the package's own loader.
37+- The generator only accepts `uintptr`, `uint32`, `int32` and `unsafe.Pointer` parameters and `uintptr`, `uint32`, `int32` or no return value, and rejects floats, bools and varargs.
@@ -1,6 +1,6 @@
1 # purego-xkbcommon1 # purego-xkbcommon
2 2
3-Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function registrations. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`.3+Go 1.27 bindings for Linux `libxkbcommon.so.0`, without cgo or X11. The safe package owns native references; the `raw` package exposes generated ABI constants, types, and function bindings. Install libxkbcommon at runtime and xkeyboard-config for rules-based keymaps. The loader reports missing libraries and required symbols via `Available`.
4 4
5 ```go5 ```go
6 ctx, err := xkbcommon.NewContext()6 ctx, err := xkbcommon.NewContext()
@@ -19,4 +19,19 @@ Use `NewKeymapFD(fd, size)` for Wayland `xkb_v1` keymaps; it copies exactly `siz
19 19
20 `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use.20 `State.UTF8Into` and `ComposeState.UTF8Into` write into a caller-owned buffer of 1..`MaxUTF8Buffer` bytes and create no Go string. On `io.ErrShortBuffer` the whole buffer is wiped and n+1 bytes are needed; the caller wipes the buffer after use.
21 21
22-Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and `RegisterFunc` with cgo disabled.22+Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and `SHA256SUMS` identify their exact source. Run `GOWORK=off go generate ./...` to regenerate `raw/generated.go`; consumers do not run generation. The generator checks the selected ABI declarations against the headers. `github.com/bnema/purego v0.13.0-bnema.1` matches purego-vulkan and supports `Dlopen`, `Dlsym`, and the fixed-arity `Syscall6`/`Syscall15` with cgo disabled.
23+
24+## Allocation-free hot path
25+
26+`raw.Funcs` holds symbol addresses resolved once by `Available`; every generated method calls `purego.Syscall6` (or `Syscall15` for more than six arguments) directly, so no reflection or return boxing happens per call. Per-key calls — `State.KeySym`, `State.UTF8Into`, `State.UpdateMask`, `State.Layout`, `State.Mods`, `State.ModIndexActive`, `State.ModNameActive`, `Keymap.KeyRepeats`, `KeysymNameInto` and the `ComposeState` `Feed`/`Status`/`KeySym`/`UTF8Into`/`Reset` methods — perform 0 allocations (asserted by `TestHotPathAllocs`). The `*Into` methods stay allocation-free only with a long-lived `dst` reused across calls: a buffer passed to C escapes, so a fresh local array costs one allocation per call. The string-returning `UTF8` and `KeysymName` still allocate their result.
27+
28+
29+## v0.2.0: `raw` API change
30+
31+Only the `raw` package changed incompatibly; the root `xkbcommon` package is source-compatible (it gains `State.ModNameActive`, `State.ModIndexActive`, `Keymap.KeyRepeats` and `KeysymNameInto`).
32+
33+- `raw.Funcs` fields are no longer `func` values. Each function is now a method with the same exported name (`f.Xkb_state_key_get_one_sym(...)`), and the struct holds unexported symbol addresses.
34+- Pointer parameters (strings, buffers, `*RuleNames`) are `unsafe.Pointer` instead of `uintptr`, and `RuleNames` fields are `unsafe.Pointer`. Pass the pointer directly; never pre-convert it to `uintptr`, which would not keep the memory alive.
35+- `raw.Register(f, addresses, register)` is now `raw.Register(f, addresses)`; there is no `purego.RegisterFunc` hook.
36+- A zero `raw.Funcs` is unusable: calling any method panics (`purego: fn is nil`) until `Register` has filled it. `xkbcommon.Available` does this for the package's own loader.
37+- The generator only accepts `uintptr`, `uint32`, `int32` and `unsafe.Pointer` parameters and `uintptr`, `uint32`, `int32` or no return value, and rejects floats, bools and varargs.
A alloc_test.go +221 -0
@@ -0,0 +1,221 @@
1+package xkbcommon
2+
3+import (
4+ "bytes"
5+ "errors"
6+ "io"
7+ "os"
8+ "runtime"
9+ "strings"
10+ "testing"
11+
12+ "github.com/bnema/purego-xkbcommon/raw"
13+)
14+
15+// fdKeymap compiles a pinned Wayland-format keymap from testdata.
16+func fdKeymap(t *testing.T, c *Context, layout string) *Keymap {
17+ t.Helper()
18+ text, e := os.ReadFile("testdata/keymaps/" + layout + ".xkb")
19+ if e != nil {
20+ t.Fatal(e)
21+ }
22+ f := keymapFile(t, append(text, 0))
23+ k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1)
24+ if e != nil {
25+ t.Fatal(e)
26+ }
27+ t.Cleanup(func() { k.Close() })
28+ return k
29+}
30+
31+func assertNoAllocs(t *testing.T, name string, fn func()) {
32+ t.Helper()
33+ fn() // warm up lazy runtime state
34+ if n := testing.AllocsPerRun(200, fn); n != 0 {
35+ t.Errorf("%s: %v allocs/op, want 0", name, n)
36+ } else {
37+ t.Logf("%s: 0 allocs/op", name)
38+ }
39+}
40+
41+func TestHotPathAllocs(t *testing.T) {
42+ c := live(t)
43+ k := fdKeymap(t, c, "fr")
44+ s := state(t, k)
45+ code := WaylandKeycode(16)
46+ var buf [MaxUTF8Buffer]byte
47+ var fail error
48+ check := func(e error) {
49+ if e != nil {
50+ fail = e
51+ }
52+ }
53+
54+ table, e := c.NewComposeTable("en_US.UTF-8")
55+ if e != nil {
56+ t.Fatal(e)
57+ }
58+ defer table.Close()
59+ compose, e := table.NewState()
60+ if e != nil {
61+ t.Fatal(e)
62+ }
63+ defer compose.Close()
64+
65+ assertNoAllocs(t, "State.KeySym", func() { _, e := s.KeySym(code); check(e) })
66+ assertNoAllocs(t, "State.UTF8Into", func() { _, e := s.UTF8Into(code, buf[:]); check(e) })
67+ assertNoAllocs(t, "State.UpdateMask", func() { _, e := s.UpdateMask(1, 0, 2, 0, 0, 0); check(e) })
68+ assertNoAllocs(t, "State.Layout", func() { _, e := s.Layout(); check(e) })
69+ assertNoAllocs(t, "State.Mods", func() { _, e := s.Mods(); check(e) })
70+ assertNoAllocs(t, "State.ModIndexActive", func() { _, e := s.ModIndexActive(0); check(e) })
71+ assertNoAllocs(t, "State.ModNameActive", func() { _, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); check(e) })
72+ assertNoAllocs(t, "Keymap.KeyRepeats", func() { _, e := k.KeyRepeats(code); check(e) })
73+ assertNoAllocs(t, "KeysymNameInto", func() { _, e := KeysymNameInto(raw.XKB_KEY_a, buf[:]); check(e) })
74+ assertNoAllocs(t, "compose key sequence", func() {
75+ _, e := compose.Feed(raw.XKB_KEY_dead_circumflex)
76+ check(e)
77+ _, e = compose.Feed(raw.XKB_KEY_e)
78+ check(e)
79+ _, e = compose.Status()
80+ check(e)
81+ _, e = compose.KeySym()
82+ check(e)
83+ _, e = compose.UTF8Into(buf[:])
84+ check(e)
85+ check(compose.Reset())
86+ })
87+ if fail != nil {
88+ t.Fatal(fail)
89+ }
90+}
91+
92+// TestHotPathValues guards the fixed-arity dispatch against argument/return
93+// conversion mistakes.
94+func TestHotPathValues(t *testing.T) {
95+ c := live(t)
96+ k := fdKeymap(t, c, "us")
97+ s := state(t, k)
98+ if r, e := k.KeyRepeats(WaylandKeycode(30)); e != nil || !r {
99+ t.Fatalf("KeyRepeats(a) = %v, %v", r, e)
100+ }
101+ ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
102+ if e != nil {
103+ t.Fatal(e)
104+ }
105+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || a {
106+ t.Fatalf("ctrl active before update: %v %v", a, e)
107+ }
108+ if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
109+ t.Fatal(e)
110+ }
111+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
112+ t.Fatalf("ctrl name active: %v %v", a, e)
113+ }
114+ if a, e := s.ModIndexActive(ctrl); e != nil || !a {
115+ t.Fatalf("ctrl index active: %v %v", a, e)
116+ }
117+ if a, e := s.ModNameActive("NoSuchModifier"); e != nil || a {
118+ t.Fatalf("unknown modifier active: %v %v", a, e)
119+ }
120+ var buf [16]byte
121+ n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf[:])
122+ if e != nil || string(buf[:n]) != "EuroSign" {
123+ t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)
124+ }
125+}
126+
127+func TestModNameActiveLongName(t *testing.T) {
128+ c := live(t)
129+ k := fdKeymap(t, c, "us")
130+ s := state(t, k)
131+ for _, n := range []int{62, 63, 64, 65, 200} {
132+ name := strings.Repeat("x", n)
133+ if a, e := s.ModNameActive(name); e != nil || a {
134+ t.Fatalf("len %d: %v %v", n, a, e)
135+ }
136+ }
137+ // A short name after a long one must not see stale scratch bytes.
138+ ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
139+ if e != nil {
140+ t.Fatal(e)
141+ }
142+ if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
143+ t.Fatal(e)
144+ }
145+ if a, e := s.ModNameActive(strings.Repeat("y", 63)); e != nil || a {
146+ t.Fatal(a, e)
147+ }
148+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
149+ t.Fatal(a, e)
150+ }
151+ if _, e := s.ModNameActive("a\x00b"); e == nil {
152+ t.Fatal("embedded NUL accepted")
153+ }
154+}
155+
156+func TestKeysymNameIntoShortBuffer(t *testing.T) {
157+ live(t)
158+ buf := []byte{0xff, 0xff, 0xff}
159+ n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf)
160+ if !errors.Is(e, io.ErrShortBuffer) || n != len("EuroSign") || !bytes.Equal(buf, make([]byte, 3)) {
161+ t.Fatalf("n=%d err=%v buf=%v", n, e, buf)
162+ }
163+ if _, e = KeysymNameInto(raw.XKB_KEY_a, nil); !errors.Is(e, ErrUTF8Buffer) {
164+ t.Fatal(e)
165+ }
166+}
167+
168+func TestModIndexActiveInvalid(t *testing.T) {
169+ c := live(t)
170+ s := state(t, fdKeymap(t, c, "us"))
171+ for _, idx := range []uint32{64, 1 << 20, ^uint32(0)} {
172+ if a, e := s.ModIndexActive(idx); e != nil || a {
173+ t.Fatalf("idx %d: %v %v", idx, a, e)
174+ }
175+ }
176+ s.Close()
177+ if _, e := s.ModIndexActive(0); !errors.Is(e, ErrClosed) {
178+ t.Fatal(e)
179+ }
180+ if _, e := s.ModNameActive("Shift"); !errors.Is(e, ErrClosed) {
181+ t.Fatal(e)
182+ }
183+}
184+
185+func TestNewKeymapRulesNames(t *testing.T) {
186+ c := live(t)
187+ // Every RMLVO component is exercised so each pointer in RuleNames must stay valid.
188+ k, e := c.NewKeymapRules("evdev", "pc105", "us,fr", "dvorak,", "grp:alt_shift_toggle")
189+ if e != nil {
190+ t.Fatal(e)
191+ }
192+ defer k.Close()
193+ if n, e := k.LayoutCount(); e != nil || n != 2 {
194+ t.Fatalf("layouts %d %v", n, e)
195+ }
196+ s := state(t, k)
197+ runtime.GC()
198+ // Dvorak puts apostrophe on the evdev KEY_Q position.
199+ if sym, e := s.KeySym(WaylandKeycode(16)); e != nil || sym != raw.XKB_KEY_apostrophe {
200+ t.Fatalf("dvorak sym %#x %v", sym, e)
201+ }
202+ // Defaults (all empty) still work, and an unknown layout is rejected.
203+ d, e := c.NewKeymapRules("", "", "", "", "")
204+ if e != nil {
205+ t.Fatal(e)
206+ }
207+ d.Close()
208+ if _, e = c.NewKeymapRules("evdev", "pc105", "nosuchlayout", "", ""); e == nil {
209+ t.Fatal("unknown layout accepted")
210+ }
211+}
212+
213+func TestZeroFuncsPanics(t *testing.T) {
214+ defer func() {
215+ if recover() == nil {
216+ t.Fatal("zero raw.Funcs must panic, not call address 0")
217+ }
218+ }()
219+ var f raw.Funcs
220+ f.Xkb_keymap_num_layouts(0)
221+}
@@ -0,0 +1,221 @@
1+package xkbcommon
2+
3+import (
4+ "bytes"
5+ "errors"
6+ "io"
7+ "os"
8+ "runtime"
9+ "strings"
10+ "testing"
11+
12+ "github.com/bnema/purego-xkbcommon/raw"
13+)
14+
15+// fdKeymap compiles a pinned Wayland-format keymap from testdata.
16+func fdKeymap(t *testing.T, c *Context, layout string) *Keymap {
17+ t.Helper()
18+ text, e := os.ReadFile("testdata/keymaps/" + layout + ".xkb")
19+ if e != nil {
20+ t.Fatal(e)
21+ }
22+ f := keymapFile(t, append(text, 0))
23+ k, e := c.NewKeymapFD(int(f.Fd()), len(text)+1)
24+ if e != nil {
25+ t.Fatal(e)
26+ }
27+ t.Cleanup(func() { k.Close() })
28+ return k
29+}
30+
31+func assertNoAllocs(t *testing.T, name string, fn func()) {
32+ t.Helper()
33+ fn() // warm up lazy runtime state
34+ if n := testing.AllocsPerRun(200, fn); n != 0 {
35+ t.Errorf("%s: %v allocs/op, want 0", name, n)
36+ } else {
37+ t.Logf("%s: 0 allocs/op", name)
38+ }
39+}
40+
41+func TestHotPathAllocs(t *testing.T) {
42+ c := live(t)
43+ k := fdKeymap(t, c, "fr")
44+ s := state(t, k)
45+ code := WaylandKeycode(16)
46+ var buf [MaxUTF8Buffer]byte
47+ var fail error
48+ check := func(e error) {
49+ if e != nil {
50+ fail = e
51+ }
52+ }
53+
54+ table, e := c.NewComposeTable("en_US.UTF-8")
55+ if e != nil {
56+ t.Fatal(e)
57+ }
58+ defer table.Close()
59+ compose, e := table.NewState()
60+ if e != nil {
61+ t.Fatal(e)
62+ }
63+ defer compose.Close()
64+
65+ assertNoAllocs(t, "State.KeySym", func() { _, e := s.KeySym(code); check(e) })
66+ assertNoAllocs(t, "State.UTF8Into", func() { _, e := s.UTF8Into(code, buf[:]); check(e) })
67+ assertNoAllocs(t, "State.UpdateMask", func() { _, e := s.UpdateMask(1, 0, 2, 0, 0, 0); check(e) })
68+ assertNoAllocs(t, "State.Layout", func() { _, e := s.Layout(); check(e) })
69+ assertNoAllocs(t, "State.Mods", func() { _, e := s.Mods(); check(e) })
70+ assertNoAllocs(t, "State.ModIndexActive", func() { _, e := s.ModIndexActive(0); check(e) })
71+ assertNoAllocs(t, "State.ModNameActive", func() { _, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); check(e) })
72+ assertNoAllocs(t, "Keymap.KeyRepeats", func() { _, e := k.KeyRepeats(code); check(e) })
73+ assertNoAllocs(t, "KeysymNameInto", func() { _, e := KeysymNameInto(raw.XKB_KEY_a, buf[:]); check(e) })
74+ assertNoAllocs(t, "compose key sequence", func() {
75+ _, e := compose.Feed(raw.XKB_KEY_dead_circumflex)
76+ check(e)
77+ _, e = compose.Feed(raw.XKB_KEY_e)
78+ check(e)
79+ _, e = compose.Status()
80+ check(e)
81+ _, e = compose.KeySym()
82+ check(e)
83+ _, e = compose.UTF8Into(buf[:])
84+ check(e)
85+ check(compose.Reset())
86+ })
87+ if fail != nil {
88+ t.Fatal(fail)
89+ }
90+}
91+
92+// TestHotPathValues guards the fixed-arity dispatch against argument/return
93+// conversion mistakes.
94+func TestHotPathValues(t *testing.T) {
95+ c := live(t)
96+ k := fdKeymap(t, c, "us")
97+ s := state(t, k)
98+ if r, e := k.KeyRepeats(WaylandKeycode(30)); e != nil || !r {
99+ t.Fatalf("KeyRepeats(a) = %v, %v", r, e)
100+ }
101+ ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
102+ if e != nil {
103+ t.Fatal(e)
104+ }
105+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || a {
106+ t.Fatalf("ctrl active before update: %v %v", a, e)
107+ }
108+ if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
109+ t.Fatal(e)
110+ }
111+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
112+ t.Fatalf("ctrl name active: %v %v", a, e)
113+ }
114+ if a, e := s.ModIndexActive(ctrl); e != nil || !a {
115+ t.Fatalf("ctrl index active: %v %v", a, e)
116+ }
117+ if a, e := s.ModNameActive("NoSuchModifier"); e != nil || a {
118+ t.Fatalf("unknown modifier active: %v %v", a, e)
119+ }
120+ var buf [16]byte
121+ n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf[:])
122+ if e != nil || string(buf[:n]) != "EuroSign" {
123+ t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)
124+ }
125+}
126+
127+func TestModNameActiveLongName(t *testing.T) {
128+ c := live(t)
129+ k := fdKeymap(t, c, "us")
130+ s := state(t, k)
131+ for _, n := range []int{62, 63, 64, 65, 200} {
132+ name := strings.Repeat("x", n)
133+ if a, e := s.ModNameActive(name); e != nil || a {
134+ t.Fatalf("len %d: %v %v", n, a, e)
135+ }
136+ }
137+ // A short name after a long one must not see stale scratch bytes.
138+ ctrl, e := k.ModIndex(raw.XKB_MOD_NAME_CTRL)
139+ if e != nil {
140+ t.Fatal(e)
141+ }
142+ if _, e := s.UpdateMask(1<<ctrl, 0, 0, 0, 0, 0); e != nil {
143+ t.Fatal(e)
144+ }
145+ if a, e := s.ModNameActive(strings.Repeat("y", 63)); e != nil || a {
146+ t.Fatal(a, e)
147+ }
148+ if a, e := s.ModNameActive(raw.XKB_MOD_NAME_CTRL); e != nil || !a {
149+ t.Fatal(a, e)
150+ }
151+ if _, e := s.ModNameActive("a\x00b"); e == nil {
152+ t.Fatal("embedded NUL accepted")
153+ }
154+}
155+
156+func TestKeysymNameIntoShortBuffer(t *testing.T) {
157+ live(t)
158+ buf := []byte{0xff, 0xff, 0xff}
159+ n, e := KeysymNameInto(raw.XKB_KEY_EuroSign, buf)
160+ if !errors.Is(e, io.ErrShortBuffer) || n != len("EuroSign") || !bytes.Equal(buf, make([]byte, 3)) {
161+ t.Fatalf("n=%d err=%v buf=%v", n, e, buf)
162+ }
163+ if _, e = KeysymNameInto(raw.XKB_KEY_a, nil); !errors.Is(e, ErrUTF8Buffer) {
164+ t.Fatal(e)
165+ }
166+}
167+
168+func TestModIndexActiveInvalid(t *testing.T) {
169+ c := live(t)
170+ s := state(t, fdKeymap(t, c, "us"))
171+ for _, idx := range []uint32{64, 1 << 20, ^uint32(0)} {
172+ if a, e := s.ModIndexActive(idx); e != nil || a {
173+ t.Fatalf("idx %d: %v %v", idx, a, e)
174+ }
175+ }
176+ s.Close()
177+ if _, e := s.ModIndexActive(0); !errors.Is(e, ErrClosed) {
178+ t.Fatal(e)
179+ }
180+ if _, e := s.ModNameActive("Shift"); !errors.Is(e, ErrClosed) {
181+ t.Fatal(e)
182+ }
183+}
184+
185+func TestNewKeymapRulesNames(t *testing.T) {
186+ c := live(t)
187+ // Every RMLVO component is exercised so each pointer in RuleNames must stay valid.
188+ k, e := c.NewKeymapRules("evdev", "pc105", "us,fr", "dvorak,", "grp:alt_shift_toggle")
189+ if e != nil {
190+ t.Fatal(e)
191+ }
192+ defer k.Close()
193+ if n, e := k.LayoutCount(); e != nil || n != 2 {
194+ t.Fatalf("layouts %d %v", n, e)
195+ }
196+ s := state(t, k)
197+ runtime.GC()
198+ // Dvorak puts apostrophe on the evdev KEY_Q position.
199+ if sym, e := s.KeySym(WaylandKeycode(16)); e != nil || sym != raw.XKB_KEY_apostrophe {
200+ t.Fatalf("dvorak sym %#x %v", sym, e)
201+ }
202+ // Defaults (all empty) still work, and an unknown layout is rejected.
203+ d, e := c.NewKeymapRules("", "", "", "", "")
204+ if e != nil {
205+ t.Fatal(e)
206+ }
207+ d.Close()
208+ if _, e = c.NewKeymapRules("evdev", "pc105", "nosuchlayout", "", ""); e == nil {
209+ t.Fatal("unknown layout accepted")
210+ }
211+}
212+
213+func TestZeroFuncsPanics(t *testing.T) {
214+ defer func() {
215+ if recover() == nil {
216+ t.Fatal("zero raw.Funcs must panic, not call address 0")
217+ }
218+ }()
219+ var f raw.Funcs
220+ f.Xkb_keymap_num_layouts(0)
221+}
M internal/gen/gen.go +86 -15
@@ -54,24 +54,89 @@ func constants(h string) string {
5454 }
5555
5656 // Functions describes the bound ABI; each name is checked against the pinned headers.
57-// Pointers are uintptr to avoid GC passing Go pointers to native code unexpectedly.
57+// Go memory is passed as unsafe.Pointer and converted inside the Syscall call
58+// expression; native handles (contexts, keymaps, states) are uintptr.
5859 var Functions = []struct{ Name, Signature string }{
5960 {"xkb_context_new", "func(uint32) uintptr"}, {"xkb_context_ref", "func(uintptr) uintptr"}, {"xkb_context_unref", "func(uintptr)"},
60- {"xkb_keymap_new_from_names", "func(uintptr, uintptr, uint32) uintptr"}, {"xkb_keymap_new_from_string", "func(uintptr, uintptr, uint32, uint32) uintptr"}, {"xkb_keymap_new_from_buffer", "func(uintptr, uintptr, uintptr, uint32, uint32) 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"},
6162 {"xkb_keymap_ref", "func(uintptr) uintptr"}, {"xkb_keymap_unref", "func(uintptr)"}, {"xkb_keymap_get_as_string", "func(uintptr,uint32) uintptr"},
62- {"xkb_keymap_mod_get_index", "func(uintptr,uintptr) uint32"}, {"xkb_keymap_num_layouts", "func(uintptr) uint32"},
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"},
6364 {"xkb_state_new", "func(uintptr) uintptr"}, {"xkb_state_ref", "func(uintptr) uintptr"}, {"xkb_state_unref", "func(uintptr)"},
6465 {"xkb_state_update_mask", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32) uint32"},
6566 {"xkb_state_serialize_layout", "func(uintptr,uint32) uint32"}, {"xkb_state_serialize_mods", "func(uintptr,uint32) uint32"},
66- {"xkb_state_key_get_one_sym", "func(uintptr,uint32) uint32"}, {"xkb_state_key_get_utf8", "func(uintptr,uint32,uintptr,uintptr) int32"},
67- {"xkb_keysym_get_name", "func(uint32,uintptr,uintptr) int32"}, {"xkb_keysym_from_name", "func(uintptr,uint32) uint32"},
68- {"xkb_compose_table_new_from_locale", "func(uintptr,uintptr,uint32) uintptr"}, {"xkb_compose_table_ref", "func(uintptr) uintptr"}, {"xkb_compose_table_unref", "func(uintptr)"},
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)"},
6971 {"xkb_compose_state_new", "func(uintptr,uint32) uintptr"}, {"xkb_compose_state_ref", "func(uintptr) uintptr"}, {"xkb_compose_state_unref", "func(uintptr)"},
7072 {"xkb_compose_state_feed", "func(uintptr,uint32) uint32"}, {"xkb_compose_state_get_status", "func(uintptr) uint32"},
71- {"xkb_compose_state_reset", "func(uintptr)"}, {"xkb_compose_state_get_utf8", "func(uintptr,uintptr,uintptr) int32"}, {"xkb_compose_state_get_one_sym", "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"},
7274 }
7375
7476 func 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.
82+var allowedParams = map[string]bool{"uintptr": true, "uint32": true, "int32": true, "unsafe.Pointer": true}
83+var 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.
87+func 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+}
75140 func Generate(root string) error {
76141 h1, err := os.ReadFile(root + "/upstream/xkbcommon.h")
77142 if err != nil {
@@ -83,9 +148,9 @@ func Generate(root string) error {
83148 }
84149 hdr := string(h1) + "\n" + string(h2)
85150 var b strings.Builder
86- b.WriteString("// Code generated by go generate; DO NOT EDIT.\npackage raw\n\n")
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")
87152 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")
88- b.WriteString("// RuleNames matches struct xkb_rule_names (five const char pointers).\ntype RuleNames struct { Rules, Model, Layout, Variant, Options uintptr }\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")
89154 b.WriteString("const (\n" + constants(hdr))
90155 for _, f := range []string{"xkbcommon-keysyms.h", "xkbcommon-names.h"} {
91156 data, e := os.ReadFile(root + "/upstream/" + f)
@@ -94,24 +159,30 @@ func Generate(root string) error {
94159 }
95160 b.WriteString(constants(string(data)))
96161 }
97- b.WriteString(")\n\n// Funcs holds resolved library functions.\ntype Funcs struct {\n")
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")
98163 for _, f := range Functions {
99164 if !declared(hdr, f.Name) {
100165 return fmt.Errorf("missing pinned declaration: %s", f.Name)
101166 }
102- fmt.Fprintf(&b, "%s %s\n", field(f.Name), f.Signature)
167+ fmt.Fprintf(&b, "%s uintptr\n", f.Name)
103168 }
104169 b.WriteString("}\n\n// Symbols is the complete required symbol set.\nvar Symbols = []string{\n")
105170 for _, f := range Functions {
106171 fmt.Fprintf(&b, "%q,\n", f.Name)
107172 }
108- b.WriteString("}\n\n// Register registers prevalidated addresses in a Funcs value.\nfunc Register(f *Funcs, register func(any,uintptr)) {\n")
173+ b.WriteString("}\n\n// Register stores prevalidated symbol addresses in a Funcs value.\nfunc Register(f *Funcs, addresses map[string]uintptr) {\n")
109174 for _, f := range Functions {
110- fmt.Fprintf(&b, "register(&f.%s, addresses[%q])\n", field(f.Name), f.Name)
175+ fmt.Fprintf(&b, "f.%s = addresses[%q]\n", f.Name, f.Name)
111176 }
112177 b.WriteString("}\n")
113- // address map is passed in, not a global mutable singleton.
114- s := strings.Replace(b.String(), "func Register(f *Funcs, register func(any,uintptr))", "func Register(f *Funcs, addresses map[string]uintptr, register func(any,uintptr))", 1)
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()
115186 formatted, err := format.Source([]byte(s))
116187 if err != nil {
117188 return fmt.Errorf("format generated file: %w", err)
@@ -54,24 +54,89 @@ func constants(h string) string {
54 }54 }
55 55
56 // Functions describes the bound ABI; each name is checked against the pinned headers.56 // Functions describes the bound ABI; each name is checked against the pinned headers.
57-// Pointers are uintptr to avoid GC passing Go pointers to native code unexpectedly.57+// Go memory is passed as unsafe.Pointer and converted inside the Syscall call
58+// expression; native handles (contexts, keymaps, states) are uintptr.
58 var Functions = []struct{ Name, Signature string }{59 var Functions = []struct{ Name, Signature string }{
59 {"xkb_context_new", "func(uint32) uintptr"}, {"xkb_context_ref", "func(uintptr) uintptr"}, {"xkb_context_unref", "func(uintptr)"},60 {"xkb_context_new", "func(uint32) uintptr"}, {"xkb_context_ref", "func(uintptr) uintptr"}, {"xkb_context_unref", "func(uintptr)"},
60- {"xkb_keymap_new_from_names", "func(uintptr, uintptr, uint32) uintptr"}, {"xkb_keymap_new_from_string", "func(uintptr, uintptr, uint32, uint32) uintptr"}, {"xkb_keymap_new_from_buffer", "func(uintptr, uintptr, uintptr, uint32, uint32) 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"},
61 {"xkb_keymap_ref", "func(uintptr) uintptr"}, {"xkb_keymap_unref", "func(uintptr)"}, {"xkb_keymap_get_as_string", "func(uintptr,uint32) uintptr"},62 {"xkb_keymap_ref", "func(uintptr) uintptr"}, {"xkb_keymap_unref", "func(uintptr)"}, {"xkb_keymap_get_as_string", "func(uintptr,uint32) uintptr"},
62- {"xkb_keymap_mod_get_index", "func(uintptr,uintptr) uint32"}, {"xkb_keymap_num_layouts", "func(uintptr) uint32"},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"},
63 {"xkb_state_new", "func(uintptr) uintptr"}, {"xkb_state_ref", "func(uintptr) uintptr"}, {"xkb_state_unref", "func(uintptr)"},64 {"xkb_state_new", "func(uintptr) uintptr"}, {"xkb_state_ref", "func(uintptr) uintptr"}, {"xkb_state_unref", "func(uintptr)"},
64 {"xkb_state_update_mask", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32) uint32"},65 {"xkb_state_update_mask", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32) uint32"},
65 {"xkb_state_serialize_layout", "func(uintptr,uint32) uint32"}, {"xkb_state_serialize_mods", "func(uintptr,uint32) uint32"},66 {"xkb_state_serialize_layout", "func(uintptr,uint32) uint32"}, {"xkb_state_serialize_mods", "func(uintptr,uint32) uint32"},
66- {"xkb_state_key_get_one_sym", "func(uintptr,uint32) uint32"}, {"xkb_state_key_get_utf8", "func(uintptr,uint32,uintptr,uintptr) int32"},67+ {"xkb_state_mod_name_is_active", "func(uintptr,unsafe.Pointer,uint32) int32"}, {"xkb_state_mod_index_is_active", "func(uintptr,uint32,uint32) int32"},
67- {"xkb_keysym_get_name", "func(uint32,uintptr,uintptr) int32"}, {"xkb_keysym_from_name", "func(uintptr,uint32) uint32"},68+ {"xkb_state_key_get_one_sym", "func(uintptr,uint32) uint32"}, {"xkb_state_key_get_utf8", "func(uintptr,uint32,unsafe.Pointer,uintptr) int32"},
68- {"xkb_compose_table_new_from_locale", "func(uintptr,uintptr,uint32) uintptr"}, {"xkb_compose_table_ref", "func(uintptr) uintptr"}, {"xkb_compose_table_unref", "func(uintptr)"},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)"},
69 {"xkb_compose_state_new", "func(uintptr,uint32) uintptr"}, {"xkb_compose_state_ref", "func(uintptr) uintptr"}, {"xkb_compose_state_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)"},
70 {"xkb_compose_state_feed", "func(uintptr,uint32) uint32"}, {"xkb_compose_state_get_status", "func(uintptr) uint32"},72 {"xkb_compose_state_feed", "func(uintptr,uint32) uint32"}, {"xkb_compose_state_get_status", "func(uintptr) uint32"},
71- {"xkb_compose_state_reset", "func(uintptr)"}, {"xkb_compose_state_get_utf8", "func(uintptr,uintptr,uintptr) int32"}, {"xkb_compose_state_get_one_sym", "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"},
72 }74 }
73 75
74 func field(name string) string { return strings.ToUpper(name[:1]) + name[1:] }76 func 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.
82+var allowedParams = map[string]bool{"uintptr": true, "uint32": true, "int32": true, "unsafe.Pointer": true}
83+var 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.
87+func 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+}
75 func Generate(root string) error {140 func Generate(root string) error {
76 h1, err := os.ReadFile(root + "/upstream/xkbcommon.h")141 h1, err := os.ReadFile(root + "/upstream/xkbcommon.h")
77 if err != nil {142 if err != nil {
@@ -83,9 +148,9 @@ func Generate(root string) error {
83 }148 }
84 hdr := string(h1) + "\n" + string(h2)149 hdr := string(h1) + "\n" + string(h2)
85 var b strings.Builder150 var b strings.Builder
86- b.WriteString("// Code generated by go generate; DO NOT EDIT.\npackage raw\n\n")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")
87 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")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")
88- b.WriteString("// RuleNames matches struct xkb_rule_names (five const char pointers).\ntype RuleNames struct { Rules, Model, Layout, Variant, Options uintptr }\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")
89 b.WriteString("const (\n" + constants(hdr))154 b.WriteString("const (\n" + constants(hdr))
90 for _, f := range []string{"xkbcommon-keysyms.h", "xkbcommon-names.h"} {155 for _, f := range []string{"xkbcommon-keysyms.h", "xkbcommon-names.h"} {
91 data, e := os.ReadFile(root + "/upstream/" + f)156 data, e := os.ReadFile(root + "/upstream/" + f)
@@ -94,24 +159,30 @@ func Generate(root string) error {
94 }159 }
95 b.WriteString(constants(string(data)))160 b.WriteString(constants(string(data)))
96 }161 }
97- b.WriteString(")\n\n// Funcs holds resolved library functions.\ntype Funcs struct {\n")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")
98 for _, f := range Functions {163 for _, f := range Functions {
99 if !declared(hdr, f.Name) {164 if !declared(hdr, f.Name) {
100 return fmt.Errorf("missing pinned declaration: %s", f.Name)165 return fmt.Errorf("missing pinned declaration: %s", f.Name)
101 }166 }
102- fmt.Fprintf(&b, "%s %s\n", field(f.Name), f.Signature)167+ fmt.Fprintf(&b, "%s uintptr\n", f.Name)
103 }168 }
104 b.WriteString("}\n\n// Symbols is the complete required symbol set.\nvar Symbols = []string{\n")169 b.WriteString("}\n\n// Symbols is the complete required symbol set.\nvar Symbols = []string{\n")
105 for _, f := range Functions {170 for _, f := range Functions {
106 fmt.Fprintf(&b, "%q,\n", f.Name)171 fmt.Fprintf(&b, "%q,\n", f.Name)
107 }172 }
108- b.WriteString("}\n\n// Register registers prevalidated addresses in a Funcs value.\nfunc Register(f *Funcs, register func(any,uintptr)) {\n")173+ b.WriteString("}\n\n// Register stores prevalidated symbol addresses in a Funcs value.\nfunc Register(f *Funcs, addresses map[string]uintptr) {\n")
109 for _, f := range Functions {174 for _, f := range Functions {
110- fmt.Fprintf(&b, "register(&f.%s, addresses[%q])\n", field(f.Name), f.Name)175+ fmt.Fprintf(&b, "f.%s = addresses[%q]\n", f.Name, f.Name)
111 }176 }
112 b.WriteString("}\n")177 b.WriteString("}\n")
113- // address map is passed in, not a global mutable singleton.178+ for _, f := range Functions {
114- s := strings.Replace(b.String(), "func Register(f *Funcs, register func(any,uintptr))", "func Register(f *Funcs, addresses map[string]uintptr, register func(any,uintptr))", 1)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()
115 formatted, err := format.Source([]byte(s))186 formatted, err := format.Source([]byte(s))
116 if err != nil {187 if err != nil {
117 return fmt.Errorf("format generated file: %w", err)188 return fmt.Errorf("format generated file: %w", err)
M internal/gen/gen_test.go +20 -0
@@ -23,3 +23,23 @@ func TestConstants(t *testing.T) {
2323 }
2424 }
2525 }
26+
27+func TestMethodWhitelist(t *testing.T) {
28+ for _, sig := range []string{"func(float64) uint32", "func(bool)", "func(uintptr, ...uintptr) uint32", "func(uint32) float32", "func(uint32) unsafe.Pointer", "func(*int)", "func(uint32) bool"} {
29+ if _, err := method("xkb_bad", sig); err == nil {
30+ t.Fatalf("accepted %s", sig)
31+ }
32+ }
33+ got, err := method("xkb_ok", "func(uintptr, unsafe.Pointer, int32) uint32")
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ for _, want := range []string{"purego.Syscall6(f.xkb_ok, a1, uintptr(a2), uintptr(a3), 0, 0, 0)", "return uint32(r)", "a2 unsafe.Pointer"} {
38+ if !strings.Contains(got, want) {
39+ t.Fatalf("missing %q in %s", want, got)
40+ }
41+ }
42+ if got, _ = method("xkb_seven", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32)"); !strings.Contains(got, "purego.Syscall15(") {
43+ t.Fatal("seven args must use Syscall15")
44+ }
45+}
@@ -23,3 +23,23 @@ func TestConstants(t *testing.T) {
23 }23 }
24 }24 }
25 }25 }
26+
27+func TestMethodWhitelist(t *testing.T) {
28+ for _, sig := range []string{"func(float64) uint32", "func(bool)", "func(uintptr, ...uintptr) uint32", "func(uint32) float32", "func(uint32) unsafe.Pointer", "func(*int)", "func(uint32) bool"} {
29+ if _, err := method("xkb_bad", sig); err == nil {
30+ t.Fatalf("accepted %s", sig)
31+ }
32+ }
33+ got, err := method("xkb_ok", "func(uintptr, unsafe.Pointer, int32) uint32")
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ for _, want := range []string{"purego.Syscall6(f.xkb_ok, a1, uintptr(a2), uintptr(a3), 0, 0, 0)", "return uint32(r)", "a2 unsafe.Pointer"} {
38+ if !strings.Contains(got, want) {
39+ t.Fatalf("missing %q in %s", want, got)
40+ }
41+ }
42+ if got, _ = method("xkb_seven", "func(uintptr,uint32,uint32,uint32,uint32,uint32,uint32)"); !strings.Contains(got, "purego.Syscall15(") {
43+ t.Fatal("seven args must use Syscall15")
44+ }
45+}
M loader.go +3 -3
@@ -15,7 +15,7 @@ var loader struct {
1515 }
1616
1717 // resolveLibrary keeps path and required symbols injectable for diagnostic tests.
18-// On success it leaves the library open for the lifetime of registered functions.
18+// On success it leaves the library open for the lifetime of resolved function addresses.
1919 func resolveLibrary(path string, symbols []string) (map[string]uintptr, error) {
2020 lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL)
2121 if err != nil {
@@ -42,8 +42,8 @@ func Available() error {
4242 loader.err = err
4343 return
4444 }
45- raw.Register(&loader.f, addresses, purego.RegisterFunc)
46- // Intentionally keep the library open: registered function pointers and live
45+ raw.Register(&loader.f, addresses)
46+ // Intentionally keep the library open: resolved function addresses and live
4747 // native objects remain valid for the process lifetime.
4848 })
4949 return loader.err
@@ -15,7 +15,7 @@ var loader struct {
15 }15 }
16 16
17 // resolveLibrary keeps path and required symbols injectable for diagnostic tests.17 // resolveLibrary keeps path and required symbols injectable for diagnostic tests.
18-// On success it leaves the library open for the lifetime of registered functions.18+// On success it leaves the library open for the lifetime of resolved function addresses.
19 func resolveLibrary(path string, symbols []string) (map[string]uintptr, error) {19 func resolveLibrary(path string, symbols []string) (map[string]uintptr, error) {
20 lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL)20 lib, err := purego.Dlopen(path, purego.RTLD_NOW|purego.RTLD_LOCAL)
21 if err != nil {21 if err != nil {
@@ -42,8 +42,8 @@ func Available() error {
42 loader.err = err42 loader.err = err
43 return43 return
44 }44 }
45- raw.Register(&loader.f, addresses, purego.RegisterFunc)45+ raw.Register(&loader.f, addresses)
46- // Intentionally keep the library open: registered function pointers and live46+ // Intentionally keep the library open: resolved function addresses and live
47 // native objects remain valid for the process lifetime.47 // native objects remain valid for the process lifetime.
48 })48 })
49 return loader.err49 return loader.err
M raw/generated.go +290 -69
@@ -1,6 +1,12 @@
11 // Code generated by go generate; DO NOT EDIT.
22 package raw
33
4+import (
5+ "unsafe"
6+
7+ "github.com/bnema/purego"
8+)
9+
410 // Opaque C handles and scalar typedefs.
511 type Context uintptr
612 type Keymap uintptr
@@ -14,8 +20,9 @@ type ModIndex uint32
1420 type ModMask uint32
1521 type LayoutMask uint32
1622
17-// RuleNames matches struct xkb_rule_names (five const char pointers).
18-type RuleNames struct{ Rules, Model, Layout, Variant, Options uintptr }
23+// RuleNames matches struct xkb_rule_names (five const char pointers). The
24+// fields are unsafe.Pointer so the GC sees the NUL-terminated strings they reference.
25+type RuleNames struct{ Rules, Model, Layout, Variant, Options unsafe.Pointer }
1926
2027 const (
2128 XKB_RMLVO_BUILDER_NO_FLAGS = 0
@@ -2725,40 +2732,44 @@ const (
27252732 XKB_LED_NAME_KANA = "Kana"
27262733 )
27272734
2728-// Funcs holds resolved library functions.
2735+// Funcs holds resolved library function addresses. Each function is invoked
2736+// through purego.Syscall6/Syscall15 (fixed arity, no reflection, no allocation).
27292737 type Funcs struct {
2730- Xkb_context_new func(uint32) uintptr
2731- Xkb_context_ref func(uintptr) uintptr
2732- Xkb_context_unref func(uintptr)
2733- Xkb_keymap_new_from_names func(uintptr, uintptr, uint32) uintptr
2734- Xkb_keymap_new_from_string func(uintptr, uintptr, uint32, uint32) uintptr
2735- Xkb_keymap_new_from_buffer func(uintptr, uintptr, uintptr, uint32, uint32) uintptr
2736- Xkb_keymap_ref func(uintptr) uintptr
2737- Xkb_keymap_unref func(uintptr)
2738- Xkb_keymap_get_as_string func(uintptr, uint32) uintptr
2739- Xkb_keymap_mod_get_index func(uintptr, uintptr) uint32
2740- Xkb_keymap_num_layouts func(uintptr) uint32
2741- Xkb_state_new func(uintptr) uintptr
2742- Xkb_state_ref func(uintptr) uintptr
2743- Xkb_state_unref func(uintptr)
2744- Xkb_state_update_mask func(uintptr, uint32, uint32, uint32, uint32, uint32, uint32) uint32
2745- Xkb_state_serialize_layout func(uintptr, uint32) uint32
2746- Xkb_state_serialize_mods func(uintptr, uint32) uint32
2747- Xkb_state_key_get_one_sym func(uintptr, uint32) uint32
2748- Xkb_state_key_get_utf8 func(uintptr, uint32, uintptr, uintptr) int32
2749- Xkb_keysym_get_name func(uint32, uintptr, uintptr) int32
2750- Xkb_keysym_from_name func(uintptr, uint32) uint32
2751- Xkb_compose_table_new_from_locale func(uintptr, uintptr, uint32) uintptr
2752- Xkb_compose_table_ref func(uintptr) uintptr
2753- Xkb_compose_table_unref func(uintptr)
2754- Xkb_compose_state_new func(uintptr, uint32) uintptr
2755- Xkb_compose_state_ref func(uintptr) uintptr
2756- Xkb_compose_state_unref func(uintptr)
2757- Xkb_compose_state_feed func(uintptr, uint32) uint32
2758- Xkb_compose_state_get_status func(uintptr) uint32
2759- Xkb_compose_state_reset func(uintptr)
2760- Xkb_compose_state_get_utf8 func(uintptr, uintptr, uintptr) int32
2761- Xkb_compose_state_get_one_sym func(uintptr) uint32
2738+ xkb_context_new uintptr
2739+ xkb_context_ref uintptr
2740+ xkb_context_unref uintptr
2741+ xkb_keymap_new_from_names uintptr
2742+ xkb_keymap_new_from_string uintptr
2743+ xkb_keymap_new_from_buffer uintptr
2744+ xkb_keymap_ref uintptr
2745+ xkb_keymap_unref uintptr
2746+ xkb_keymap_get_as_string uintptr
2747+ xkb_keymap_mod_get_index uintptr
2748+ xkb_keymap_key_repeats uintptr
2749+ xkb_keymap_num_layouts uintptr
2750+ xkb_state_new uintptr
2751+ xkb_state_ref uintptr
2752+ xkb_state_unref uintptr
2753+ xkb_state_update_mask uintptr
2754+ xkb_state_serialize_layout uintptr
2755+ xkb_state_serialize_mods uintptr
2756+ xkb_state_mod_name_is_active uintptr
2757+ xkb_state_mod_index_is_active uintptr
2758+ xkb_state_key_get_one_sym uintptr
2759+ xkb_state_key_get_utf8 uintptr
2760+ xkb_keysym_get_name uintptr
2761+ xkb_keysym_from_name uintptr
2762+ xkb_compose_table_new_from_locale uintptr
2763+ xkb_compose_table_ref uintptr
2764+ xkb_compose_table_unref uintptr
2765+ xkb_compose_state_new uintptr
2766+ xkb_compose_state_ref uintptr
2767+ xkb_compose_state_unref uintptr
2768+ xkb_compose_state_feed uintptr
2769+ xkb_compose_state_get_status uintptr
2770+ xkb_compose_state_reset uintptr
2771+ xkb_compose_state_get_utf8 uintptr
2772+ xkb_compose_state_get_one_sym uintptr
27622773 }
27632774
27642775 // Symbols is the complete required symbol set.
@@ -2773,6 +2784,7 @@ var Symbols = []string{
27732784 "xkb_keymap_unref",
27742785 "xkb_keymap_get_as_string",
27752786 "xkb_keymap_mod_get_index",
2787+ "xkb_keymap_key_repeats",
27762788 "xkb_keymap_num_layouts",
27772789 "xkb_state_new",
27782790 "xkb_state_ref",
@@ -2780,6 +2792,8 @@ var Symbols = []string{
27802792 "xkb_state_update_mask",
27812793 "xkb_state_serialize_layout",
27822794 "xkb_state_serialize_mods",
2795+ "xkb_state_mod_name_is_active",
2796+ "xkb_state_mod_index_is_active",
27832797 "xkb_state_key_get_one_sym",
27842798 "xkb_state_key_get_utf8",
27852799 "xkb_keysym_get_name",
@@ -2797,38 +2811,245 @@ var Symbols = []string{
27972811 "xkb_compose_state_get_one_sym",
27982812 }
27992813
2800-// Register registers prevalidated addresses in a Funcs value.
2801-func Register(f *Funcs, addresses map[string]uintptr, register func(any, uintptr)) {
2802- register(&f.Xkb_context_new, addresses["xkb_context_new"])
2803- register(&f.Xkb_context_ref, addresses["xkb_context_ref"])
2804- register(&f.Xkb_context_unref, addresses["xkb_context_unref"])
2805- register(&f.Xkb_keymap_new_from_names, addresses["xkb_keymap_new_from_names"])
2806- register(&f.Xkb_keymap_new_from_string, addresses["xkb_keymap_new_from_string"])
2807- register(&f.Xkb_keymap_new_from_buffer, addresses["xkb_keymap_new_from_buffer"])
2808- register(&f.Xkb_keymap_ref, addresses["xkb_keymap_ref"])
2809- register(&f.Xkb_keymap_unref, addresses["xkb_keymap_unref"])
2810- register(&f.Xkb_keymap_get_as_string, addresses["xkb_keymap_get_as_string"])
2811- register(&f.Xkb_keymap_mod_get_index, addresses["xkb_keymap_mod_get_index"])
2812- register(&f.Xkb_keymap_num_layouts, addresses["xkb_keymap_num_layouts"])
2813- register(&f.Xkb_state_new, addresses["xkb_state_new"])
2814- register(&f.Xkb_state_ref, addresses["xkb_state_ref"])
2815- register(&f.Xkb_state_unref, addresses["xkb_state_unref"])
2816- register(&f.Xkb_state_update_mask, addresses["xkb_state_update_mask"])
2817- register(&f.Xkb_state_serialize_layout, addresses["xkb_state_serialize_layout"])
2818- register(&f.Xkb_state_serialize_mods, addresses["xkb_state_serialize_mods"])
2819- register(&f.Xkb_state_key_get_one_sym, addresses["xkb_state_key_get_one_sym"])
2820- register(&f.Xkb_state_key_get_utf8, addresses["xkb_state_key_get_utf8"])
2821- register(&f.Xkb_keysym_get_name, addresses["xkb_keysym_get_name"])
2822- register(&f.Xkb_keysym_from_name, addresses["xkb_keysym_from_name"])
2823- register(&f.Xkb_compose_table_new_from_locale, addresses["xkb_compose_table_new_from_locale"])
2824- register(&f.Xkb_compose_table_ref, addresses["xkb_compose_table_ref"])
2825- register(&f.Xkb_compose_table_unref, addresses["xkb_compose_table_unref"])
2826- register(&f.Xkb_compose_state_new, addresses["xkb_compose_state_new"])
2827- register(&f.Xkb_compose_state_ref, addresses["xkb_compose_state_ref"])
2828- register(&f.Xkb_compose_state_unref, addresses["xkb_compose_state_unref"])
2829- register(&f.Xkb_compose_state_feed, addresses["xkb_compose_state_feed"])
2830- register(&f.Xkb_compose_state_get_status, addresses["xkb_compose_state_get_status"])
2831- register(&f.Xkb_compose_state_reset, addresses["xkb_compose_state_reset"])
2832- register(&f.Xkb_compose_state_get_utf8, addresses["xkb_compose_state_get_utf8"])
2833- register(&f.Xkb_compose_state_get_one_sym, addresses["xkb_compose_state_get_one_sym"])
2814+// Register stores prevalidated symbol addresses in a Funcs value.
2815+func Register(f *Funcs, addresses map[string]uintptr) {
2816+ f.xkb_context_new = addresses["xkb_context_new"]
2817+ f.xkb_context_ref = addresses["xkb_context_ref"]
2818+ f.xkb_context_unref = addresses["xkb_context_unref"]
2819+ f.xkb_keymap_new_from_names = addresses["xkb_keymap_new_from_names"]
2820+ f.xkb_keymap_new_from_string = addresses["xkb_keymap_new_from_string"]
2821+ f.xkb_keymap_new_from_buffer = addresses["xkb_keymap_new_from_buffer"]
2822+ f.xkb_keymap_ref = addresses["xkb_keymap_ref"]
2823+ f.xkb_keymap_unref = addresses["xkb_keymap_unref"]
2824+ f.xkb_keymap_get_as_string = addresses["xkb_keymap_get_as_string"]
2825+ f.xkb_keymap_mod_get_index = addresses["xkb_keymap_mod_get_index"]
2826+ f.xkb_keymap_key_repeats = addresses["xkb_keymap_key_repeats"]
2827+ f.xkb_keymap_num_layouts = addresses["xkb_keymap_num_layouts"]
2828+ f.xkb_state_new = addresses["xkb_state_new"]
2829+ f.xkb_state_ref = addresses["xkb_state_ref"]
2830+ f.xkb_state_unref = addresses["xkb_state_unref"]
2831+ f.xkb_state_update_mask = addresses["xkb_state_update_mask"]
2832+ f.xkb_state_serialize_layout = addresses["xkb_state_serialize_layout"]
2833+ f.xkb_state_serialize_mods = addresses["xkb_state_serialize_mods"]
2834+ f.xkb_state_mod_name_is_active = addresses["xkb_state_mod_name_is_active"]
2835+ f.xkb_state_mod_index_is_active = addresses["xkb_state_mod_index_is_active"]
2836+ f.xkb_state_key_get_one_sym = addresses["xkb_state_key_get_one_sym"]
2837+ f.xkb_state_key_get_utf8 = addresses["xkb_state_key_get_utf8"]
2838+ f.xkb_keysym_get_name = addresses["xkb_keysym_get_name"]
2839+ f.xkb_keysym_from_name = addresses["xkb_keysym_from_name"]
2840+ f.xkb_compose_table_new_from_locale = addresses["xkb_compose_table_new_from_locale"]
2841+ f.xkb_compose_table_ref = addresses["xkb_compose_table_ref"]
2842+ f.xkb_compose_table_unref = addresses["xkb_compose_table_unref"]
2843+ f.xkb_compose_state_new = addresses["xkb_compose_state_new"]
2844+ f.xkb_compose_state_ref = addresses["xkb_compose_state_ref"]
2845+ f.xkb_compose_state_unref = addresses["xkb_compose_state_unref"]
2846+ f.xkb_compose_state_feed = addresses["xkb_compose_state_feed"]
2847+ f.xkb_compose_state_get_status = addresses["xkb_compose_state_get_status"]
2848+ f.xkb_compose_state_reset = addresses["xkb_compose_state_reset"]
2849+ f.xkb_compose_state_get_utf8 = addresses["xkb_compose_state_get_utf8"]
2850+ f.xkb_compose_state_get_one_sym = addresses["xkb_compose_state_get_one_sym"]
2851+}
2852+
2853+// Xkb_context_new calls xkb_context_new without allocating.
2854+func (f *Funcs) Xkb_context_new(a1 uint32) uintptr {
2855+ r, _, _ := purego.Syscall6(f.xkb_context_new, uintptr(a1), 0, 0, 0, 0, 0)
2856+ return r
2857+}
2858+
2859+// Xkb_context_ref calls xkb_context_ref without allocating.
2860+func (f *Funcs) Xkb_context_ref(a1 uintptr) uintptr {
2861+ r, _, _ := purego.Syscall6(f.xkb_context_ref, a1, 0, 0, 0, 0, 0)
2862+ return r
2863+}
2864+
2865+// Xkb_context_unref calls xkb_context_unref without allocating.
2866+func (f *Funcs) Xkb_context_unref(a1 uintptr) {
2867+ purego.Syscall6(f.xkb_context_unref, a1, 0, 0, 0, 0, 0)
2868+}
2869+
2870+// Xkb_keymap_new_from_names calls xkb_keymap_new_from_names without allocating.
2871+func (f *Funcs) Xkb_keymap_new_from_names(a1 uintptr, a2 unsafe.Pointer, a3 uint32) uintptr {
2872+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_names, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2873+ return r
2874+}
2875+
2876+// Xkb_keymap_new_from_string calls xkb_keymap_new_from_string without allocating.
2877+func (f *Funcs) Xkb_keymap_new_from_string(a1 uintptr, a2 unsafe.Pointer, a3 uint32, a4 uint32) uintptr {
2878+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_string, a1, uintptr(a2), uintptr(a3), uintptr(a4), 0, 0)
2879+ return r
2880+}
2881+
2882+// Xkb_keymap_new_from_buffer calls xkb_keymap_new_from_buffer without allocating.
2883+func (f *Funcs) Xkb_keymap_new_from_buffer(a1 uintptr, a2 unsafe.Pointer, a3 uintptr, a4 uint32, a5 uint32) uintptr {
2884+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_buffer, a1, uintptr(a2), a3, uintptr(a4), uintptr(a5), 0)
2885+ return r
2886+}
2887+
2888+// Xkb_keymap_ref calls xkb_keymap_ref without allocating.
2889+func (f *Funcs) Xkb_keymap_ref(a1 uintptr) uintptr {
2890+ r, _, _ := purego.Syscall6(f.xkb_keymap_ref, a1, 0, 0, 0, 0, 0)
2891+ return r
2892+}
2893+
2894+// Xkb_keymap_unref calls xkb_keymap_unref without allocating.
2895+func (f *Funcs) Xkb_keymap_unref(a1 uintptr) {
2896+ purego.Syscall6(f.xkb_keymap_unref, a1, 0, 0, 0, 0, 0)
2897+}
2898+
2899+// Xkb_keymap_get_as_string calls xkb_keymap_get_as_string without allocating.
2900+func (f *Funcs) Xkb_keymap_get_as_string(a1 uintptr, a2 uint32) uintptr {
2901+ r, _, _ := purego.Syscall6(f.xkb_keymap_get_as_string, a1, uintptr(a2), 0, 0, 0, 0)
2902+ return r
2903+}
2904+
2905+// Xkb_keymap_mod_get_index calls xkb_keymap_mod_get_index without allocating.
2906+func (f *Funcs) Xkb_keymap_mod_get_index(a1 uintptr, a2 unsafe.Pointer) uint32 {
2907+ r, _, _ := purego.Syscall6(f.xkb_keymap_mod_get_index, a1, uintptr(a2), 0, 0, 0, 0)
2908+ return uint32(r)
2909+}
2910+
2911+// Xkb_keymap_key_repeats calls xkb_keymap_key_repeats without allocating.
2912+func (f *Funcs) Xkb_keymap_key_repeats(a1 uintptr, a2 uint32) int32 {
2913+ r, _, _ := purego.Syscall6(f.xkb_keymap_key_repeats, a1, uintptr(a2), 0, 0, 0, 0)
2914+ return int32(r)
2915+}
2916+
2917+// Xkb_keymap_num_layouts calls xkb_keymap_num_layouts without allocating.
2918+func (f *Funcs) Xkb_keymap_num_layouts(a1 uintptr) uint32 {
2919+ r, _, _ := purego.Syscall6(f.xkb_keymap_num_layouts, a1, 0, 0, 0, 0, 0)
2920+ return uint32(r)
2921+}
2922+
2923+// Xkb_state_new calls xkb_state_new without allocating.
2924+func (f *Funcs) Xkb_state_new(a1 uintptr) uintptr {
2925+ r, _, _ := purego.Syscall6(f.xkb_state_new, a1, 0, 0, 0, 0, 0)
2926+ return r
2927+}
2928+
2929+// Xkb_state_ref calls xkb_state_ref without allocating.
2930+func (f *Funcs) Xkb_state_ref(a1 uintptr) uintptr {
2931+ r, _, _ := purego.Syscall6(f.xkb_state_ref, a1, 0, 0, 0, 0, 0)
2932+ return r
2933+}
2934+
2935+// Xkb_state_unref calls xkb_state_unref without allocating.
2936+func (f *Funcs) Xkb_state_unref(a1 uintptr) {
2937+ purego.Syscall6(f.xkb_state_unref, a1, 0, 0, 0, 0, 0)
2938+}
2939+
2940+// Xkb_state_update_mask calls xkb_state_update_mask without allocating.
2941+func (f *Funcs) Xkb_state_update_mask(a1 uintptr, a2 uint32, a3 uint32, a4 uint32, a5 uint32, a6 uint32, a7 uint32) uint32 {
2942+ r, _, _ := purego.Syscall15(f.xkb_state_update_mask, a1, uintptr(a2), uintptr(a3), uintptr(a4), uintptr(a5), uintptr(a6), uintptr(a7), 0, 0, 0, 0, 0, 0, 0, 0)
2943+ return uint32(r)
2944+}
2945+
2946+// Xkb_state_serialize_layout calls xkb_state_serialize_layout without allocating.
2947+func (f *Funcs) Xkb_state_serialize_layout(a1 uintptr, a2 uint32) uint32 {
2948+ r, _, _ := purego.Syscall6(f.xkb_state_serialize_layout, a1, uintptr(a2), 0, 0, 0, 0)
2949+ return uint32(r)
2950+}
2951+
2952+// Xkb_state_serialize_mods calls xkb_state_serialize_mods without allocating.
2953+func (f *Funcs) Xkb_state_serialize_mods(a1 uintptr, a2 uint32) uint32 {
2954+ r, _, _ := purego.Syscall6(f.xkb_state_serialize_mods, a1, uintptr(a2), 0, 0, 0, 0)
2955+ return uint32(r)
2956+}
2957+
2958+// Xkb_state_mod_name_is_active calls xkb_state_mod_name_is_active without allocating.
2959+func (f *Funcs) Xkb_state_mod_name_is_active(a1 uintptr, a2 unsafe.Pointer, a3 uint32) int32 {
2960+ r, _, _ := purego.Syscall6(f.xkb_state_mod_name_is_active, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2961+ return int32(r)
2962+}
2963+
2964+// Xkb_state_mod_index_is_active calls xkb_state_mod_index_is_active without allocating.
2965+func (f *Funcs) Xkb_state_mod_index_is_active(a1 uintptr, a2 uint32, a3 uint32) int32 {
2966+ r, _, _ := purego.Syscall6(f.xkb_state_mod_index_is_active, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2967+ return int32(r)
2968+}
2969+
2970+// Xkb_state_key_get_one_sym calls xkb_state_key_get_one_sym without allocating.
2971+func (f *Funcs) Xkb_state_key_get_one_sym(a1 uintptr, a2 uint32) uint32 {
2972+ r, _, _ := purego.Syscall6(f.xkb_state_key_get_one_sym, a1, uintptr(a2), 0, 0, 0, 0)
2973+ return uint32(r)
2974+}
2975+
2976+// Xkb_state_key_get_utf8 calls xkb_state_key_get_utf8 without allocating.
2977+func (f *Funcs) Xkb_state_key_get_utf8(a1 uintptr, a2 uint32, a3 unsafe.Pointer, a4 uintptr) int32 {
2978+ r, _, _ := purego.Syscall6(f.xkb_state_key_get_utf8, a1, uintptr(a2), uintptr(a3), a4, 0, 0)
2979+ return int32(r)
2980+}
2981+
2982+// Xkb_keysym_get_name calls xkb_keysym_get_name without allocating.
2983+func (f *Funcs) Xkb_keysym_get_name(a1 uint32, a2 unsafe.Pointer, a3 uintptr) int32 {
2984+ r, _, _ := purego.Syscall6(f.xkb_keysym_get_name, uintptr(a1), uintptr(a2), a3, 0, 0, 0)
2985+ return int32(r)
2986+}
2987+
2988+// Xkb_keysym_from_name calls xkb_keysym_from_name without allocating.
2989+func (f *Funcs) Xkb_keysym_from_name(a1 unsafe.Pointer, a2 uint32) uint32 {
2990+ r, _, _ := purego.Syscall6(f.xkb_keysym_from_name, uintptr(a1), uintptr(a2), 0, 0, 0, 0)
2991+ return uint32(r)
2992+}
2993+
2994+// Xkb_compose_table_new_from_locale calls xkb_compose_table_new_from_locale without allocating.
2995+func (f *Funcs) Xkb_compose_table_new_from_locale(a1 uintptr, a2 unsafe.Pointer, a3 uint32) uintptr {
2996+ r, _, _ := purego.Syscall6(f.xkb_compose_table_new_from_locale, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2997+ return r
2998+}
2999+
3000+// Xkb_compose_table_ref calls xkb_compose_table_ref without allocating.
3001+func (f *Funcs) Xkb_compose_table_ref(a1 uintptr) uintptr {
3002+ r, _, _ := purego.Syscall6(f.xkb_compose_table_ref, a1, 0, 0, 0, 0, 0)
3003+ return r
3004+}
3005+
3006+// Xkb_compose_table_unref calls xkb_compose_table_unref without allocating.
3007+func (f *Funcs) Xkb_compose_table_unref(a1 uintptr) {
3008+ purego.Syscall6(f.xkb_compose_table_unref, a1, 0, 0, 0, 0, 0)
3009+}
3010+
3011+// Xkb_compose_state_new calls xkb_compose_state_new without allocating.
3012+func (f *Funcs) Xkb_compose_state_new(a1 uintptr, a2 uint32) uintptr {
3013+ r, _, _ := purego.Syscall6(f.xkb_compose_state_new, a1, uintptr(a2), 0, 0, 0, 0)
3014+ return r
3015+}
3016+
3017+// Xkb_compose_state_ref calls xkb_compose_state_ref without allocating.
3018+func (f *Funcs) Xkb_compose_state_ref(a1 uintptr) uintptr {
3019+ r, _, _ := purego.Syscall6(f.xkb_compose_state_ref, a1, 0, 0, 0, 0, 0)
3020+ return r
3021+}
3022+
3023+// Xkb_compose_state_unref calls xkb_compose_state_unref without allocating.
3024+func (f *Funcs) Xkb_compose_state_unref(a1 uintptr) {
3025+ purego.Syscall6(f.xkb_compose_state_unref, a1, 0, 0, 0, 0, 0)
3026+}
3027+
3028+// Xkb_compose_state_feed calls xkb_compose_state_feed without allocating.
3029+func (f *Funcs) Xkb_compose_state_feed(a1 uintptr, a2 uint32) uint32 {
3030+ r, _, _ := purego.Syscall6(f.xkb_compose_state_feed, a1, uintptr(a2), 0, 0, 0, 0)
3031+ return uint32(r)
3032+}
3033+
3034+// Xkb_compose_state_get_status calls xkb_compose_state_get_status without allocating.
3035+func (f *Funcs) Xkb_compose_state_get_status(a1 uintptr) uint32 {
3036+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_status, a1, 0, 0, 0, 0, 0)
3037+ return uint32(r)
3038+}
3039+
3040+// Xkb_compose_state_reset calls xkb_compose_state_reset without allocating.
3041+func (f *Funcs) Xkb_compose_state_reset(a1 uintptr) {
3042+ purego.Syscall6(f.xkb_compose_state_reset, a1, 0, 0, 0, 0, 0)
3043+}
3044+
3045+// Xkb_compose_state_get_utf8 calls xkb_compose_state_get_utf8 without allocating.
3046+func (f *Funcs) Xkb_compose_state_get_utf8(a1 uintptr, a2 unsafe.Pointer, a3 uintptr) int32 {
3047+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_utf8, a1, uintptr(a2), a3, 0, 0, 0)
3048+ return int32(r)
3049+}
3050+
3051+// Xkb_compose_state_get_one_sym calls xkb_compose_state_get_one_sym without allocating.
3052+func (f *Funcs) Xkb_compose_state_get_one_sym(a1 uintptr) uint32 {
3053+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_one_sym, a1, 0, 0, 0, 0, 0)
3054+ return uint32(r)
28343055 }
@@ -1,6 +1,12 @@
1 // Code generated by go generate; DO NOT EDIT.1 // Code generated by go generate; DO NOT EDIT.
2 package raw2 package raw
3 3
4+import (
5+ "unsafe"
6+
7+ "github.com/bnema/purego"
8+)
9+
4 // Opaque C handles and scalar typedefs.10 // Opaque C handles and scalar typedefs.
5 type Context uintptr11 type Context uintptr
6 type Keymap uintptr12 type Keymap uintptr
@@ -14,8 +20,9 @@ type ModIndex uint32
14 type ModMask uint3220 type ModMask uint32
15 type LayoutMask uint3221 type LayoutMask uint32
16 22
17-// RuleNames matches struct xkb_rule_names (five const char pointers).23+// RuleNames matches struct xkb_rule_names (five const char pointers). The
18-type RuleNames struct{ Rules, Model, Layout, Variant, Options uintptr }24+// fields are unsafe.Pointer so the GC sees the NUL-terminated strings they reference.
25+type RuleNames struct{ Rules, Model, Layout, Variant, Options unsafe.Pointer }
19 26
20 const (27 const (
21 XKB_RMLVO_BUILDER_NO_FLAGS = 028 XKB_RMLVO_BUILDER_NO_FLAGS = 0
@@ -2725,40 +2732,44 @@ const (
2725 XKB_LED_NAME_KANA = "Kana"2732 XKB_LED_NAME_KANA = "Kana"
2726 )2733 )
2727 2734
2728-// Funcs holds resolved library functions.2735+// Funcs holds resolved library function addresses. Each function is invoked
2736+// through purego.Syscall6/Syscall15 (fixed arity, no reflection, no allocation).
2729 type Funcs struct {2737 type Funcs struct {
2730- Xkb_context_new func(uint32) uintptr2738+ xkb_context_new uintptr
2731- Xkb_context_ref func(uintptr) uintptr2739+ xkb_context_ref uintptr
2732- Xkb_context_unref func(uintptr)2740+ xkb_context_unref uintptr
2733- Xkb_keymap_new_from_names func(uintptr, uintptr, uint32) uintptr2741+ xkb_keymap_new_from_names uintptr
2734- Xkb_keymap_new_from_string func(uintptr, uintptr, uint32, uint32) uintptr2742+ xkb_keymap_new_from_string uintptr
2735- Xkb_keymap_new_from_buffer func(uintptr, uintptr, uintptr, uint32, uint32) uintptr2743+ xkb_keymap_new_from_buffer uintptr
2736- Xkb_keymap_ref func(uintptr) uintptr2744+ xkb_keymap_ref uintptr
2737- Xkb_keymap_unref func(uintptr)2745+ xkb_keymap_unref uintptr
2738- Xkb_keymap_get_as_string func(uintptr, uint32) uintptr2746+ xkb_keymap_get_as_string uintptr
2739- Xkb_keymap_mod_get_index func(uintptr, uintptr) uint322747+ xkb_keymap_mod_get_index uintptr
2740- Xkb_keymap_num_layouts func(uintptr) uint322748+ xkb_keymap_key_repeats uintptr
2741- Xkb_state_new func(uintptr) uintptr2749+ xkb_keymap_num_layouts uintptr
2742- Xkb_state_ref func(uintptr) uintptr2750+ xkb_state_new uintptr
2743- Xkb_state_unref func(uintptr)2751+ xkb_state_ref uintptr
2744- Xkb_state_update_mask func(uintptr, uint32, uint32, uint32, uint32, uint32, uint32) uint322752+ xkb_state_unref uintptr
2745- Xkb_state_serialize_layout func(uintptr, uint32) uint322753+ xkb_state_update_mask uintptr
2746- Xkb_state_serialize_mods func(uintptr, uint32) uint322754+ xkb_state_serialize_layout uintptr
2747- Xkb_state_key_get_one_sym func(uintptr, uint32) uint322755+ xkb_state_serialize_mods uintptr
2748- Xkb_state_key_get_utf8 func(uintptr, uint32, uintptr, uintptr) int322756+ xkb_state_mod_name_is_active uintptr
2749- Xkb_keysym_get_name func(uint32, uintptr, uintptr) int322757+ xkb_state_mod_index_is_active uintptr
2750- Xkb_keysym_from_name func(uintptr, uint32) uint322758+ xkb_state_key_get_one_sym uintptr
2751- Xkb_compose_table_new_from_locale func(uintptr, uintptr, uint32) uintptr2759+ xkb_state_key_get_utf8 uintptr
2752- Xkb_compose_table_ref func(uintptr) uintptr2760+ xkb_keysym_get_name uintptr
2753- Xkb_compose_table_unref func(uintptr)2761+ xkb_keysym_from_name uintptr
2754- Xkb_compose_state_new func(uintptr, uint32) uintptr2762+ xkb_compose_table_new_from_locale uintptr
2755- Xkb_compose_state_ref func(uintptr) uintptr2763+ xkb_compose_table_ref uintptr
2756- Xkb_compose_state_unref func(uintptr)2764+ xkb_compose_table_unref uintptr
2757- Xkb_compose_state_feed func(uintptr, uint32) uint322765+ xkb_compose_state_new uintptr
2758- Xkb_compose_state_get_status func(uintptr) uint322766+ xkb_compose_state_ref uintptr
2759- Xkb_compose_state_reset func(uintptr)2767+ xkb_compose_state_unref uintptr
2760- Xkb_compose_state_get_utf8 func(uintptr, uintptr, uintptr) int322768+ xkb_compose_state_feed uintptr
2761- Xkb_compose_state_get_one_sym func(uintptr) uint322769+ xkb_compose_state_get_status uintptr
2770+ xkb_compose_state_reset uintptr
2771+ xkb_compose_state_get_utf8 uintptr
2772+ xkb_compose_state_get_one_sym uintptr
2762 }2773 }
2763 2774
2764 // Symbols is the complete required symbol set.2775 // Symbols is the complete required symbol set.
@@ -2773,6 +2784,7 @@ var Symbols = []string{
2773 "xkb_keymap_unref",2784 "xkb_keymap_unref",
2774 "xkb_keymap_get_as_string",2785 "xkb_keymap_get_as_string",
2775 "xkb_keymap_mod_get_index",2786 "xkb_keymap_mod_get_index",
2787+ "xkb_keymap_key_repeats",
2776 "xkb_keymap_num_layouts",2788 "xkb_keymap_num_layouts",
2777 "xkb_state_new",2789 "xkb_state_new",
2778 "xkb_state_ref",2790 "xkb_state_ref",
@@ -2780,6 +2792,8 @@ var Symbols = []string{
2780 "xkb_state_update_mask",2792 "xkb_state_update_mask",
2781 "xkb_state_serialize_layout",2793 "xkb_state_serialize_layout",
2782 "xkb_state_serialize_mods",2794 "xkb_state_serialize_mods",
2795+ "xkb_state_mod_name_is_active",
2796+ "xkb_state_mod_index_is_active",
2783 "xkb_state_key_get_one_sym",2797 "xkb_state_key_get_one_sym",
2784 "xkb_state_key_get_utf8",2798 "xkb_state_key_get_utf8",
2785 "xkb_keysym_get_name",2799 "xkb_keysym_get_name",
@@ -2797,38 +2811,245 @@ var Symbols = []string{
2797 "xkb_compose_state_get_one_sym",2811 "xkb_compose_state_get_one_sym",
2798 }2812 }
2799 2813
2800-// Register registers prevalidated addresses in a Funcs value.2814+// Register stores prevalidated symbol addresses in a Funcs value.
2801-func Register(f *Funcs, addresses map[string]uintptr, register func(any, uintptr)) {2815+func Register(f *Funcs, addresses map[string]uintptr) {
2802- register(&f.Xkb_context_new, addresses["xkb_context_new"])2816+ f.xkb_context_new = addresses["xkb_context_new"]
2803- register(&f.Xkb_context_ref, addresses["xkb_context_ref"])2817+ f.xkb_context_ref = addresses["xkb_context_ref"]
2804- register(&f.Xkb_context_unref, addresses["xkb_context_unref"])2818+ f.xkb_context_unref = addresses["xkb_context_unref"]
2805- register(&f.Xkb_keymap_new_from_names, addresses["xkb_keymap_new_from_names"])2819+ f.xkb_keymap_new_from_names = addresses["xkb_keymap_new_from_names"]
2806- register(&f.Xkb_keymap_new_from_string, addresses["xkb_keymap_new_from_string"])2820+ f.xkb_keymap_new_from_string = addresses["xkb_keymap_new_from_string"]
2807- register(&f.Xkb_keymap_new_from_buffer, addresses["xkb_keymap_new_from_buffer"])2821+ f.xkb_keymap_new_from_buffer = addresses["xkb_keymap_new_from_buffer"]
2808- register(&f.Xkb_keymap_ref, addresses["xkb_keymap_ref"])2822+ f.xkb_keymap_ref = addresses["xkb_keymap_ref"]
2809- register(&f.Xkb_keymap_unref, addresses["xkb_keymap_unref"])2823+ f.xkb_keymap_unref = addresses["xkb_keymap_unref"]
2810- register(&f.Xkb_keymap_get_as_string, addresses["xkb_keymap_get_as_string"])2824+ f.xkb_keymap_get_as_string = addresses["xkb_keymap_get_as_string"]
2811- register(&f.Xkb_keymap_mod_get_index, addresses["xkb_keymap_mod_get_index"])2825+ f.xkb_keymap_mod_get_index = addresses["xkb_keymap_mod_get_index"]
2812- register(&f.Xkb_keymap_num_layouts, addresses["xkb_keymap_num_layouts"])2826+ f.xkb_keymap_key_repeats = addresses["xkb_keymap_key_repeats"]
2813- register(&f.Xkb_state_new, addresses["xkb_state_new"])2827+ f.xkb_keymap_num_layouts = addresses["xkb_keymap_num_layouts"]
2814- register(&f.Xkb_state_ref, addresses["xkb_state_ref"])2828+ f.xkb_state_new = addresses["xkb_state_new"]
2815- register(&f.Xkb_state_unref, addresses["xkb_state_unref"])2829+ f.xkb_state_ref = addresses["xkb_state_ref"]
2816- register(&f.Xkb_state_update_mask, addresses["xkb_state_update_mask"])2830+ f.xkb_state_unref = addresses["xkb_state_unref"]
2817- register(&f.Xkb_state_serialize_layout, addresses["xkb_state_serialize_layout"])2831+ f.xkb_state_update_mask = addresses["xkb_state_update_mask"]
2818- register(&f.Xkb_state_serialize_mods, addresses["xkb_state_serialize_mods"])2832+ f.xkb_state_serialize_layout = addresses["xkb_state_serialize_layout"]
2819- register(&f.Xkb_state_key_get_one_sym, addresses["xkb_state_key_get_one_sym"])2833+ f.xkb_state_serialize_mods = addresses["xkb_state_serialize_mods"]
2820- register(&f.Xkb_state_key_get_utf8, addresses["xkb_state_key_get_utf8"])2834+ f.xkb_state_mod_name_is_active = addresses["xkb_state_mod_name_is_active"]
2821- register(&f.Xkb_keysym_get_name, addresses["xkb_keysym_get_name"])2835+ f.xkb_state_mod_index_is_active = addresses["xkb_state_mod_index_is_active"]
2822- register(&f.Xkb_keysym_from_name, addresses["xkb_keysym_from_name"])2836+ f.xkb_state_key_get_one_sym = addresses["xkb_state_key_get_one_sym"]
2823- register(&f.Xkb_compose_table_new_from_locale, addresses["xkb_compose_table_new_from_locale"])2837+ f.xkb_state_key_get_utf8 = addresses["xkb_state_key_get_utf8"]
2824- register(&f.Xkb_compose_table_ref, addresses["xkb_compose_table_ref"])2838+ f.xkb_keysym_get_name = addresses["xkb_keysym_get_name"]
2825- register(&f.Xkb_compose_table_unref, addresses["xkb_compose_table_unref"])2839+ f.xkb_keysym_from_name = addresses["xkb_keysym_from_name"]
2826- register(&f.Xkb_compose_state_new, addresses["xkb_compose_state_new"])2840+ f.xkb_compose_table_new_from_locale = addresses["xkb_compose_table_new_from_locale"]
2827- register(&f.Xkb_compose_state_ref, addresses["xkb_compose_state_ref"])2841+ f.xkb_compose_table_ref = addresses["xkb_compose_table_ref"]
2828- register(&f.Xkb_compose_state_unref, addresses["xkb_compose_state_unref"])2842+ f.xkb_compose_table_unref = addresses["xkb_compose_table_unref"]
2829- register(&f.Xkb_compose_state_feed, addresses["xkb_compose_state_feed"])2843+ f.xkb_compose_state_new = addresses["xkb_compose_state_new"]
2830- register(&f.Xkb_compose_state_get_status, addresses["xkb_compose_state_get_status"])2844+ f.xkb_compose_state_ref = addresses["xkb_compose_state_ref"]
2831- register(&f.Xkb_compose_state_reset, addresses["xkb_compose_state_reset"])2845+ f.xkb_compose_state_unref = addresses["xkb_compose_state_unref"]
2832- register(&f.Xkb_compose_state_get_utf8, addresses["xkb_compose_state_get_utf8"])2846+ f.xkb_compose_state_feed = addresses["xkb_compose_state_feed"]
2833- register(&f.Xkb_compose_state_get_one_sym, addresses["xkb_compose_state_get_one_sym"])2847+ f.xkb_compose_state_get_status = addresses["xkb_compose_state_get_status"]
2848+ f.xkb_compose_state_reset = addresses["xkb_compose_state_reset"]
2849+ f.xkb_compose_state_get_utf8 = addresses["xkb_compose_state_get_utf8"]
2850+ f.xkb_compose_state_get_one_sym = addresses["xkb_compose_state_get_one_sym"]
2851+}
2852+
2853+// Xkb_context_new calls xkb_context_new without allocating.
2854+func (f *Funcs) Xkb_context_new(a1 uint32) uintptr {
2855+ r, _, _ := purego.Syscall6(f.xkb_context_new, uintptr(a1), 0, 0, 0, 0, 0)
2856+ return r
2857+}
2858+
2859+// Xkb_context_ref calls xkb_context_ref without allocating.
2860+func (f *Funcs) Xkb_context_ref(a1 uintptr) uintptr {
2861+ r, _, _ := purego.Syscall6(f.xkb_context_ref, a1, 0, 0, 0, 0, 0)
2862+ return r
2863+}
2864+
2865+// Xkb_context_unref calls xkb_context_unref without allocating.
2866+func (f *Funcs) Xkb_context_unref(a1 uintptr) {
2867+ purego.Syscall6(f.xkb_context_unref, a1, 0, 0, 0, 0, 0)
2868+}
2869+
2870+// Xkb_keymap_new_from_names calls xkb_keymap_new_from_names without allocating.
2871+func (f *Funcs) Xkb_keymap_new_from_names(a1 uintptr, a2 unsafe.Pointer, a3 uint32) uintptr {
2872+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_names, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2873+ return r
2874+}
2875+
2876+// Xkb_keymap_new_from_string calls xkb_keymap_new_from_string without allocating.
2877+func (f *Funcs) Xkb_keymap_new_from_string(a1 uintptr, a2 unsafe.Pointer, a3 uint32, a4 uint32) uintptr {
2878+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_string, a1, uintptr(a2), uintptr(a3), uintptr(a4), 0, 0)
2879+ return r
2880+}
2881+
2882+// Xkb_keymap_new_from_buffer calls xkb_keymap_new_from_buffer without allocating.
2883+func (f *Funcs) Xkb_keymap_new_from_buffer(a1 uintptr, a2 unsafe.Pointer, a3 uintptr, a4 uint32, a5 uint32) uintptr {
2884+ r, _, _ := purego.Syscall6(f.xkb_keymap_new_from_buffer, a1, uintptr(a2), a3, uintptr(a4), uintptr(a5), 0)
2885+ return r
2886+}
2887+
2888+// Xkb_keymap_ref calls xkb_keymap_ref without allocating.
2889+func (f *Funcs) Xkb_keymap_ref(a1 uintptr) uintptr {
2890+ r, _, _ := purego.Syscall6(f.xkb_keymap_ref, a1, 0, 0, 0, 0, 0)
2891+ return r
2892+}
2893+
2894+// Xkb_keymap_unref calls xkb_keymap_unref without allocating.
2895+func (f *Funcs) Xkb_keymap_unref(a1 uintptr) {
2896+ purego.Syscall6(f.xkb_keymap_unref, a1, 0, 0, 0, 0, 0)
2897+}
2898+
2899+// Xkb_keymap_get_as_string calls xkb_keymap_get_as_string without allocating.
2900+func (f *Funcs) Xkb_keymap_get_as_string(a1 uintptr, a2 uint32) uintptr {
2901+ r, _, _ := purego.Syscall6(f.xkb_keymap_get_as_string, a1, uintptr(a2), 0, 0, 0, 0)
2902+ return r
2903+}
2904+
2905+// Xkb_keymap_mod_get_index calls xkb_keymap_mod_get_index without allocating.
2906+func (f *Funcs) Xkb_keymap_mod_get_index(a1 uintptr, a2 unsafe.Pointer) uint32 {
2907+ r, _, _ := purego.Syscall6(f.xkb_keymap_mod_get_index, a1, uintptr(a2), 0, 0, 0, 0)
2908+ return uint32(r)
2909+}
2910+
2911+// Xkb_keymap_key_repeats calls xkb_keymap_key_repeats without allocating.
2912+func (f *Funcs) Xkb_keymap_key_repeats(a1 uintptr, a2 uint32) int32 {
2913+ r, _, _ := purego.Syscall6(f.xkb_keymap_key_repeats, a1, uintptr(a2), 0, 0, 0, 0)
2914+ return int32(r)
2915+}
2916+
2917+// Xkb_keymap_num_layouts calls xkb_keymap_num_layouts without allocating.
2918+func (f *Funcs) Xkb_keymap_num_layouts(a1 uintptr) uint32 {
2919+ r, _, _ := purego.Syscall6(f.xkb_keymap_num_layouts, a1, 0, 0, 0, 0, 0)
2920+ return uint32(r)
2921+}
2922+
2923+// Xkb_state_new calls xkb_state_new without allocating.
2924+func (f *Funcs) Xkb_state_new(a1 uintptr) uintptr {
2925+ r, _, _ := purego.Syscall6(f.xkb_state_new, a1, 0, 0, 0, 0, 0)
2926+ return r
2927+}
2928+
2929+// Xkb_state_ref calls xkb_state_ref without allocating.
2930+func (f *Funcs) Xkb_state_ref(a1 uintptr) uintptr {
2931+ r, _, _ := purego.Syscall6(f.xkb_state_ref, a1, 0, 0, 0, 0, 0)
2932+ return r
2933+}
2934+
2935+// Xkb_state_unref calls xkb_state_unref without allocating.
2936+func (f *Funcs) Xkb_state_unref(a1 uintptr) {
2937+ purego.Syscall6(f.xkb_state_unref, a1, 0, 0, 0, 0, 0)
2938+}
2939+
2940+// Xkb_state_update_mask calls xkb_state_update_mask without allocating.
2941+func (f *Funcs) Xkb_state_update_mask(a1 uintptr, a2 uint32, a3 uint32, a4 uint32, a5 uint32, a6 uint32, a7 uint32) uint32 {
2942+ r, _, _ := purego.Syscall15(f.xkb_state_update_mask, a1, uintptr(a2), uintptr(a3), uintptr(a4), uintptr(a5), uintptr(a6), uintptr(a7), 0, 0, 0, 0, 0, 0, 0, 0)
2943+ return uint32(r)
2944+}
2945+
2946+// Xkb_state_serialize_layout calls xkb_state_serialize_layout without allocating.
2947+func (f *Funcs) Xkb_state_serialize_layout(a1 uintptr, a2 uint32) uint32 {
2948+ r, _, _ := purego.Syscall6(f.xkb_state_serialize_layout, a1, uintptr(a2), 0, 0, 0, 0)
2949+ return uint32(r)
2950+}
2951+
2952+// Xkb_state_serialize_mods calls xkb_state_serialize_mods without allocating.
2953+func (f *Funcs) Xkb_state_serialize_mods(a1 uintptr, a2 uint32) uint32 {
2954+ r, _, _ := purego.Syscall6(f.xkb_state_serialize_mods, a1, uintptr(a2), 0, 0, 0, 0)
2955+ return uint32(r)
2956+}
2957+
2958+// Xkb_state_mod_name_is_active calls xkb_state_mod_name_is_active without allocating.
2959+func (f *Funcs) Xkb_state_mod_name_is_active(a1 uintptr, a2 unsafe.Pointer, a3 uint32) int32 {
2960+ r, _, _ := purego.Syscall6(f.xkb_state_mod_name_is_active, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2961+ return int32(r)
2962+}
2963+
2964+// Xkb_state_mod_index_is_active calls xkb_state_mod_index_is_active without allocating.
2965+func (f *Funcs) Xkb_state_mod_index_is_active(a1 uintptr, a2 uint32, a3 uint32) int32 {
2966+ r, _, _ := purego.Syscall6(f.xkb_state_mod_index_is_active, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2967+ return int32(r)
2968+}
2969+
2970+// Xkb_state_key_get_one_sym calls xkb_state_key_get_one_sym without allocating.
2971+func (f *Funcs) Xkb_state_key_get_one_sym(a1 uintptr, a2 uint32) uint32 {
2972+ r, _, _ := purego.Syscall6(f.xkb_state_key_get_one_sym, a1, uintptr(a2), 0, 0, 0, 0)
2973+ return uint32(r)
2974+}
2975+
2976+// Xkb_state_key_get_utf8 calls xkb_state_key_get_utf8 without allocating.
2977+func (f *Funcs) Xkb_state_key_get_utf8(a1 uintptr, a2 uint32, a3 unsafe.Pointer, a4 uintptr) int32 {
2978+ r, _, _ := purego.Syscall6(f.xkb_state_key_get_utf8, a1, uintptr(a2), uintptr(a3), a4, 0, 0)
2979+ return int32(r)
2980+}
2981+
2982+// Xkb_keysym_get_name calls xkb_keysym_get_name without allocating.
2983+func (f *Funcs) Xkb_keysym_get_name(a1 uint32, a2 unsafe.Pointer, a3 uintptr) int32 {
2984+ r, _, _ := purego.Syscall6(f.xkb_keysym_get_name, uintptr(a1), uintptr(a2), a3, 0, 0, 0)
2985+ return int32(r)
2986+}
2987+
2988+// Xkb_keysym_from_name calls xkb_keysym_from_name without allocating.
2989+func (f *Funcs) Xkb_keysym_from_name(a1 unsafe.Pointer, a2 uint32) uint32 {
2990+ r, _, _ := purego.Syscall6(f.xkb_keysym_from_name, uintptr(a1), uintptr(a2), 0, 0, 0, 0)
2991+ return uint32(r)
2992+}
2993+
2994+// Xkb_compose_table_new_from_locale calls xkb_compose_table_new_from_locale without allocating.
2995+func (f *Funcs) Xkb_compose_table_new_from_locale(a1 uintptr, a2 unsafe.Pointer, a3 uint32) uintptr {
2996+ r, _, _ := purego.Syscall6(f.xkb_compose_table_new_from_locale, a1, uintptr(a2), uintptr(a3), 0, 0, 0)
2997+ return r
2998+}
2999+
3000+// Xkb_compose_table_ref calls xkb_compose_table_ref without allocating.
3001+func (f *Funcs) Xkb_compose_table_ref(a1 uintptr) uintptr {
3002+ r, _, _ := purego.Syscall6(f.xkb_compose_table_ref, a1, 0, 0, 0, 0, 0)
3003+ return r
3004+}
3005+
3006+// Xkb_compose_table_unref calls xkb_compose_table_unref without allocating.
3007+func (f *Funcs) Xkb_compose_table_unref(a1 uintptr) {
3008+ purego.Syscall6(f.xkb_compose_table_unref, a1, 0, 0, 0, 0, 0)
3009+}
3010+
3011+// Xkb_compose_state_new calls xkb_compose_state_new without allocating.
3012+func (f *Funcs) Xkb_compose_state_new(a1 uintptr, a2 uint32) uintptr {
3013+ r, _, _ := purego.Syscall6(f.xkb_compose_state_new, a1, uintptr(a2), 0, 0, 0, 0)
3014+ return r
3015+}
3016+
3017+// Xkb_compose_state_ref calls xkb_compose_state_ref without allocating.
3018+func (f *Funcs) Xkb_compose_state_ref(a1 uintptr) uintptr {
3019+ r, _, _ := purego.Syscall6(f.xkb_compose_state_ref, a1, 0, 0, 0, 0, 0)
3020+ return r
3021+}
3022+
3023+// Xkb_compose_state_unref calls xkb_compose_state_unref without allocating.
3024+func (f *Funcs) Xkb_compose_state_unref(a1 uintptr) {
3025+ purego.Syscall6(f.xkb_compose_state_unref, a1, 0, 0, 0, 0, 0)
3026+}
3027+
3028+// Xkb_compose_state_feed calls xkb_compose_state_feed without allocating.
3029+func (f *Funcs) Xkb_compose_state_feed(a1 uintptr, a2 uint32) uint32 {
3030+ r, _, _ := purego.Syscall6(f.xkb_compose_state_feed, a1, uintptr(a2), 0, 0, 0, 0)
3031+ return uint32(r)
3032+}
3033+
3034+// Xkb_compose_state_get_status calls xkb_compose_state_get_status without allocating.
3035+func (f *Funcs) Xkb_compose_state_get_status(a1 uintptr) uint32 {
3036+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_status, a1, 0, 0, 0, 0, 0)
3037+ return uint32(r)
3038+}
3039+
3040+// Xkb_compose_state_reset calls xkb_compose_state_reset without allocating.
3041+func (f *Funcs) Xkb_compose_state_reset(a1 uintptr) {
3042+ purego.Syscall6(f.xkb_compose_state_reset, a1, 0, 0, 0, 0, 0)
3043+}
3044+
3045+// Xkb_compose_state_get_utf8 calls xkb_compose_state_get_utf8 without allocating.
3046+func (f *Funcs) Xkb_compose_state_get_utf8(a1 uintptr, a2 unsafe.Pointer, a3 uintptr) int32 {
3047+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_utf8, a1, uintptr(a2), a3, 0, 0, 0)
3048+ return int32(r)
3049+}
3050+
3051+// Xkb_compose_state_get_one_sym calls xkb_compose_state_get_one_sym without allocating.
3052+func (f *Funcs) Xkb_compose_state_get_one_sym(a1 uintptr) uint32 {
3053+ r, _, _ := purego.Syscall6(f.xkb_compose_state_get_one_sym, a1, 0, 0, 0, 0, 0)
3054+ return uint32(r)
2834 }3055 }
M xkbcommon.go +89 -18
@@ -18,11 +18,15 @@ import (
1818 var ErrClosed = errors.New("xkbcommon: closed handle")
1919
2020 func cstr(s string) []byte { return append([]byte(s), 0) }
21-func ptr(b []byte) uintptr {
21+
22+// ptr returns the address of b's first byte, or nil. The result is only valid
23+// as an argument to a raw.Funcs method, whose uintptr conversion keeps the
24+// backing array alive and (via //go:uintptrescapes) on the heap for the call.
25+func ptr(b []byte) unsafe.Pointer {
2226 if len(b) == 0 {
23- return 0
27+ return nil
2428 }
25- return uintptr(unsafe.Pointer(&b[0]))
29+ return unsafe.Pointer(&b[0])
2630 }
2731 func validString(s string) error {
2832 if strings.IndexByte(s, 0) >= 0 {
@@ -112,7 +116,7 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string)
112116 args := []string{rules, model, layout, variant, options}
113117 var b [5][]byte
114118 var n raw.RuleNames
115- p := []*uintptr{&n.Rules, &n.Model, &n.Layout, &n.Variant, &n.Options}
119+ p := []*unsafe.Pointer{&n.Rules, &n.Model, &n.Layout, &n.Variant, &n.Options}
116120 for i, s := range args {
117121 if e := validString(s); e != nil {
118122 return nil, e
@@ -123,7 +127,7 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string)
123127 }
124128 }
125129 return newKeymap(c, func(h uintptr) uintptr {
126- v := loader.f.Xkb_keymap_new_from_names(h, uintptr(unsafe.Pointer(&n)), 0)
130+ v := loader.f.Xkb_keymap_new_from_names(h, unsafe.Pointer(&n), 0)
127131 runtime.KeepAlive(b)
128132 runtime.KeepAlive(n)
129133 return v
@@ -213,6 +217,19 @@ func (k *Keymap) ModIndex(name string) (uint32, error) {
213217 runtime.KeepAlive(b)
214218 return v, nil
215219 }
220+
221+// KeyRepeats reports whether the key repeats when held. It does not allocate.
222+func (k *Keymap) KeyRepeats(keycode uint32) (bool, error) {
223+ if k == nil {
224+ return false, ErrClosed
225+ }
226+ k.mu.Lock()
227+ defer k.mu.Unlock()
228+ if k.h == 0 {
229+ return false, ErrClosed
230+ }
231+ return loader.f.Xkb_keymap_key_repeats(k.h, keycode) > 0, nil
232+}
216233 func (k *Keymap) LayoutCount() (uint32, error) {
217234 if k == nil {
218235 return 0, ErrClosed
@@ -240,6 +257,7 @@ func (k *Keymap) NewState() (*State, error) {
240257 type State struct {
241258 mu sync.Mutex
242259 h, keymap uintptr
260+ name [64]byte // NUL-terminated modifier-name scratch, guarded by mu
243261 }
244262
245263 func (s *State) Close() error {
@@ -284,6 +302,50 @@ func (s *State) Layout() (uint32, error) {
284302 }
285303 return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil
286304 }
305+
306+// ModIndexActive reports whether the modifier at idx is effectively active.
307+// It does not allocate. An unknown index reports false.
308+func (s *State) ModIndexActive(idx uint32) (bool, error) {
309+ if s == nil {
310+ return false, ErrClosed
311+ }
312+ s.mu.Lock()
313+ defer s.mu.Unlock()
314+ if s.h == 0 {
315+ return false, ErrClosed
316+ }
317+ return loader.f.Xkb_state_mod_index_is_active(s.h, idx, raw.XKB_STATE_MODS_EFFECTIVE) > 0, nil
318+}
319+
320+// ModNameActive reports whether the named modifier (for example
321+// raw.XKB_MOD_NAME_CTRL) is effectively active. Names up to 63 bytes do not
322+// allocate. An unknown name reports false.
323+func (s *State) ModNameActive(name string) (bool, error) {
324+ if e := validString(name); e != nil {
325+ return false, e
326+ }
327+ if s == nil {
328+ return false, ErrClosed
329+ }
330+ s.mu.Lock()
331+ defer s.mu.Unlock()
332+ if s.h == 0 {
333+ return false, ErrClosed
334+ }
335+ var b []byte
336+ if len(name) < len(s.name) {
337+ // Heap-resident scratch owned by State and guarded by s.mu.
338+ b = s.name[:len(name)+1]
339+ copy(b, name)
340+ b[len(name)] = 0
341+ } else {
342+ b = cstr(name)
343+ }
344+ v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)
345+ runtime.KeepAlive(b)
346+ return v > 0, nil
347+}
348+
287349 func (s *State) Mods() (uint32, error) {
288350 if s == nil {
289351 return 0, ErrClosed
@@ -308,8 +370,8 @@ func (s *State) KeySym(keycode uint32) (uint32, error) {
308370 }
309371
310372 // sizedUTF8 probes the required byte count (excluding NUL) and retries if it grows.
311-func sizedUTF8(call func(uintptr, uintptr) int32) (string, error) {
312- n := call(0, 0)
373+func sizedUTF8(call func(unsafe.Pointer, uintptr) int32) (string, error) {
374+ n := call(nil, 0)
313375 if n < 0 {
314376 return "", errors.New("xkbcommon: UTF-8 conversion failed")
315377 }
@@ -343,16 +405,14 @@ const MaxUTF8Buffer = 512
343405 var ErrUTF8Buffer = fmt.Errorf("xkbcommon: UTF-8 buffer length must be 1..%d", MaxUTF8Buffer)
344406
345407 // utf8Into never creates a string or keeps the caller's storage. Native writes
346-// are synchronous; the backing array is pinned only for the duration of the call.
408+// are synchronous; the Syscall uintptr conversion keeps dst's backing array
409+// alive and unmoved for the duration of the call (go:uintptrescapes).
347410 // Short buffers are wiped rather than exposing truncated UTF-8 or credentials.
348411 // Callers clear dst before any early return.
349-func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
412+func utf8Into(dst []byte, call func(unsafe.Pointer, uintptr) int32) (int, error) {
350413 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {
351414 return 0, ErrUTF8Buffer
352415 }
353- var pin runtime.Pinner
354- pin.Pin(&dst[0])
355- defer pin.Unpin()
356416 n := call(ptr(dst), uintptr(len(dst)))
357417 runtime.KeepAlive(dst)
358418 if n < 0 {
@@ -368,7 +428,8 @@ func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
368428 }
369429
370430 // UTF8Into writes into caller-owned mutable storage without constructing a
371-// string. n excludes NUL; on io.ErrShortBuffer n is the required payload size,
431+// string. dst is passed to C through a uintptr-escaping call, so its backing
432+// array lives on the heap (a stack array sliced into dst is moved there once). n excludes NUL; on io.ErrShortBuffer n is the required payload size,
372433 // all of dst is cleared, and the caller needs n+1 bytes. dst must have length
373434 // 1..MaxUTF8Buffer. No storage is retained; the caller owns wiping after use.
374435 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
@@ -381,7 +442,7 @@ func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
381442 if s.h == 0 {
382443 return 0, ErrClosed
383444 }
384- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
445+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
385446 }
386447
387448 func (s *State) UTF8(keycode uint32) (string, error) {
@@ -393,13 +454,23 @@ func (s *State) UTF8(keycode uint32) (string, error) {
393454 if s.h == 0 {
394455 return "", ErrClosed
395456 }
396- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
457+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
397458 }
398459 func KeysymName(sym uint32) (string, error) {
399460 if e := Available(); e != nil {
400461 return "", e
401462 }
402- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
463+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
464+}
465+
466+// KeysymNameInto writes the keysym name into dst without allocating, with the
467+// same bounds and short-buffer semantics as State.UTF8Into.
468+func KeysymNameInto(sym uint32, dst []byte) (int, error) {
469+ clear(dst)
470+ if e := Available(); e != nil {
471+ return 0, e
472+ }
473+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
403474 }
404475 func KeysymFromName(name string) (uint32, error) {
405476 if e := Available(); e != nil {
@@ -529,7 +600,7 @@ func (s *ComposeState) UTF8Into(dst []byte) (int, error) {
529600 if s.h == 0 {
530601 return 0, ErrClosed
531602 }
532- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
603+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
533604 }
534605
535606 func (s *ComposeState) UTF8() (string, error) {
@@ -541,7 +612,7 @@ func (s *ComposeState) UTF8() (string, error) {
541612 if s.h == 0 {
542613 return "", ErrClosed
543614 }
544- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
615+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
545616 }
546617 func (s *ComposeState) KeySym() (uint32, error) {
547618 if s == nil {
@@ -18,11 +18,15 @@ import (
18 var ErrClosed = errors.New("xkbcommon: closed handle")18 var ErrClosed = errors.New("xkbcommon: closed handle")
19 19
20 func cstr(s string) []byte { return append([]byte(s), 0) }20 func cstr(s string) []byte { return append([]byte(s), 0) }
21-func ptr(b []byte) uintptr {21+
22+// ptr returns the address of b's first byte, or nil. The result is only valid
23+// as an argument to a raw.Funcs method, whose uintptr conversion keeps the
24+// backing array alive and (via //go:uintptrescapes) on the heap for the call.
25+func ptr(b []byte) unsafe.Pointer {
22 if len(b) == 0 {26 if len(b) == 0 {
23- return 027+ return nil
24 }28 }
25- return uintptr(unsafe.Pointer(&b[0]))29+ return unsafe.Pointer(&b[0])
26 }30 }
27 func validString(s string) error {31 func validString(s string) error {
28 if strings.IndexByte(s, 0) >= 0 {32 if strings.IndexByte(s, 0) >= 0 {
@@ -112,7 +116,7 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string)
112 args := []string{rules, model, layout, variant, options}116 args := []string{rules, model, layout, variant, options}
113 var b [5][]byte117 var b [5][]byte
114 var n raw.RuleNames118 var n raw.RuleNames
115- p := []*uintptr{&n.Rules, &n.Model, &n.Layout, &n.Variant, &n.Options}119+ p := []*unsafe.Pointer{&n.Rules, &n.Model, &n.Layout, &n.Variant, &n.Options}
116 for i, s := range args {120 for i, s := range args {
117 if e := validString(s); e != nil {121 if e := validString(s); e != nil {
118 return nil, e122 return nil, e
@@ -123,7 +127,7 @@ func (c *Context) NewKeymapRules(rules, model, layout, variant, options string)
123 }127 }
124 }128 }
125 return newKeymap(c, func(h uintptr) uintptr {129 return newKeymap(c, func(h uintptr) uintptr {
126- v := loader.f.Xkb_keymap_new_from_names(h, uintptr(unsafe.Pointer(&n)), 0)130+ v := loader.f.Xkb_keymap_new_from_names(h, unsafe.Pointer(&n), 0)
127 runtime.KeepAlive(b)131 runtime.KeepAlive(b)
128 runtime.KeepAlive(n)132 runtime.KeepAlive(n)
129 return v133 return v
@@ -213,6 +217,19 @@ func (k *Keymap) ModIndex(name string) (uint32, error) {
213 runtime.KeepAlive(b)217 runtime.KeepAlive(b)
214 return v, nil218 return v, nil
215 }219 }
220+
221+// KeyRepeats reports whether the key repeats when held. It does not allocate.
222+func (k *Keymap) KeyRepeats(keycode uint32) (bool, error) {
223+ if k == nil {
224+ return false, ErrClosed
225+ }
226+ k.mu.Lock()
227+ defer k.mu.Unlock()
228+ if k.h == 0 {
229+ return false, ErrClosed
230+ }
231+ return loader.f.Xkb_keymap_key_repeats(k.h, keycode) > 0, nil
232+}
216 func (k *Keymap) LayoutCount() (uint32, error) {233 func (k *Keymap) LayoutCount() (uint32, error) {
217 if k == nil {234 if k == nil {
218 return 0, ErrClosed235 return 0, ErrClosed
@@ -240,6 +257,7 @@ func (k *Keymap) NewState() (*State, error) {
240 type State struct {257 type State struct {
241 mu sync.Mutex258 mu sync.Mutex
242 h, keymap uintptr259 h, keymap uintptr
260+ name [64]byte // NUL-terminated modifier-name scratch, guarded by mu
243 }261 }
244 262
245 func (s *State) Close() error {263 func (s *State) Close() error {
@@ -284,6 +302,50 @@ func (s *State) Layout() (uint32, error) {
284 }302 }
285 return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil303 return loader.f.Xkb_state_serialize_layout(s.h, raw.XKB_STATE_LAYOUT_EFFECTIVE), nil
286 }304 }
305+
306+// ModIndexActive reports whether the modifier at idx is effectively active.
307+// It does not allocate. An unknown index reports false.
308+func (s *State) ModIndexActive(idx uint32) (bool, error) {
309+ if s == nil {
310+ return false, ErrClosed
311+ }
312+ s.mu.Lock()
313+ defer s.mu.Unlock()
314+ if s.h == 0 {
315+ return false, ErrClosed
316+ }
317+ return loader.f.Xkb_state_mod_index_is_active(s.h, idx, raw.XKB_STATE_MODS_EFFECTIVE) > 0, nil
318+}
319+
320+// ModNameActive reports whether the named modifier (for example
321+// raw.XKB_MOD_NAME_CTRL) is effectively active. Names up to 63 bytes do not
322+// allocate. An unknown name reports false.
323+func (s *State) ModNameActive(name string) (bool, error) {
324+ if e := validString(name); e != nil {
325+ return false, e
326+ }
327+ if s == nil {
328+ return false, ErrClosed
329+ }
330+ s.mu.Lock()
331+ defer s.mu.Unlock()
332+ if s.h == 0 {
333+ return false, ErrClosed
334+ }
335+ var b []byte
336+ if len(name) < len(s.name) {
337+ // Heap-resident scratch owned by State and guarded by s.mu.
338+ b = s.name[:len(name)+1]
339+ copy(b, name)
340+ b[len(name)] = 0
341+ } else {
342+ b = cstr(name)
343+ }
344+ v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)
345+ runtime.KeepAlive(b)
346+ return v > 0, nil
347+}
348+
287 func (s *State) Mods() (uint32, error) {349 func (s *State) Mods() (uint32, error) {
288 if s == nil {350 if s == nil {
289 return 0, ErrClosed351 return 0, ErrClosed
@@ -308,8 +370,8 @@ func (s *State) KeySym(keycode uint32) (uint32, error) {
308 }370 }
309 371
310 // sizedUTF8 probes the required byte count (excluding NUL) and retries if it grows.372 // sizedUTF8 probes the required byte count (excluding NUL) and retries if it grows.
311-func sizedUTF8(call func(uintptr, uintptr) int32) (string, error) {373+func sizedUTF8(call func(unsafe.Pointer, uintptr) int32) (string, error) {
312- n := call(0, 0)374+ n := call(nil, 0)
313 if n < 0 {375 if n < 0 {
314 return "", errors.New("xkbcommon: UTF-8 conversion failed")376 return "", errors.New("xkbcommon: UTF-8 conversion failed")
315 }377 }
@@ -343,16 +405,14 @@ const MaxUTF8Buffer = 512
343 var ErrUTF8Buffer = fmt.Errorf("xkbcommon: UTF-8 buffer length must be 1..%d", MaxUTF8Buffer)405 var ErrUTF8Buffer = fmt.Errorf("xkbcommon: UTF-8 buffer length must be 1..%d", MaxUTF8Buffer)
344 406
345 // utf8Into never creates a string or keeps the caller's storage. Native writes407 // utf8Into never creates a string or keeps the caller's storage. Native writes
346-// are synchronous; the backing array is pinned only for the duration of the call.408+// are synchronous; the Syscall uintptr conversion keeps dst's backing array
409+// alive and unmoved for the duration of the call (go:uintptrescapes).
347 // Short buffers are wiped rather than exposing truncated UTF-8 or credentials.410 // Short buffers are wiped rather than exposing truncated UTF-8 or credentials.
348 // Callers clear dst before any early return.411 // Callers clear dst before any early return.
349-func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {412+func utf8Into(dst []byte, call func(unsafe.Pointer, uintptr) int32) (int, error) {
350 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {413 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {
351 return 0, ErrUTF8Buffer414 return 0, ErrUTF8Buffer
352 }415 }
353- var pin runtime.Pinner
354- pin.Pin(&dst[0])
355- defer pin.Unpin()
356 n := call(ptr(dst), uintptr(len(dst)))416 n := call(ptr(dst), uintptr(len(dst)))
357 runtime.KeepAlive(dst)417 runtime.KeepAlive(dst)
358 if n < 0 {418 if n < 0 {
@@ -368,7 +428,8 @@ func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
368 }428 }
369 429
370 // UTF8Into writes into caller-owned mutable storage without constructing a430 // UTF8Into writes into caller-owned mutable storage without constructing a
371-// string. n excludes NUL; on io.ErrShortBuffer n is the required payload size,431+// string. dst is passed to C through a uintptr-escaping call, so its backing
432+// array lives on the heap (a stack array sliced into dst is moved there once). n excludes NUL; on io.ErrShortBuffer n is the required payload size,
372 // all of dst is cleared, and the caller needs n+1 bytes. dst must have length433 // all of dst is cleared, and the caller needs n+1 bytes. dst must have length
373 // 1..MaxUTF8Buffer. No storage is retained; the caller owns wiping after use.434 // 1..MaxUTF8Buffer. No storage is retained; the caller owns wiping after use.
374 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {435 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
@@ -381,7 +442,7 @@ func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
381 if s.h == 0 {442 if s.h == 0 {
382 return 0, ErrClosed443 return 0, ErrClosed
383 }444 }
384- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })445+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
385 }446 }
386 447
387 func (s *State) UTF8(keycode uint32) (string, error) {448 func (s *State) UTF8(keycode uint32) (string, error) {
@@ -393,13 +454,23 @@ func (s *State) UTF8(keycode uint32) (string, error) {
393 if s.h == 0 {454 if s.h == 0 {
394 return "", ErrClosed455 return "", ErrClosed
395 }456 }
396- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })457+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_state_key_get_utf8(s.h, keycode, p, n) })
397 }458 }
398 func KeysymName(sym uint32) (string, error) {459 func KeysymName(sym uint32) (string, error) {
399 if e := Available(); e != nil {460 if e := Available(); e != nil {
400 return "", e461 return "", e
401 }462 }
402- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })463+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
464+}
465+
466+// KeysymNameInto writes the keysym name into dst without allocating, with the
467+// same bounds and short-buffer semantics as State.UTF8Into.
468+func KeysymNameInto(sym uint32, dst []byte) (int, error) {
469+ clear(dst)
470+ if e := Available(); e != nil {
471+ return 0, e
472+ }
473+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
403 }474 }
404 func KeysymFromName(name string) (uint32, error) {475 func KeysymFromName(name string) (uint32, error) {
405 if e := Available(); e != nil {476 if e := Available(); e != nil {
@@ -529,7 +600,7 @@ func (s *ComposeState) UTF8Into(dst []byte) (int, error) {
529 if s.h == 0 {600 if s.h == 0 {
530 return 0, ErrClosed601 return 0, ErrClosed
531 }602 }
532- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })603+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
533 }604 }
534 605
535 func (s *ComposeState) UTF8() (string, error) {606 func (s *ComposeState) UTF8() (string, error) {
@@ -541,7 +612,7 @@ func (s *ComposeState) UTF8() (string, error) {
541 if s.h == 0 {612 if s.h == 0 {
542 return "", ErrClosed613 return "", ErrClosed
543 }614 }
544- return sizedUTF8(func(p, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })615+ return sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_compose_state_get_utf8(s.h, p, n) })
545 }616 }
546 func (s *ComposeState) KeySym() (uint32, error) {617 func (s *ComposeState) KeySym() (uint32, error) {
547 if s == nil {618 if s == nil {
M xkbcommon_test.go +3 -3
@@ -198,7 +198,7 @@ func TestLayoutsAndDifferential(t *testing.T) {
198198 if nativeSym != sym {
199199 t.Fatalf("native sym %d != %d", nativeSym, sym)
200200 }
201- nativeN := loader.f.Xkb_state_key_get_utf8(s.h, tc.code, 0, 0)
201+ nativeN := loader.f.Xkb_state_key_get_utf8(s.h, tc.code, nil, 0)
202202 if nativeN != int32(len(got)) {
203203 t.Fatalf("native bytes %d != %d", nativeN, len(got))
204204 }
@@ -387,11 +387,11 @@ func TestCompose(t *testing.T) {
387387 }
388388 }
389389 func TestSizing(t *testing.T) {
390- v, e := sizedUTF8(func(p, n uintptr) int32 { return 0 })
390+ v, e := sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return 0 })
391391 if e != nil || v != "" {
392392 t.Fatalf("%q %v", v, e)
393393 }
394- _, e = sizedUTF8(func(p, n uintptr) int32 { return -1 })
394+ _, e = sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return -1 })
395395 if e == nil {
396396 t.Fatal("negative result accepted")
397397 }
@@ -198,7 +198,7 @@ func TestLayoutsAndDifferential(t *testing.T) {
198 if nativeSym != sym {198 if nativeSym != sym {
199 t.Fatalf("native sym %d != %d", nativeSym, sym)199 t.Fatalf("native sym %d != %d", nativeSym, sym)
200 }200 }
201- nativeN := loader.f.Xkb_state_key_get_utf8(s.h, tc.code, 0, 0)201+ nativeN := loader.f.Xkb_state_key_get_utf8(s.h, tc.code, nil, 0)
202 if nativeN != int32(len(got)) {202 if nativeN != int32(len(got)) {
203 t.Fatalf("native bytes %d != %d", nativeN, len(got))203 t.Fatalf("native bytes %d != %d", nativeN, len(got))
204 }204 }
@@ -387,11 +387,11 @@ func TestCompose(t *testing.T) {
387 }387 }
388 }388 }
389 func TestSizing(t *testing.T) {389 func TestSizing(t *testing.T) {
390- v, e := sizedUTF8(func(p, n uintptr) int32 { return 0 })390+ v, e := sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return 0 })
391 if e != nil || v != "" {391 if e != nil || v != "" {
392 t.Fatalf("%q %v", v, e)392 t.Fatalf("%q %v", v, e)
393 }393 }
394- _, e = sizedUTF8(func(p, n uintptr) int32 { return -1 })394+ _, e = sizedUTF8(func(p unsafe.Pointer, n uintptr) int32 { return -1 })
395 if e == nil {395 if e == nil {
396 t.Fatal("negative result accepted")396 t.Fatal("negative result accepted")
397 }397 }

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