detached 0db760d

Merge e13fc3637de80cc2cf1b59725cbdab734f284829 into f5b8db49413730383c33654512df6fac6fba210e

commit
0db760d0cbb037334d4b522908393ef69f11e672
author
ermite · · 2026-10-22 05:50 UTC
merge of
f5b8db4 + e13fc36 · diff against the first parent
signature
unsigned
4 files · +366 -21
M README.md +18 -0
@@ -20,6 +20,24 @@ CGO_ENABLED=0 go build ./...
2020
2121 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.
2222
23+## Display integration
24+
25+`Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
26+and bind each global; passing `nil` removes the filter. Set it before clients
27+receive restricted globals. Global handles are stable, including during creation;
28+filters must not create globals or block.
29+
30+`Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
31+writable interest initially disabled. `Source.SetWritable(true)` arms writable
32+callbacks after `EAGAIN`; `SetWritable(false)` disarms them when output drains.
33+Both return errors on registration/update failures. Readable interest remains
34+active. Callbacks run on the display goroutine with no dispatch allocations.
35+`AddFD` remains the readable-only API. Hangup/error readiness calls onReadable;
36+remove the source on EOF/error. The caller owns the original fd.
37+
38+These APIs, like other libwayland operations, belong on the display's OS-locked
39+goroutine (use `Do` or `Post` from other goroutines).
40+
2341 ## License
2442
2543 MIT, see `LICENSE`.
@@ -20,6 +20,24 @@ CGO_ENABLED=0 go build ./...
20 20
21 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.21 Generated protocol bindings live in [github.com/bnema/go-wayland-bindings](https://github.com/bnema/go-wayland-bindings), one package per protocol under `server/<pkg>`, for example `server/xdgshell` and `server/wlrlayershell`. They build on this runtime's `server` package.
22 22
23+## Display integration
24+
25+`Display.SetGlobalFilter(func(Client, *Global) bool)` controls which clients see
26+and bind each global; passing `nil` removes the filter. Set it before clients
27+receive restricted globals. Global handles are stable, including during creation;
28+filters must not create globals or block.
29+
30+`Display.AddFDRW(fd, onReadable, onWritable)` registers a non-blocking fd with
31+writable interest initially disabled. `Source.SetWritable(true)` arms writable
32+callbacks after `EAGAIN`; `SetWritable(false)` disarms them when output drains.
33+Both return errors on registration/update failures. Readable interest remains
34+active. Callbacks run on the display goroutine with no dispatch allocations.
35+`AddFD` remains the readable-only API. Hangup/error readiness calls onReadable;
36+remove the source on EOF/error. The caller owns the original fd.
37+
38+These APIs, like other libwayland operations, belong on the display's OS-locked
39+goroutine (use `Do` or `Post` from other goroutines).
40+
23 ## License41 ## License
24 42
25 MIT, see `LICENSE`.43 MIT, see `LICENSE`.
M server/display.go +100 -15
@@ -50,10 +50,12 @@ var live = struct {
5050 nextBind uintptr
5151 // clientGone maps wl_listener addresses to OnDestroy functions.
5252 clientGone map[uintptr]func()
53- // fds maps AddFD tokens to their readable callbacks.
54- fds map[uintptr]func()
55- nextFD uintptr
56-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]func(){}}
53+ // fds maps AddFD/AddFDRW tokens to their sources.
54+ fds map[uintptr]*Source
55+ nextFD uintptr
56+ globals map[uintptr]*Global
57+ displays map[uintptr]*Display
58+}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]*Source{}, globals: map[uintptr]*Global{}, displays: map[uintptr]*Display{}}
5759
5860 // Display owns a wl_display. All methods except Do and Post must run on the
5961 // goroutine that calls Run, or before Run starts.
@@ -88,7 +90,9 @@ type Display struct {
8890 wakeWrites atomic.Uint64
8991
9092 // sources are the AddFD sources not yet removed (display goroutine only).
91- sources map[*Source]struct{}
93+ sources map[*Source]struct{}
94+ filter func(Client, *Global) bool
95+ pendingGlobal *Global
9296 }
9397
9498 // maxDrainRounds bounds how many times one loop iteration re-drains calls and
@@ -116,6 +120,7 @@ func NewDisplay() (*Display, error) {
116120 wlDisplayDestroy(c)
117121 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
118122 }
123+ live.displays[c] = d
119124 return d, nil
120125 }
121126
@@ -133,19 +138,56 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
133138 }
134139
135140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
136-type Global struct{ c uintptr }
141+type Global struct {
142+ c uintptr
143+ d *Display
144+}
145+
146+// SetGlobalFilter controls global visibility and binding for each client. nil
147+// removes the filter. Install it before advertising restricted globals; it does
148+// not retract announcements already sent. The callback must not block or create
149+// globals. Display goroutine only.
150+func (d *Display) SetGlobalFilter(fn func(Client, *Global) bool) {
151+ d.filter = fn
152+ callback := uintptr(0)
153+ if fn != nil {
154+ callback = cbGlobalFilter
155+ }
156+ wlDisplaySetGlobalFilter(d.c, callback, d.c)
157+}
158+
159+func globalFilter(client, global, data uintptr) uint32 {
160+ d := live.displays[data]
161+ if d == nil || d.filter == nil {
162+ return 1
163+ }
164+ g := live.globals[global]
165+ // wl_global_create announces synchronously, before returning its pointer.
166+ if g == nil {
167+ g = d.pendingGlobal
168+ }
169+ if g != nil && d.filter(Client{client}, g) {
170+ return 1
171+ }
172+ return 0
173+}
137174
138175 // AddGlobal advertises a global and returns a handle to remove it later.
139176 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
140177 live.nextBind++
141178 token := live.nextBind
142179 live.binds[token] = bindFn
180+ g := &Global{d: d}
181+ d.pendingGlobal = g
143182 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
183+ d.pendingGlobal = nil
144184 if c == 0 {
145185 delete(live.binds, token)
146186 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
147187 }
148- return &Global{c: c}, nil
188+ g.c = c
189+ live.globals[c] = g
190+ return g, nil
149191 }
150192
151193 // Remove withdraws the global: clients get wl_registry.global_remove.
@@ -204,6 +246,14 @@ func (d *Display) destroy() {
204246 wlDisplayDestroyClients(d.c)
205247 wlEventSourceRemove(d.wakeSrc)
206248 wlDisplayDestroy(d.c)
249+ delete(live.displays, d.c)
250+ for p, g := range live.globals {
251+ if g.d == d {
252+ g.c = 0
253+ delete(live.globals, p)
254+ }
255+ }
256+ d.filter = nil
207257 unix.Close(d.wake)
208258 }
209259
@@ -390,9 +440,10 @@ func (d *Display) Post(fn func()) {
390440
391441 // Source is an fd registered with the display's event loop by AddFD.
392442 type Source struct {
393- d *Display
394- c uintptr
395- token uintptr
443+ d *Display
444+ c uintptr
445+ token uintptr
446+ onReadable, onWritable func()
396447 }
397448
398449 // AddFD calls onReadable on the display goroutine whenever fd is readable. The
@@ -410,6 +461,14 @@ type Source struct {
410461 // goroutine or before Run starts. Sources not removed are released when the
411462 // display is destroyed.
412463 func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
464+ return d.AddFDRW(fd, onReadable, nil)
465+}
466+
467+// AddFDRW is AddFD with an optional writable callback. Writable interest starts
468+// disabled; use SetWritable to arm it after a non-blocking write returns EAGAIN.
469+// Both callbacks must not block. Readable runs first; removing the source there
470+// suppresses writable delivery in the same dispatch.
471+func (d *Display) AddFDRW(fd int, onReadable, onWritable func()) (*Source, error) {
413472 if fd < 0 {
414473 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
415474 }
@@ -421,17 +480,38 @@ func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
421480 }
422481 live.nextFD++
423482 token := live.nextFD
424- live.fds[token] = onReadable
483+ s := &Source{d: d, token: token, onReadable: onReadable, onWritable: onWritable}
484+ live.fds[token] = s
425485 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
426486 if c == 0 {
427487 delete(live.fds, token)
428488 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
429489 }
430- s := &Source{d: d, c: c, token: token}
490+ s.c = c
431491 d.sources[s] = struct{}{}
432492 return s, nil
433493 }
434494
495+// SetWritable toggles writable interest while always retaining readable interest.
496+// Disable it when buffered writes are drained to avoid a busy loop. Display
497+// goroutine only. A removed source is a no-op; enabling requires onWritable.
498+func (s *Source) SetWritable(on bool) error {
499+ if s == nil || s.c == 0 {
500+ return nil
501+ }
502+ mask := uint32(1)
503+ if on {
504+ if s.onWritable == nil {
505+ return errors.New("purego-libwayland: no writable callback")
506+ }
507+ mask |= 2
508+ }
509+ if wlEventSourceFDUpdate(s.c, mask) < 0 {
510+ return lastErrno()
511+ }
512+ return nil
513+}
514+
435515 // Remove stops the callbacks and releases libwayland's duplicate of the fd.
436516 // It may be called from the source's own callback. Display goroutine only;
437517 // idempotent, and a no-op after the display was destroyed.
@@ -658,9 +738,14 @@ func wake(fd int32, mask uint32, data uintptr) int32 {
658738 }
659739
660740 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
661-func fdReadable(_ int32, _ uint32, data uintptr) int32 {
662- if fn := live.fds[data]; fn != nil {
663- fn()
741+func fdReadable(_ int32, mask uint32, data uintptr) int32 {
742+ if s := live.fds[data]; s != nil {
743+ if mask&(1|4|8) != 0 {
744+ s.onReadable()
745+ }
746+ if s.c != 0 && mask&2 != 0 && s.onWritable != nil {
747+ s.onWritable()
748+ }
664749 }
665750 return 0
666751 }
@@ -50,10 +50,12 @@ var live = struct {
50 nextBind uintptr50 nextBind uintptr
51 // clientGone maps wl_listener addresses to OnDestroy functions.51 // clientGone maps wl_listener addresses to OnDestroy functions.
52 clientGone map[uintptr]func()52 clientGone map[uintptr]func()
53- // fds maps AddFD tokens to their readable callbacks.53+ // fds maps AddFD/AddFDRW tokens to their sources.
54- fds map[uintptr]func()54+ fds map[uintptr]*Source
55- nextFD uintptr55+ nextFD uintptr
56-}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]func(){}}56+ globals map[uintptr]*Global
57+ displays map[uintptr]*Display
58+}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}, clientGone: map[uintptr]func(){}, fds: map[uintptr]*Source{}, globals: map[uintptr]*Global{}, displays: map[uintptr]*Display{}}
57 59
58 // Display owns a wl_display. All methods except Do and Post must run on the60 // Display owns a wl_display. All methods except Do and Post must run on the
59 // goroutine that calls Run, or before Run starts.61 // goroutine that calls Run, or before Run starts.
@@ -88,7 +90,9 @@ type Display struct {
88 wakeWrites atomic.Uint6490 wakeWrites atomic.Uint64
89 91
90 // sources are the AddFD sources not yet removed (display goroutine only).92 // sources are the AddFD sources not yet removed (display goroutine only).
91- sources map[*Source]struct{}93+ sources map[*Source]struct{}
94+ filter func(Client, *Global) bool
95+ pendingGlobal *Global
92 }96 }
93 97
94 // maxDrainRounds bounds how many times one loop iteration re-drains calls and98 // maxDrainRounds bounds how many times one loop iteration re-drains calls and
@@ -116,6 +120,7 @@ func NewDisplay() (*Display, error) {
116 wlDisplayDestroy(c)120 wlDisplayDestroy(c)
117 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")121 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
118 }122 }
123+ live.displays[c] = d
119 return d, nil124 return d, nil
120 }125 }
121 126
@@ -133,19 +138,56 @@ func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc)
133 }138 }
134 139
135 // Global is a global that can be withdrawn, such as a wl_output on hotplug.140 // Global is a global that can be withdrawn, such as a wl_output on hotplug.
136-type Global struct{ c uintptr }141+type Global struct {
142+ c uintptr
143+ d *Display
144+}
145+
146+// SetGlobalFilter controls global visibility and binding for each client. nil
147+// removes the filter. Install it before advertising restricted globals; it does
148+// not retract announcements already sent. The callback must not block or create
149+// globals. Display goroutine only.
150+func (d *Display) SetGlobalFilter(fn func(Client, *Global) bool) {
151+ d.filter = fn
152+ callback := uintptr(0)
153+ if fn != nil {
154+ callback = cbGlobalFilter
155+ }
156+ wlDisplaySetGlobalFilter(d.c, callback, d.c)
157+}
158+
159+func globalFilter(client, global, data uintptr) uint32 {
160+ d := live.displays[data]
161+ if d == nil || d.filter == nil {
162+ return 1
163+ }
164+ g := live.globals[global]
165+ // wl_global_create announces synchronously, before returning its pointer.
166+ if g == nil {
167+ g = d.pendingGlobal
168+ }
169+ if g != nil && d.filter(Client{client}, g) {
170+ return 1
171+ }
172+ return 0
173+}
137 174
138 // AddGlobal advertises a global and returns a handle to remove it later.175 // AddGlobal advertises a global and returns a handle to remove it later.
139 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {176 func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
140 live.nextBind++177 live.nextBind++
141 token := live.nextBind178 token := live.nextBind
142 live.binds[token] = bindFn179 live.binds[token] = bindFn
180+ g := &Global{d: d}
181+ d.pendingGlobal = g
143 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)182 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
183+ d.pendingGlobal = nil
144 if c == 0 {184 if c == 0 {
145 delete(live.binds, token)185 delete(live.binds, token)
146 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)186 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
147 }187 }
148- return &Global{c: c}, nil188+ g.c = c
189+ live.globals[c] = g
190+ return g, nil
149 }191 }
150 192
151 // Remove withdraws the global: clients get wl_registry.global_remove.193 // Remove withdraws the global: clients get wl_registry.global_remove.
@@ -204,6 +246,14 @@ func (d *Display) destroy() {
204 wlDisplayDestroyClients(d.c)246 wlDisplayDestroyClients(d.c)
205 wlEventSourceRemove(d.wakeSrc)247 wlEventSourceRemove(d.wakeSrc)
206 wlDisplayDestroy(d.c)248 wlDisplayDestroy(d.c)
249+ delete(live.displays, d.c)
250+ for p, g := range live.globals {
251+ if g.d == d {
252+ g.c = 0
253+ delete(live.globals, p)
254+ }
255+ }
256+ d.filter = nil
207 unix.Close(d.wake)257 unix.Close(d.wake)
208 }258 }
209 259
@@ -390,9 +440,10 @@ func (d *Display) Post(fn func()) {
390 440
391 // Source is an fd registered with the display's event loop by AddFD.441 // Source is an fd registered with the display's event loop by AddFD.
392 type Source struct {442 type Source struct {
393- d *Display443+ d *Display
394- c uintptr444+ c uintptr
395- token uintptr445+ token uintptr
446+ onReadable, onWritable func()
396 }447 }
397 448
398 // AddFD calls onReadable on the display goroutine whenever fd is readable. The449 // AddFD calls onReadable on the display goroutine whenever fd is readable. The
@@ -410,6 +461,14 @@ type Source struct {
410 // goroutine or before Run starts. Sources not removed are released when the461 // goroutine or before Run starts. Sources not removed are released when the
411 // display is destroyed.462 // display is destroyed.
412 func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {463 func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
464+ return d.AddFDRW(fd, onReadable, nil)
465+}
466+
467+// AddFDRW is AddFD with an optional writable callback. Writable interest starts
468+// disabled; use SetWritable to arm it after a non-blocking write returns EAGAIN.
469+// Both callbacks must not block. Readable runs first; removing the source there
470+// suppresses writable delivery in the same dispatch.
471+func (d *Display) AddFDRW(fd int, onReadable, onWritable func()) (*Source, error) {
413 if fd < 0 {472 if fd < 0 {
414 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)473 return nil, fmt.Errorf("purego-libwayland: AddFD: invalid fd %d", fd)
415 }474 }
@@ -421,17 +480,38 @@ func (d *Display) AddFD(fd int, onReadable func()) (*Source, error) {
421 }480 }
422 live.nextFD++481 live.nextFD++
423 token := live.nextFD482 token := live.nextFD
424- live.fds[token] = onReadable483+ s := &Source{d: d, token: token, onReadable: onReadable, onWritable: onWritable}
484+ live.fds[token] = s
425 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)485 c := wlEventLoopAddFD(d.loop, int32(fd), 1 /* WL_EVENT_READABLE */, cbFD, token)
426 if c == 0 {486 if c == 0 {
427 delete(live.fds, token)487 delete(live.fds, token)
428 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)488 return nil, fmt.Errorf("purego-libwayland: wl_event_loop_add_fd(%d) failed", fd)
429 }489 }
430- s := &Source{d: d, c: c, token: token}490+ s.c = c
431 d.sources[s] = struct{}{}491 d.sources[s] = struct{}{}
432 return s, nil492 return s, nil
433 }493 }
434 494
495+// SetWritable toggles writable interest while always retaining readable interest.
496+// Disable it when buffered writes are drained to avoid a busy loop. Display
497+// goroutine only. A removed source is a no-op; enabling requires onWritable.
498+func (s *Source) SetWritable(on bool) error {
499+ if s == nil || s.c == 0 {
500+ return nil
501+ }
502+ mask := uint32(1)
503+ if on {
504+ if s.onWritable == nil {
505+ return errors.New("purego-libwayland: no writable callback")
506+ }
507+ mask |= 2
508+ }
509+ if wlEventSourceFDUpdate(s.c, mask) < 0 {
510+ return lastErrno()
511+ }
512+ return nil
513+}
514+
435 // Remove stops the callbacks and releases libwayland's duplicate of the fd.515 // Remove stops the callbacks and releases libwayland's duplicate of the fd.
436 // It may be called from the source's own callback. Display goroutine only;516 // It may be called from the source's own callback. Display goroutine only;
437 // idempotent, and a no-op after the display was destroyed.517 // idempotent, and a no-op after the display was destroyed.
@@ -658,9 +738,14 @@ func wake(fd int32, mask uint32, data uintptr) int32 {
658 }738 }
659 739
660 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).740 // fdReadable dispatches an AddFD source (wl_event_loop_fd_func_t).
661-func fdReadable(_ int32, _ uint32, data uintptr) int32 {741+func fdReadable(_ int32, mask uint32, data uintptr) int32 {
662- if fn := live.fds[data]; fn != nil {742+ if s := live.fds[data]; s != nil {
663- fn()743+ if mask&(1|4|8) != 0 {
744+ s.onReadable()
745+ }
746+ if s.c != 0 && mask&2 != 0 && s.onWritable != nil {
747+ s.onWritable()
748+ }
664 }749 }
665 return 0750 return 0
666 }751 }
M server/lib.go +19 -6
@@ -33,17 +33,19 @@ var (
3333 symClientGetCredentials, symClientGetFD, symClientCreate uintptr
3434 symClientAddDestroyListener, symListRemove uintptr
3535 symErrnoLocation uintptr
36+ symDisplaySetGlobalFilter, symEventSourceFDUpdate uintptr
3637 // wl_fixes support (libwayland 1.26+); zero on older libraries.
3738 symFixesAckGlobalRemove uintptr
3839
3940 // Shared C callbacks. purego callbacks are never freed, so there is one
4041 // of each for the whole process.
41- cbDispatcher uintptr
42- cbDestroy uintptr
43- cbClientGone uintptr
44- cbBind uintptr
45- cbWake uintptr
46- cbFD uintptr
42+ cbDispatcher uintptr
43+ cbDestroy uintptr
44+ cbClientGone uintptr
45+ cbBind uintptr
46+ cbWake uintptr
47+ cbFD uintptr
48+ cbGlobalFilter uintptr
4749 )
4850
4951 func load() error {
@@ -74,6 +76,8 @@ func load() error {
7476 name string
7577 sym *uintptr
7678 }{
79+ {"wl_display_set_global_filter", &symDisplaySetGlobalFilter},
80+ {"wl_event_source_fd_update", &symEventSourceFDUpdate},
7781 {"wl_display_flush_clients", &symDisplayFlushClients},
7882 {"wl_event_loop_dispatch", &symEventLoopDispatch},
7983 {"wl_resource_create", &symResourceCreate},
@@ -128,6 +132,7 @@ func load() error {
128132 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
129133 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
130134 })
135+ cbGlobalFilter = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { return uintptr(globalFilter(a.Int(0), a.Int(1), a.Int(2))) })
131136 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
132137 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
133138 })
@@ -168,3 +173,11 @@ func wlResourceDestroy(resource uintptr) {
168173 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
169174 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
170175 }
176+
177+func wlDisplaySetGlobalFilter(display, callback, data uintptr) {
178+ purego.Syscall6(symDisplaySetGlobalFilter, display, callback, data, 0, 0, 0)
179+}
180+func wlEventSourceFDUpdate(source uintptr, mask uint32) int32 {
181+ r, _, _ := purego.Syscall6(symEventSourceFDUpdate, source, uintptr(mask), 0, 0, 0, 0)
182+ return int32(r)
183+}
@@ -33,17 +33,19 @@ var (
33 symClientGetCredentials, symClientGetFD, symClientCreate uintptr33 symClientGetCredentials, symClientGetFD, symClientCreate uintptr
34 symClientAddDestroyListener, symListRemove uintptr34 symClientAddDestroyListener, symListRemove uintptr
35 symErrnoLocation uintptr35 symErrnoLocation uintptr
36+ symDisplaySetGlobalFilter, symEventSourceFDUpdate uintptr
36 // wl_fixes support (libwayland 1.26+); zero on older libraries.37 // wl_fixes support (libwayland 1.26+); zero on older libraries.
37 symFixesAckGlobalRemove uintptr38 symFixesAckGlobalRemove uintptr
38 39
39 // Shared C callbacks. purego callbacks are never freed, so there is one40 // Shared C callbacks. purego callbacks are never freed, so there is one
40 // of each for the whole process.41 // of each for the whole process.
41- cbDispatcher uintptr42+ cbDispatcher uintptr
42- cbDestroy uintptr43+ cbDestroy uintptr
43- cbClientGone uintptr44+ cbClientGone uintptr
44- cbBind uintptr45+ cbBind uintptr
45- cbWake uintptr46+ cbWake uintptr
46- cbFD uintptr47+ cbFD uintptr
48+ cbGlobalFilter uintptr
47 )49 )
48 50
49 func load() error {51 func load() error {
@@ -74,6 +76,8 @@ func load() error {
74 name string76 name string
75 sym *uintptr77 sym *uintptr
76 }{78 }{
79+ {"wl_display_set_global_filter", &symDisplaySetGlobalFilter},
80+ {"wl_event_source_fd_update", &symEventSourceFDUpdate},
77 {"wl_display_flush_clients", &symDisplayFlushClients},81 {"wl_display_flush_clients", &symDisplayFlushClients},
78 {"wl_event_loop_dispatch", &symEventLoopDispatch},82 {"wl_event_loop_dispatch", &symEventLoopDispatch},
79 {"wl_resource_create", &symResourceCreate},83 {"wl_resource_create", &symResourceCreate},
@@ -128,6 +132,7 @@ func load() error {
128 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {132 cbWake = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
129 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))133 return uintptr(uint32(wake(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
130 })134 })
135+ cbGlobalFilter = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr { return uintptr(globalFilter(a.Int(0), a.Int(1), a.Int(2))) })
131 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {136 cbFD = purego.NewCallbackInts(func(a *purego.CallbackArgs) uintptr {
132 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))137 return uintptr(uint32(fdReadable(int32(a.Int(0)), uint32(a.Int(1)), a.Int(2))))
133 })138 })
@@ -168,3 +173,11 @@ func wlResourceDestroy(resource uintptr) {
168 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {173 func wlFixesHandleAckGlobalRemove(fixes, registry uintptr, name uint32) {
169 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)174 purego.Syscall6(symFixesAckGlobalRemove, fixes, registry, uintptr(name), 0, 0, 0)
170 }175 }
176+
177+func wlDisplaySetGlobalFilter(display, callback, data uintptr) {
178+ purego.Syscall6(symDisplaySetGlobalFilter, display, callback, data, 0, 0, 0)
179+}
180+func wlEventSourceFDUpdate(source uintptr, mask uint32) int32 {
181+ r, _, _ := purego.Syscall6(symEventSourceFDUpdate, source, uintptr(mask), 0, 0, 0, 0)
182+ return int32(r)
183+}
A server/xwm_test.go +229 -0
@@ -0,0 +1,229 @@
1+package server
2+
3+import (
4+ "encoding/binary"
5+ "runtime"
6+ "testing"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+func xwmDisplay(t testing.TB) *Display {
13+ t.Helper()
14+ runtime.LockOSThread()
15+ t.Cleanup(runtime.UnlockOSThread)
16+ d, err := NewDisplay()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ t.Cleanup(d.Close)
21+ return d
22+}
23+
24+func xwmClient(t testing.TB, d *Display) (Client, int) {
25+ t.Helper()
26+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ t.Cleanup(func() { unix.Close(fds[1]) })
31+ c, err := d.CreateClient(fds[0])
32+ if err != nil {
33+ unix.Close(fds[0])
34+ t.Fatal(err)
35+ }
36+ return c, fds[1]
37+}
38+
39+// registry requests exercise libwayland's real advertisement/filter path.
40+func registry(t testing.TB, d *Display, fd int) int {
41+ t.Helper()
42+ request := [12]byte{}
43+ binary.NativeEndian.PutUint32(request[:4], 1)
44+ binary.NativeEndian.PutUint32(request[4:8], 12<<16|1)
45+ binary.NativeEndian.PutUint32(request[8:], 2)
46+ if _, err := unix.Write(fd, request[:]); err != nil {
47+ t.Fatal(err)
48+ }
49+ if wlEventLoopDispatch(d.loop, 0) < 0 {
50+ t.Fatal("dispatch failed")
51+ }
52+ wlDisplayFlushClients(d.c)
53+ var buf [4096]byte
54+ n, err := unix.Read(fd, buf[:])
55+ if err == unix.EAGAIN {
56+ return 0
57+ }
58+ if err != nil {
59+ t.Fatal(err)
60+ }
61+ return n
62+}
63+
64+func TestGlobalFilter(t *testing.T) {
65+ d := xwmDisplay(t)
66+ iface := &Interface{Name: "xwm_filter_test", Version: 1}
67+ if err := NewInterfaces(iface); err != nil {
68+ t.Fatal(err)
69+ }
70+ g, err := d.AddGlobal(iface, 1, nil)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ allowed, a := xwmClient(t, d)
75+ _, b := xwmClient(t, d)
76+ calls := 0
77+ d.SetGlobalFilter(func(c Client, got *Global) bool {
78+ calls++
79+ if got != g {
80+ t.Error("wrong global handle")
81+ }
82+ return c == allowed
83+ })
84+ if registry(t, d, a) == 0 {
85+ t.Fatal("allowed client saw no global")
86+ }
87+ if registry(t, d, b) != 0 {
88+ t.Fatal("denied client saw global")
89+ }
90+ if calls != 2 {
91+ t.Fatalf("filter calls = %d", calls)
92+ }
93+ allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) })
94+ if allocs != 0 {
95+ t.Fatalf("filter allocs = %g", allocs)
96+ }
97+ // Creating a global with registries already present invokes the filter
98+ // before AddGlobal returns, but still passes the eventual handle.
99+ var during *Global
100+ d.SetGlobalFilter(func(_ Client, got *Global) bool { during = got; return false })
101+ second, err := d.AddGlobal(iface, 1, nil)
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ if during != second {
106+ t.Fatal("creation-time filter handle differs")
107+ }
108+ d.SetGlobalFilter(nil)
109+ _, c := xwmClient(t, d)
110+ if registry(t, d, c) == 0 {
111+ t.Fatal("nil did not remove filter")
112+ }
113+ g.Remove()
114+ g.Remove()
115+}
116+
117+func TestWritableInterest(t *testing.T) {
118+ d := xwmDisplay(t)
119+ client, fd := xwmClient(t, d)
120+ reads, writes := 0, 0
121+ s, err := d.AddFDRW(fd, func() { reads++ }, func() { writes++ })
122+ if err != nil {
123+ t.Fatal(err)
124+ }
125+ dispatch := func() {
126+ if wlEventLoopDispatch(d.loop, 0) < 0 {
127+ t.Fatal("dispatch failed")
128+ }
129+ }
130+ dispatch()
131+ if writes != 0 {
132+ t.Fatal("writable starts enabled")
133+ }
134+ if err := s.SetWritable(true); err != nil {
135+ t.Fatal(err)
136+ }
137+ dispatch()
138+ if writes != 1 || reads != 0 {
139+ t.Fatalf("reads=%d writes=%d", reads, writes)
140+ }
141+ allocs := testing.AllocsPerRun(1000, dispatch)
142+ if allocs != 0 {
143+ t.Fatalf("fd dispatch allocs = %g", allocs)
144+ }
145+ before := writes
146+ if err := s.SetWritable(false); err != nil {
147+ t.Fatal(err)
148+ }
149+ dispatch()
150+ if writes != before {
151+ t.Fatal("writable not disabled")
152+ }
153+ // Readable callbacks also stay allocation-free through the real C loop.
154+ serverFD, err := client.FD()
155+ if err != nil {
156+ t.Fatal(err)
157+ }
158+ var payload = [1]byte{1}
159+ if _, err := unix.Write(serverFD, payload[:]); err != nil {
160+ t.Fatal(err)
161+ }
162+ allocs = testing.AllocsPerRun(1000, dispatch)
163+ if allocs != 0 {
164+ t.Fatalf("readable dispatch allocs = %g", allocs)
165+ }
166+ if reads == 0 {
167+ t.Fatal("readable interest lost")
168+ }
169+ if _, err := unix.Read(fd, payload[:]); err != nil {
170+ t.Fatal(err)
171+ }
172+ reads = 0
173+ // Combined masks, hangup routing and removal during readable delivery.
174+ fdReadable(0, 1|2, s.token)
175+ if reads != 1 || writes != before+1 {
176+ t.Fatal("combined mask not delivered")
177+ }
178+ s.onReadable = func() { reads++; s.Remove() }
179+ fdReadable(0, 4|8|2, s.token)
180+ if reads != 2 || writes != before+1 {
181+ t.Fatal("removal did not suppress writable")
182+ }
183+ if err := s.SetWritable(true); err != nil {
184+ t.Fatal(err)
185+ }
186+ plain, err := d.AddFD(fd, func() {})
187+ if err != nil {
188+ t.Fatal(err)
189+ }
190+ if plain.SetWritable(true) == nil {
191+ t.Fatal("enabled without writable callback")
192+ }
193+}
194+
195+func BenchmarkGlobalFilter(b *testing.B) {
196+ d := xwmDisplay(b)
197+ iface := &Interface{Name: "xwm_filter_bench", Version: 1}
198+ if err := NewInterfaces(iface); err != nil {
199+ b.Fatal(err)
200+ }
201+ g, err := d.AddGlobal(iface, 1, nil)
202+ if err != nil {
203+ b.Fatal(err)
204+ }
205+ c, _ := xwmClient(b, d)
206+ d.SetGlobalFilter(func(Client, *Global) bool { return true })
207+ b.ReportAllocs()
208+ b.ResetTimer()
209+ for b.Loop() {
210+ purego.Syscall6(cbGlobalFilter, c.c, g.c, d.c, 0, 0, 0)
211+ }
212+}
213+
214+func BenchmarkFDCallback(b *testing.B) {
215+ d := xwmDisplay(b)
216+ _, fd := xwmClient(b, d)
217+ s, err := d.AddFDRW(fd, func() {}, func() {})
218+ if err != nil {
219+ b.Fatal(err)
220+ }
221+ if err := s.SetWritable(true); err != nil {
222+ b.Fatal(err)
223+ }
224+ b.ReportAllocs()
225+ b.ResetTimer()
226+ for b.Loop() {
227+ wlEventLoopDispatch(d.loop, 0)
228+ }
229+}
@@ -0,0 +1,229 @@
1+package server
2+
3+import (
4+ "encoding/binary"
5+ "runtime"
6+ "testing"
7+
8+ "github.com/bnema/purego"
9+ "golang.org/x/sys/unix"
10+)
11+
12+func xwmDisplay(t testing.TB) *Display {
13+ t.Helper()
14+ runtime.LockOSThread()
15+ t.Cleanup(runtime.UnlockOSThread)
16+ d, err := NewDisplay()
17+ if err != nil {
18+ t.Fatal(err)
19+ }
20+ t.Cleanup(d.Close)
21+ return d
22+}
23+
24+func xwmClient(t testing.TB, d *Display) (Client, int) {
25+ t.Helper()
26+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0)
27+ if err != nil {
28+ t.Fatal(err)
29+ }
30+ t.Cleanup(func() { unix.Close(fds[1]) })
31+ c, err := d.CreateClient(fds[0])
32+ if err != nil {
33+ unix.Close(fds[0])
34+ t.Fatal(err)
35+ }
36+ return c, fds[1]
37+}
38+
39+// registry requests exercise libwayland's real advertisement/filter path.
40+func registry(t testing.TB, d *Display, fd int) int {
41+ t.Helper()
42+ request := [12]byte{}
43+ binary.NativeEndian.PutUint32(request[:4], 1)
44+ binary.NativeEndian.PutUint32(request[4:8], 12<<16|1)
45+ binary.NativeEndian.PutUint32(request[8:], 2)
46+ if _, err := unix.Write(fd, request[:]); err != nil {
47+ t.Fatal(err)
48+ }
49+ if wlEventLoopDispatch(d.loop, 0) < 0 {
50+ t.Fatal("dispatch failed")
51+ }
52+ wlDisplayFlushClients(d.c)
53+ var buf [4096]byte
54+ n, err := unix.Read(fd, buf[:])
55+ if err == unix.EAGAIN {
56+ return 0
57+ }
58+ if err != nil {
59+ t.Fatal(err)
60+ }
61+ return n
62+}
63+
64+func TestGlobalFilter(t *testing.T) {
65+ d := xwmDisplay(t)
66+ iface := &Interface{Name: "xwm_filter_test", Version: 1}
67+ if err := NewInterfaces(iface); err != nil {
68+ t.Fatal(err)
69+ }
70+ g, err := d.AddGlobal(iface, 1, nil)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ allowed, a := xwmClient(t, d)
75+ _, b := xwmClient(t, d)
76+ calls := 0
77+ d.SetGlobalFilter(func(c Client, got *Global) bool {
78+ calls++
79+ if got != g {
80+ t.Error("wrong global handle")
81+ }
82+ return c == allowed
83+ })
84+ if registry(t, d, a) == 0 {
85+ t.Fatal("allowed client saw no global")
86+ }
87+ if registry(t, d, b) != 0 {
88+ t.Fatal("denied client saw global")
89+ }
90+ if calls != 2 {
91+ t.Fatalf("filter calls = %d", calls)
92+ }
93+ allocs := testing.AllocsPerRun(1000, func() { purego.Syscall6(cbGlobalFilter, allowed.c, g.c, d.c, 0, 0, 0) })
94+ if allocs != 0 {
95+ t.Fatalf("filter allocs = %g", allocs)
96+ }
97+ // Creating a global with registries already present invokes the filter
98+ // before AddGlobal returns, but still passes the eventual handle.
99+ var during *Global
100+ d.SetGlobalFilter(func(_ Client, got *Global) bool { during = got; return false })
101+ second, err := d.AddGlobal(iface, 1, nil)
102+ if err != nil {
103+ t.Fatal(err)
104+ }
105+ if during != second {
106+ t.Fatal("creation-time filter handle differs")
107+ }
108+ d.SetGlobalFilter(nil)
109+ _, c := xwmClient(t, d)
110+ if registry(t, d, c) == 0 {
111+ t.Fatal("nil did not remove filter")
112+ }
113+ g.Remove()
114+ g.Remove()
115+}
116+
117+func TestWritableInterest(t *testing.T) {
118+ d := xwmDisplay(t)
119+ client, fd := xwmClient(t, d)
120+ reads, writes := 0, 0
121+ s, err := d.AddFDRW(fd, func() { reads++ }, func() { writes++ })
122+ if err != nil {
123+ t.Fatal(err)
124+ }
125+ dispatch := func() {
126+ if wlEventLoopDispatch(d.loop, 0) < 0 {
127+ t.Fatal("dispatch failed")
128+ }
129+ }
130+ dispatch()
131+ if writes != 0 {
132+ t.Fatal("writable starts enabled")
133+ }
134+ if err := s.SetWritable(true); err != nil {
135+ t.Fatal(err)
136+ }
137+ dispatch()
138+ if writes != 1 || reads != 0 {
139+ t.Fatalf("reads=%d writes=%d", reads, writes)
140+ }
141+ allocs := testing.AllocsPerRun(1000, dispatch)
142+ if allocs != 0 {
143+ t.Fatalf("fd dispatch allocs = %g", allocs)
144+ }
145+ before := writes
146+ if err := s.SetWritable(false); err != nil {
147+ t.Fatal(err)
148+ }
149+ dispatch()
150+ if writes != before {
151+ t.Fatal("writable not disabled")
152+ }
153+ // Readable callbacks also stay allocation-free through the real C loop.
154+ serverFD, err := client.FD()
155+ if err != nil {
156+ t.Fatal(err)
157+ }
158+ var payload = [1]byte{1}
159+ if _, err := unix.Write(serverFD, payload[:]); err != nil {
160+ t.Fatal(err)
161+ }
162+ allocs = testing.AllocsPerRun(1000, dispatch)
163+ if allocs != 0 {
164+ t.Fatalf("readable dispatch allocs = %g", allocs)
165+ }
166+ if reads == 0 {
167+ t.Fatal("readable interest lost")
168+ }
169+ if _, err := unix.Read(fd, payload[:]); err != nil {
170+ t.Fatal(err)
171+ }
172+ reads = 0
173+ // Combined masks, hangup routing and removal during readable delivery.
174+ fdReadable(0, 1|2, s.token)
175+ if reads != 1 || writes != before+1 {
176+ t.Fatal("combined mask not delivered")
177+ }
178+ s.onReadable = func() { reads++; s.Remove() }
179+ fdReadable(0, 4|8|2, s.token)
180+ if reads != 2 || writes != before+1 {
181+ t.Fatal("removal did not suppress writable")
182+ }
183+ if err := s.SetWritable(true); err != nil {
184+ t.Fatal(err)
185+ }
186+ plain, err := d.AddFD(fd, func() {})
187+ if err != nil {
188+ t.Fatal(err)
189+ }
190+ if plain.SetWritable(true) == nil {
191+ t.Fatal("enabled without writable callback")
192+ }
193+}
194+
195+func BenchmarkGlobalFilter(b *testing.B) {
196+ d := xwmDisplay(b)
197+ iface := &Interface{Name: "xwm_filter_bench", Version: 1}
198+ if err := NewInterfaces(iface); err != nil {
199+ b.Fatal(err)
200+ }
201+ g, err := d.AddGlobal(iface, 1, nil)
202+ if err != nil {
203+ b.Fatal(err)
204+ }
205+ c, _ := xwmClient(b, d)
206+ d.SetGlobalFilter(func(Client, *Global) bool { return true })
207+ b.ReportAllocs()
208+ b.ResetTimer()
209+ for b.Loop() {
210+ purego.Syscall6(cbGlobalFilter, c.c, g.c, d.c, 0, 0, 0)
211+ }
212+}
213+
214+func BenchmarkFDCallback(b *testing.B) {
215+ d := xwmDisplay(b)
216+ _, fd := xwmClient(b, d)
217+ s, err := d.AddFDRW(fd, func() {}, func() {})
218+ if err != nil {
219+ b.Fatal(err)
220+ }
221+ if err := s.SetWritable(true); err != nil {
222+ b.Fatal(err)
223+ }
224+ b.ReportAllocs()
225+ b.ResetTimer()
226+ for b.Loop() {
227+ wlEventLoopDispatch(d.loop, 0)
228+ }
229+}

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