detached d4696de

Merge pull request #6 from bnema/feat/wl-fixes

feat(server): support wl_fixes registry destruction and global removal acks
commit
d4696de0766403c79dda095e32186023ce87e1c7
author
bnema · · 2026-09-29 05:44 UTC
committer
GitHub · · 2026-09-29 05:44 UTC
merge of
ad674f5 + 57e7c0d · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.6.0
4 files · +63 -5
M server/args.go +16 -5
@@ -67,11 +67,22 @@ func Array(p *runtime.Pinner, b []byte) Arg {
6767
6868 // Request argument accessors. Values are only valid during dispatch; the
6969 // String and Array accessors copy.
70-func (a Arg) Uint() uint32 { return uint32(a) }
71-func (a Arg) Int() int32 { return int32(uint32(a)) }
72-func (a Arg) Fixed() Fixed { return Fixed(int32(uint32(a))) }
73-func (a Arg) NewID() uint32 { return uint32(a) }
74-func (a Arg) Resource() *Resource { return live.resources[uintptr(a)] }
70+func (a Arg) Uint() uint32 { return uint32(a) }
71+func (a Arg) Int() int32 { return int32(uint32(a)) }
72+func (a Arg) Fixed() Fixed { return Fixed(int32(uint32(a))) }
73+func (a Arg) NewID() uint32 { return uint32(a) }
74+
75+// Resource returns the object argument, nil for a null object. An object
76+// libwayland created itself (wl_registry, wl_callback of wl_display) comes
77+// back as a bare handle: Destroy works, it has no handler, version or client.
78+// Like other arguments, a bare handle is only valid during the dispatch; do
79+// not retain it.
80+func (a Arg) Resource() *Resource {
81+ if r := live.resources[uintptr(a)]; r != nil || a == 0 {
82+ return r
83+ }
84+ return &Resource{c: uintptr(a), bare: true}
85+}
7586
7687 // Fd returns a received file descriptor. The handler owns it and must close it.
7788 func (a Arg) Fd() int { return int(int32(uint32(a))) }
@@ -67,11 +67,22 @@ func Array(p *runtime.Pinner, b []byte) Arg {
67 67
68 // Request argument accessors. Values are only valid during dispatch; the68 // Request argument accessors. Values are only valid during dispatch; the
69 // String and Array accessors copy.69 // String and Array accessors copy.
70-func (a Arg) Uint() uint32 { return uint32(a) }70+func (a Arg) Uint() uint32 { return uint32(a) }
71-func (a Arg) Int() int32 { return int32(uint32(a)) }71+func (a Arg) Int() int32 { return int32(uint32(a)) }
72-func (a Arg) Fixed() Fixed { return Fixed(int32(uint32(a))) }72+func (a Arg) Fixed() Fixed { return Fixed(int32(uint32(a))) }
73-func (a Arg) NewID() uint32 { return uint32(a) }73+func (a Arg) NewID() uint32 { return uint32(a) }
74-func (a Arg) Resource() *Resource { return live.resources[uintptr(a)] }74+
75+// Resource returns the object argument, nil for a null object. An object
76+// libwayland created itself (wl_registry, wl_callback of wl_display) comes
77+// back as a bare handle: Destroy works, it has no handler, version or client.
78+// Like other arguments, a bare handle is only valid during the dispatch; do
79+// not retain it.
80+func (a Arg) Resource() *Resource {
81+ if r := live.resources[uintptr(a)]; r != nil || a == 0 {
82+ return r
83+ }
84+ return &Resource{c: uintptr(a), bare: true}
85+}
75 86
76 // Fd returns a received file descriptor. The handler owns it and must close it.87 // Fd returns a received file descriptor. The handler owns it and must close it.
77 func (a Arg) Fd() int { return int(int32(uint32(a))) }88 func (a Arg) Fd() int { return int(int32(uint32(a))) }
M server/display.go +19 -0
@@ -32,6 +32,7 @@ type Resource struct {
3232 handler Handler
3333 OnDestroy func()
3434 gone bool
35+ bare bool // created by libwayland, no destroy callback
3536
3637 // Data holds the owner's state, typically the generated wrapper.
3738 Data any
@@ -127,6 +128,22 @@ func (g *Global) Remove() {
127128 g.c = 0
128129 }
129130
131+// AckGlobalRemove implements wl_fixes.ack_global_remove: libwayland checks
132+// that registry announced the removed global name, posting
133+// wl_fixes.invalid_ack_remove on fixes otherwise. It reports false when
134+// libwayland-server is older than 1.26 and has no support.
135+// Display goroutine only.
136+func AckGlobalRemove(fixes, registry *Resource, name uint32) bool {
137+ if symFixesAckGlobalRemove == 0 {
138+ return false
139+ }
140+ if fixes == nil || fixes.gone || registry == nil || registry.gone {
141+ return true
142+ }
143+ wlFixesHandleAckGlobalRemove(fixes.c, registry.c, name)
144+ return true
145+}
146+
130147 // Close destroys a display that has not started Run. It is a no-op once Run
131148 // has started; Run owns destruction in that case. Call before Run starts.
132149 func (d *Display) Close() {
@@ -270,6 +287,8 @@ func (r *Resource) Iface() *Interface { return r.iface }
270287 func (r *Resource) Destroy() {
271288 if !r.gone {
272289 wlResourceDestroy(r.c)
290+ // A bare handle gets no destroy callback to mark it gone.
291+ r.gone = r.gone || r.bare
273292 }
274293 }
275294
@@ -32,6 +32,7 @@ type Resource struct {
32 handler Handler32 handler Handler
33 OnDestroy func()33 OnDestroy func()
34 gone bool34 gone bool
35+ bare bool // created by libwayland, no destroy callback
35 36
36 // Data holds the owner's state, typically the generated wrapper.37 // Data holds the owner's state, typically the generated wrapper.
37 Data any38 Data any
@@ -127,6 +128,22 @@ func (g *Global) Remove() {
127 g.c = 0128 g.c = 0
128 }129 }
129 130
131+// AckGlobalRemove implements wl_fixes.ack_global_remove: libwayland checks
132+// that registry announced the removed global name, posting
133+// wl_fixes.invalid_ack_remove on fixes otherwise. It reports false when
134+// libwayland-server is older than 1.26 and has no support.
135+// Display goroutine only.
136+func AckGlobalRemove(fixes, registry *Resource, name uint32) bool {
137+ if symFixesAckGlobalRemove == 0 {
138+ return false
139+ }
140+ if fixes == nil || fixes.gone || registry == nil || registry.gone {
141+ return true
142+ }
143+ wlFixesHandleAckGlobalRemove(fixes.c, registry.c, name)
144+ return true
145+}
146+
130 // Close destroys a display that has not started Run. It is a no-op once Run147 // Close destroys a display that has not started Run. It is a no-op once Run
131 // has started; Run owns destruction in that case. Call before Run starts.148 // has started; Run owns destruction in that case. Call before Run starts.
132 func (d *Display) Close() {149 func (d *Display) Close() {
@@ -270,6 +287,8 @@ func (r *Resource) Iface() *Interface { return r.iface }
270 func (r *Resource) Destroy() {287 func (r *Resource) Destroy() {
271 if !r.gone {288 if !r.gone {
272 wlResourceDestroy(r.c)289 wlResourceDestroy(r.c)
290+ // A bare handle gets no destroy callback to mark it gone.
291+ r.gone = r.gone || r.bare
273 }292 }
274 }293 }
275 294
M server/display_test.go +22 -0
@@ -38,6 +38,28 @@ func TestNoHandlerClosesFDs(t *testing.T) {
3838 }
3939 }
4040
41+// Objects libwayland created itself (wl_registry) come back as bare handles.
42+func TestArgResourceUntracked(t *testing.T) {
43+ if Arg(0).Resource() != nil {
44+ t.Fatal("null object is not nil")
45+ }
46+ r := Arg(0x1234).Resource()
47+ if r == nil || r.c != 0x1234 || r.handler != nil || !r.bare || !r.Alive() {
48+ t.Fatalf("untracked object %+v", r)
49+ }
50+}
51+
52+func TestAckGlobalRemoveNilResources(t *testing.T) {
53+ if err := load(); err != nil {
54+ t.Skip(err)
55+ }
56+ // A dead or missing resource is ignored, whatever the library version.
57+ supported := AckGlobalRemove(nil, nil, 1)
58+ if supported != (symFixesAckGlobalRemove != 0) {
59+ t.Fatalf("supported %v with symbol %#x", supported, symFixesAckGlobalRemove)
60+ }
61+}
62+
4163 func TestDoStopped(t *testing.T) {
4264 d, err := NewDisplay()
4365 if err != nil {
@@ -38,6 +38,28 @@ func TestNoHandlerClosesFDs(t *testing.T) {
38 }38 }
39 }39 }
40 40
41+// Objects libwayland created itself (wl_registry) come back as bare handles.
42+func TestArgResourceUntracked(t *testing.T) {
43+ if Arg(0).Resource() != nil {
44+ t.Fatal("null object is not nil")
45+ }
46+ r := Arg(0x1234).Resource()
47+ if r == nil || r.c != 0x1234 || r.handler != nil || !r.bare || !r.Alive() {
48+ t.Fatalf("untracked object %+v", r)
49+ }
50+}
51+
52+func TestAckGlobalRemoveNilResources(t *testing.T) {
53+ if err := load(); err != nil {
54+ t.Skip(err)
55+ }
56+ // A dead or missing resource is ignored, whatever the library version.
57+ supported := AckGlobalRemove(nil, nil, 1)
58+ if supported != (symFixesAckGlobalRemove != 0) {
59+ t.Fatalf("supported %v with symbol %#x", supported, symFixesAckGlobalRemove)
60+ }
61+}
62+
41 func TestDoStopped(t *testing.T) {63 func TestDoStopped(t *testing.T) {
42 d, err := NewDisplay()64 d, err := NewDisplay()
43 if err != nil {65 if err != nil {
M server/lib.go +6 -0
@@ -32,6 +32,8 @@ var (
3232 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
3333 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
3434 symErrnoLocation uintptr
35+ // wl_fixes support (libwayland 1.26+); zero on older libraries.
36+ symFixesAckGlobalRemove uintptr
3537
3638 // Shared C callbacks. purego callbacks are never freed, so there is one
3739 // of each for the whole process.
@@ -84,6 +86,7 @@ func load() error {
8486 return
8587 }
8688 }
89+ symFixesAckGlobalRemove, _ = purego.Dlsym(lib, "wl_fixes_handle_ack_global_remove")
8790 symErrnoLocation, err = purego.Dlsym(lib, "__errno_location")
8891 if err != nil {
8992 libc, openErr := purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_GLOBAL)
@@ -148,3 +151,6 @@ func wlResourcePostEventArr(resource uintptr, opcode uint32, args unsafe.Pointer
148151 func wlResourceDestroy(resource uintptr) {
149152 purego.Syscall6(symResourceDestroy, resource, 0, 0, 0, 0, 0)
150153 }
154+func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
155+ purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
156+}
@@ -32,6 +32,8 @@ var (
32 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr32 symDisplayFlushClients, symEventLoopDispatch, symResourceCreate uintptr
33 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr33 symResourceSetDispatcher, symResourcePostEventArr, symResourceDestroy, symResourceGetID uintptr
34 symErrnoLocation uintptr34 symErrnoLocation uintptr
35+ // wl_fixes support (libwayland 1.26+); zero on older libraries.
36+ symFixesAckGlobalRemove uintptr
35 37
36 // Shared C callbacks. purego callbacks are never freed, so there is one38 // Shared C callbacks. purego callbacks are never freed, so there is one
37 // of each for the whole process.39 // of each for the whole process.
@@ -84,6 +86,7 @@ func load() error {
84 return86 return
85 }87 }
86 }88 }
89+ symFixesAckGlobalRemove, _ = purego.Dlsym(lib, "wl_fixes_handle_ack_global_remove")
87 symErrnoLocation, err = purego.Dlsym(lib, "__errno_location")90 symErrnoLocation, err = purego.Dlsym(lib, "__errno_location")
88 if err != nil {91 if err != nil {
89 libc, openErr := purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_GLOBAL)92 libc, openErr := purego.Dlopen("libc.so.6", purego.RTLD_NOW|purego.RTLD_GLOBAL)
@@ -148,3 +151,6 @@ func wlResourcePostEventArr(resource uintptr, opcode uint32, args unsafe.Pointer
148 func wlResourceDestroy(resource uintptr) {151 func wlResourceDestroy(resource uintptr) {
149 purego.Syscall6(symResourceDestroy, resource, 0, 0, 0, 0, 0)152 purego.Syscall6(symResourceDestroy, resource, 0, 0, 0, 0, 0)
150 }153 }
154+func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
155+ purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
156+}

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 14

↑↓ moveenter openesc close