detached a0f4c49

fix: heap-resident scratch for modifier names; broaden tests; document raw API change

State owns a mutex-guarded name scratch for ModNameActive. Adds tests for
long modifier names, KeysymNameInto short buffers, invalid mod indices,
RMLVO rule names and zero raw.Funcs, and documents the v0.2.0 raw changes.
commit
a0f4c49a8b00a38e8d82bcbb5fd8c93201476f42
author
brice · · 2026-10-01 18:50 UTC
parent
c0b0a8f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +147 -30
M README.md +11 -0
@@ -24,3 +24,14 @@ Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and
2424 ## Allocation-free hot path
2525
2626 `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 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.
@@ -24,3 +24,14 @@ Pinned upstream headers and licenses are in `upstream/`; `upstream/METADATA` and
24 ## Allocation-free hot path24 ## Allocation-free hot path
25 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 string-returning `UTF8` and `KeysymName` still allocate their result.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 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.
M alloc_test.go +101 -0
@@ -1,7 +1,12 @@
11 package xkbcommon
22
33 import (
4+ "bytes"
5+ "errors"
6+ "io"
47 "os"
8+ "runtime"
9+ "strings"
510 "testing"
611
712 "github.com/bnema/purego-xkbcommon/raw"
@@ -118,3 +123,99 @@ func TestHotPathValues(t *testing.T) {
118123 t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)
119124 }
120125 }
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+}
@@ -1,7 +1,12 @@
1 package xkbcommon1 package xkbcommon
2 2
3 import (3 import (
4+ "bytes"
5+ "errors"
6+ "io"
4 "os"7 "os"
8+ "runtime"
9+ "strings"
5 "testing"10 "testing"
6 11
7 "github.com/bnema/purego-xkbcommon/raw"12 "github.com/bnema/purego-xkbcommon/raw"
@@ -118,3 +123,99 @@ func TestHotPathValues(t *testing.T) {
118 t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)123 t.Fatalf("KeysymNameInto = %q %v", buf[:n], e)
119 }124 }
120 }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 xkbcommon.go +32 -27
@@ -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
@@ -253,6 +257,7 @@ func (k *Keymap) NewState() (*State, error) {
253257 type State struct {
254258 mu sync.Mutex
255259 h, keymap uintptr
260+ name [64]byte // NUL-terminated modifier-name scratch, guarded by mu
256261 }
257262
258263 func (s *State) Close() error {
@@ -322,19 +327,20 @@ func (s *State) ModNameActive(name string) (bool, error) {
322327 if s == nil {
323328 return false, ErrClosed
324329 }
325- var small [64]byte
326- var b []byte
327- if len(name) < len(small) {
328- copy(small[:], name)
329- b = small[:len(name)+1]
330- } else {
331- b = cstr(name)
332- }
333330 s.mu.Lock()
334331 defer s.mu.Unlock()
335332 if s.h == 0 {
336333 return false, ErrClosed
337334 }
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+ }
338344 v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)
339345 runtime.KeepAlive(b)
340346 return v > 0, nil
@@ -364,8 +370,8 @@ func (s *State) KeySym(keycode uint32) (uint32, error) {
364370 }
365371
366372 // sizedUTF8 probes the required byte count (excluding NUL) and retries if it grows.
367-func sizedUTF8(call func(uintptr, uintptr) int32) (string, error) {
368- n := call(0, 0)
373+func sizedUTF8(call func(unsafe.Pointer, uintptr) int32) (string, error) {
374+ n := call(nil, 0)
369375 if n < 0 {
370376 return "", errors.New("xkbcommon: UTF-8 conversion failed")
371377 }
@@ -399,16 +405,14 @@ const MaxUTF8Buffer = 512
399405 var ErrUTF8Buffer = fmt.Errorf("xkbcommon: UTF-8 buffer length must be 1..%d", MaxUTF8Buffer)
400406
401407 // utf8Into never creates a string or keeps the caller's storage. Native writes
402-// 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).
403410 // Short buffers are wiped rather than exposing truncated UTF-8 or credentials.
404411 // Callers clear dst before any early return.
405-func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
412+func utf8Into(dst []byte, call func(unsafe.Pointer, uintptr) int32) (int, error) {
406413 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {
407414 return 0, ErrUTF8Buffer
408415 }
409- var pin runtime.Pinner
410- pin.Pin(&dst[0])
411- defer pin.Unpin()
412416 n := call(ptr(dst), uintptr(len(dst)))
413417 runtime.KeepAlive(dst)
414418 if n < 0 {
@@ -424,7 +428,8 @@ func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
424428 }
425429
426430 // UTF8Into writes into caller-owned mutable storage without constructing a
427-// 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,
428433 // all of dst is cleared, and the caller needs n+1 bytes. dst must have length
429434 // 1..MaxUTF8Buffer. No storage is retained; the caller owns wiping after use.
430435 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
@@ -437,7 +442,7 @@ func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
437442 if s.h == 0 {
438443 return 0, ErrClosed
439444 }
440- 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) })
441446 }
442447
443448 func (s *State) UTF8(keycode uint32) (string, error) {
@@ -449,13 +454,13 @@ func (s *State) UTF8(keycode uint32) (string, error) {
449454 if s.h == 0 {
450455 return "", ErrClosed
451456 }
452- 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) })
453458 }
454459 func KeysymName(sym uint32) (string, error) {
455460 if e := Available(); e != nil {
456461 return "", e
457462 }
458- 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) })
459464 }
460465
461466 // KeysymNameInto writes the keysym name into dst without allocating, with the
@@ -465,7 +470,7 @@ func KeysymNameInto(sym uint32, dst []byte) (int, error) {
465470 if e := Available(); e != nil {
466471 return 0, e
467472 }
468- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
473+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
469474 }
470475 func KeysymFromName(name string) (uint32, error) {
471476 if e := Available(); e != nil {
@@ -595,7 +600,7 @@ func (s *ComposeState) UTF8Into(dst []byte) (int, error) {
595600 if s.h == 0 {
596601 return 0, ErrClosed
597602 }
598- 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) })
599604 }
600605
601606 func (s *ComposeState) UTF8() (string, error) {
@@ -607,7 +612,7 @@ func (s *ComposeState) UTF8() (string, error) {
607612 if s.h == 0 {
608613 return "", ErrClosed
609614 }
610- 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) })
611616 }
612617 func (s *ComposeState) KeySym() (uint32, error) {
613618 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
@@ -253,6 +257,7 @@ func (k *Keymap) NewState() (*State, error) {
253 type State struct {257 type State struct {
254 mu sync.Mutex258 mu sync.Mutex
255 h, keymap uintptr259 h, keymap uintptr
260+ name [64]byte // NUL-terminated modifier-name scratch, guarded by mu
256 }261 }
257 262
258 func (s *State) Close() error {263 func (s *State) Close() error {
@@ -322,19 +327,20 @@ func (s *State) ModNameActive(name string) (bool, error) {
322 if s == nil {327 if s == nil {
323 return false, ErrClosed328 return false, ErrClosed
324 }329 }
325- var small [64]byte
326- var b []byte
327- if len(name) < len(small) {
328- copy(small[:], name)
329- b = small[:len(name)+1]
330- } else {
331- b = cstr(name)
332- }
333 s.mu.Lock()330 s.mu.Lock()
334 defer s.mu.Unlock()331 defer s.mu.Unlock()
335 if s.h == 0 {332 if s.h == 0 {
336 return false, ErrClosed333 return false, ErrClosed
337 }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+ }
338 v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)344 v := loader.f.Xkb_state_mod_name_is_active(s.h, ptr(b), raw.XKB_STATE_MODS_EFFECTIVE)
339 runtime.KeepAlive(b)345 runtime.KeepAlive(b)
340 return v > 0, nil346 return v > 0, nil
@@ -364,8 +370,8 @@ func (s *State) KeySym(keycode uint32) (uint32, error) {
364 }370 }
365 371
366 // 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.
367-func sizedUTF8(call func(uintptr, uintptr) int32) (string, error) {373+func sizedUTF8(call func(unsafe.Pointer, uintptr) int32) (string, error) {
368- n := call(0, 0)374+ n := call(nil, 0)
369 if n < 0 {375 if n < 0 {
370 return "", errors.New("xkbcommon: UTF-8 conversion failed")376 return "", errors.New("xkbcommon: UTF-8 conversion failed")
371 }377 }
@@ -399,16 +405,14 @@ const MaxUTF8Buffer = 512
399 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)
400 406
401 // 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
402-// 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).
403 // 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.
404 // Callers clear dst before any early return.411 // Callers clear dst before any early return.
405-func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {412+func utf8Into(dst []byte, call func(unsafe.Pointer, uintptr) int32) (int, error) {
406 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {413 if len(dst) < 1 || len(dst) > MaxUTF8Buffer {
407 return 0, ErrUTF8Buffer414 return 0, ErrUTF8Buffer
408 }415 }
409- var pin runtime.Pinner
410- pin.Pin(&dst[0])
411- defer pin.Unpin()
412 n := call(ptr(dst), uintptr(len(dst)))416 n := call(ptr(dst), uintptr(len(dst)))
413 runtime.KeepAlive(dst)417 runtime.KeepAlive(dst)
414 if n < 0 {418 if n < 0 {
@@ -424,7 +428,8 @@ func utf8Into(dst []byte, call func(uintptr, uintptr) int32) (int, error) {
424 }428 }
425 429
426 // UTF8Into writes into caller-owned mutable storage without constructing a430 // UTF8Into writes into caller-owned mutable storage without constructing a
427-// 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,
428 // 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
429 // 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.
430 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {435 func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
@@ -437,7 +442,7 @@ func (s *State) UTF8Into(keycode uint32, dst []byte) (int, error) {
437 if s.h == 0 {442 if s.h == 0 {
438 return 0, ErrClosed443 return 0, ErrClosed
439 }444 }
440- 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) })
441 }446 }
442 447
443 func (s *State) UTF8(keycode uint32) (string, error) {448 func (s *State) UTF8(keycode uint32) (string, error) {
@@ -449,13 +454,13 @@ func (s *State) UTF8(keycode uint32) (string, error) {
449 if s.h == 0 {454 if s.h == 0 {
450 return "", ErrClosed455 return "", ErrClosed
451 }456 }
452- 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) })
453 }458 }
454 func KeysymName(sym uint32) (string, error) {459 func KeysymName(sym uint32) (string, error) {
455 if e := Available(); e != nil {460 if e := Available(); e != nil {
456 return "", e461 return "", e
457 }462 }
458- 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) })
459 }464 }
460 465
461 // KeysymNameInto writes the keysym name into dst without allocating, with the466 // KeysymNameInto writes the keysym name into dst without allocating, with the
@@ -465,7 +470,7 @@ func KeysymNameInto(sym uint32, dst []byte) (int, error) {
465 if e := Available(); e != nil {470 if e := Available(); e != nil {
466 return 0, e471 return 0, e
467 }472 }
468- return utf8Into(dst, func(p, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })473+ return utf8Into(dst, func(p unsafe.Pointer, n uintptr) int32 { return loader.f.Xkb_keysym_get_name(sym, p, n) })
469 }474 }
470 func KeysymFromName(name string) (uint32, error) {475 func KeysymFromName(name string) (uint32, error) {
471 if e := Available(); e != nil {476 if e := Available(); e != nil {
@@ -595,7 +600,7 @@ func (s *ComposeState) UTF8Into(dst []byte) (int, error) {
595 if s.h == 0 {600 if s.h == 0 {
596 return 0, ErrClosed601 return 0, ErrClosed
597 }602 }
598- 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) })
599 }604 }
600 605
601 func (s *ComposeState) UTF8() (string, error) {606 func (s *ComposeState) UTF8() (string, error) {
@@ -607,7 +612,7 @@ func (s *ComposeState) UTF8() (string, error) {
607 if s.h == 0 {612 if s.h == 0 {
608 return "", ErrClosed613 return "", ErrClosed
609 }614 }
610- 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) })
611 }616 }
612 func (s *ComposeState) KeySym() (uint32, error) {617 func (s *ComposeState) KeySym() (uint32, error) {
613 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