1package server
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "runtime"
8 "strings"
9 "sync/atomic"
10 "unsafe"
11
12 "golang.org/x/sys/unix"
13)
14
15// Handler receives every request sent to a resource. args borrows libwayland's
16// argument array and must not be retained after the handler returns.
17type Handler func(r *Resource, opcode uint32, args []Arg)
18
19// BindFunc runs when a client binds a global.
20type BindFunc func(c Client, version, id uint32)
21
22// Client is a connected wl_client.
23type Client struct{ c uintptr }
24
25// Resource is a server-side wl_resource.
26type Resource struct {
27 c uintptr
28 id uint32
29 version int32
30 client uintptr
31 iface *Interface
32 handler Handler
33 OnDestroy func()
34 gone bool
35 bare bool // created by libwayland, no destroy callback
36
37 // Data holds the owner's state, typically the generated wrapper.
38 Data any
39}
40
41// live holds Go state reachable from C callbacks. It is only touched on the
42// display goroutine, so it needs no lock.
43var live = struct {
44 resources map[uintptr]*Resource
45 binds map[uintptr]BindFunc
46 nextBind uintptr
47}{resources: map[uintptr]*Resource{}, binds: map[uintptr]BindFunc{}}
48
49// Display owns a wl_display. All methods except Do must run on the goroutine
50// that calls Run, or before Run starts.
51// Do wakes the loop through an eventfd so calls run at once instead of after
52// the dispatch timeout.
53type Display struct {
54 c uintptr
55 loop uintptr
56 calls chan func()
57 wake int
58 // wakeSrc owns libwayland's duplicate of wake; only removing the source
59 // closes it, destroying the display does not.
60 wakeSrc uintptr
61 stopped chan struct{}
62 state atomic.Uint32
63}
64
65func NewDisplay() (*Display, error) {
66 if err := load(); err != nil {
67 return nil, err
68 }
69 c := wlDisplayCreate()
70 if c == 0 {
71 return nil, errors.New("purego-libwayland: wl_display_create failed")
72 }
73 wake, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
74 if err != nil {
75 wlDisplayDestroy(c)
76 return nil, fmt.Errorf("purego-libwayland: eventfd: %w", err)
77 }
78 d := &Display{c: c, loop: wlDisplayGetEventLoop(c), calls: make(chan func(), 64), wake: wake, stopped: make(chan struct{})}
79 // The callback only drains the counter; Run then executes queued calls.
80 if d.wakeSrc = wlEventLoopAddFD(d.loop, int32(wake), 1 /* WL_EVENT_READABLE */, cbWake, 0); d.wakeSrc == 0 {
81 unix.Close(wake)
82 wlDisplayDestroy(c)
83 return nil, errors.New("purego-libwayland: wl_event_loop_add_fd failed")
84 }
85 return d, nil
86}
87
88// AddSocketFD serves clients on an already bound, listening unix socket.
89func (d *Display) AddSocketFD(fd int) error {
90 if wlDisplayAddSocketFD(d.c, int32(fd)) != 0 {
91 return fmt.Errorf("purego-libwayland: wl_display_add_socket_fd(%d) failed", fd)
92 }
93 return nil
94}
95
96func (d *Display) CreateGlobal(iface *Interface, version int32, bindFn BindFunc) error {
97 _, err := d.AddGlobal(iface, version, bindFn)
98 return err
99}
100
101// Global is a global that can be withdrawn, such as a wl_output on hotplug.
102type Global struct{ c uintptr }
103
104// AddGlobal advertises a global and returns a handle to remove it later.
105func (d *Display) AddGlobal(iface *Interface, version int32, bindFn BindFunc) (*Global, error) {
106 live.nextBind++
107 token := live.nextBind
108 live.binds[token] = bindFn
109 c := wlGlobalCreate(d.c, iface.c, version, token, cbBind)
110 if c == 0 {
111 delete(live.binds, token)
112 return nil, fmt.Errorf("purego-libwayland: wl_global_create(%s) failed", iface.Name)
113 }
114 return &Global{c: c}, nil
115}
116
117// Remove withdraws the global: clients get wl_registry.global_remove.
118// Existing resources stay valid. A client that binds before it sees the
119// removal still gets its bind function called, so it always receives a
120// resource (the protocol allows binding a just-removed global); the bind
121// function should then send inert state. The global is freed with the
122// display. Display goroutine only; idempotent.
123func (g *Global) Remove() {
124 if g == nil || g.c == 0 {
125 return
126 }
127 wlGlobalRemove(g.c)
128 g.c = 0
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.
136func 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
147// Close destroys a display that has not started Run. It is a no-op once Run
148// has started; Run owns destruction in that case. Call before Run starts.
149func (d *Display) Close() {
150 if !d.state.CompareAndSwap(0, 1) {
151 return
152 }
153 d.destroy()
154 close(d.stopped)
155}
156
157// destroy frees the display and the wake-up fds.
158func (d *Display) destroy() {
159 wlDisplayDestroyClients(d.c)
160 wlEventSourceRemove(d.wakeSrc)
161 wlDisplayDestroy(d.c)
162 unix.Close(d.wake)
163}
164
165// Run dispatches libwayland on one OS-locked goroutine until ctx is done.
166func (d *Display) Run(ctx context.Context) error {
167 if !d.state.CompareAndSwap(0, 2) {
168 return errors.New("purego-libwayland: display already started or closed")
169 }
170 runtime.LockOSThread()
171 defer runtime.UnlockOSThread()
172 defer close(d.stopped)
173 defer d.destroy()
174 consecutiveFailures := 0
175 for {
176 if ctx.Err() != nil {
177 return nil
178 }
179 // Run every queued call; each Do also wrote the eventfd.
180 drain:
181 for {
182 select {
183 case fn := <-d.calls:
184 fn()
185 default:
186 break drain
187 }
188 }
189 wlDisplayFlushClients(d.c)
190 // The timeout only bounds how late ctx cancellation is seen.
191 if wlEventLoopDispatch(d.loop, 100) < 0 {
192 errno := lastErrno()
193 consecutiveFailures++
194 // epoll_wait is interrupted by Go's preemption signals (EINTR); a persistent failure (EBADF, EINVAL) repeats.
195 if consecutiveFailures < 16 {
196 continue
197 }
198 return fmt.Errorf("purego-libwayland: wl_event_loop_dispatch failed %d times in a row: %w", consecutiveFailures, errno)
199 }
200 consecutiveFailures = 0
201 wlDisplayFlushClients(d.c)
202 }
203}
204
205// Do runs fn on the display goroutine and waits for it. It returns false if
206// the display loop has stopped (fn was not run, or its run was not confirmed).
207// Do must not be called from the display goroutine: it would deadlock.
208func (d *Display) Do(fn func()) bool {
209 done := make(chan struct{})
210 select {
211 case d.calls <- func() { fn(); close(done) }:
212 case <-d.stopped:
213 return false
214 }
215 var one = [8]byte{1}
216 _, _ = unix.Write(d.wake, one[:])
217 select {
218 case <-done:
219 return true
220 case <-d.stopped:
221 // If both are ready, prefer confirmation of a completed call.
222 select {
223 case <-done:
224 return true
225 default:
226 return false
227 }
228 }
229}
230
231// Stopped is closed after Run returns and the display is destroyed.
232func (d *Display) Stopped() <-chan struct{} { return d.stopped }
233
234// LiveResources reports resources not yet destroyed. Display goroutine only.
235func LiveResources() int { return len(live.resources) }
236
237// PID is the process ID of the client, read from the socket credentials
238// (SO_PEERCRED) when it connected. Zero for an invalid client.
239// Display goroutine only.
240func (c Client) PID() int {
241 if c.c == 0 {
242 return 0
243 }
244 var pid, uid, gid int32
245 wlClientGetCredentials(c.c, unsafe.Pointer(&pid), unsafe.Pointer(&uid), unsafe.Pointer(&gid))
246 return int(pid)
247}
248
249func (c Client) CreateResource(iface *Interface, version int32, id uint32, h Handler) (*Resource, error) {
250 rc := wlResourceCreate(c.c, iface.c, version, id)
251 if rc == 0 {
252 return nil, fmt.Errorf("purego-libwayland: wl_resource_create(%s) failed", iface.Name)
253 }
254 // id 0 asks libwayland to allocate a server-side ID.
255 if id == 0 {
256 id = wlResourceGetID(rc)
257 }
258 r := &Resource{c: rc, id: id, version: version, client: c.c, iface: iface, handler: h}
259 live.resources[rc] = r
260 wlResourceSetDispatcher(rc, cbDispatcher, implMarker(), 0, cbDestroy)
261 return r, nil
262}
263
264func (r *Resource) Alive() bool { return !r.gone }
265func (r *Resource) ID() uint32 {
266 if r.gone {
267 return 0
268 }
269 return r.id
270}
271func (r *Resource) Version() int32 {
272 if r.gone {
273 return 0
274 }
275 return r.version
276}
277func (r *Resource) Client() Client {
278 if r.gone {
279 return Client{}
280 }
281 return Client{r.client}
282}
283func (r *Resource) Iface() *Interface { return r.iface }
284
285// Destroy frees the resource. It is a no-op once the resource is gone, so
286// generated destructor requests and handlers may both call it.
287func (r *Resource) Destroy() {
288 if !r.gone {
289 wlResourceDestroy(r.c)
290 // A bare handle gets no destroy callback to mark it gone.
291 r.gone = r.gone || r.bare
292 }
293}
294
295// eventArgs holds the wl_argument array of the event being posted.
296// libwayland copies it before wl_resource_post_event_array returns.
297// Display goroutine only; post_event_array does not call back into Go, so
298// PostEvent is not re-entrant. No protocol event has more than 8 arguments.
299var eventArgs [16]Arg
300
301// PostEvent sends an event. Arguments must match the event signature; build
302// string and array arguments with a Pinner and unpin it after this returns.
303// Calling it after destruction is safe and does nothing.
304func (r *Resource) PostEvent(opcode uint32, args ...Arg) {
305 if r.gone {
306 return
307 }
308 if len(args) > len(eventArgs) {
309 panic("purego-libwayland: too many event arguments")
310 }
311 var p unsafe.Pointer
312 if len(args) > 0 {
313 copy(eventArgs[:], args)
314 p = unsafe.Pointer(&eventArgs[0])
315 }
316 wlResourcePostEventArr(r.c, opcode, p)
317}
318
319// PostError sends a protocol error and disconnects the client.
320// Calling it after destruction is safe and does nothing.
321func (r *Resource) PostError(code uint32, msg string) {
322 if r.gone {
323 return
324 }
325 // wl_resource_post_error takes a printf format; escape it and pass no
326 // variadic arguments (purego clears AL, as SysV varargs require).
327 b := append([]byte(strings.ReplaceAll(msg, "%", "%%")), 0)
328 var pin runtime.Pinner
329 pin.Pin(&b[0])
330 defer pin.Unpin()
331 wlResourcePostError(r.c, code, uintptr(unsafe.Pointer(&b[0])))
332 runtime.KeepAlive(b)
333}
334
335// implMarker is a non-nil implementation pointer. libwayland only passes it
336// back to our dispatcher.
337func implMarker() uintptr {
338 if marker == 0 {
339 tablesMu.Lock()
340 if err := tables.reserve(8); err != nil {
341 panic(err)
342 }
343 _, marker = tables.alloc(8)
344 tablesMu.Unlock()
345 }
346 return marker
347}
348
349var marker uintptr
350
351// --- C callbacks (display goroutine) ---
352
353func dispatch(_ uintptr, target uintptr, opcode uint32, _ unsafe.Pointer, args unsafe.Pointer) int32 {
354 r := live.resources[target]
355 if r == nil || opcode >= uint32(len(r.iface.Requests)) {
356 return 0
357 }
358 m := &r.iface.Requests[opcode]
359 if r.handler == nil {
360 closeRequestFDs(m.fdPositions, args)
361 return 0
362 }
363 var a []Arg
364 if m.argc > 0 {
365 a = unsafe.Slice((*Arg)(args), m.argc)
366 }
367 r.handler(r, opcode, a)
368 return 0
369}
370
371func destroyed(res uintptr) {
372 r := live.resources[res]
373 delete(live.resources, res)
374 if r == nil {
375 return
376 }
377 r.gone = true
378 if r.OnDestroy != nil {
379 r.OnDestroy()
380 }
381}
382
383// wake drains a Display's eventfd (wl_event_loop_fd_func_t).
384func wake(fd int32, mask uint32, data uintptr) int32 {
385 var buf [8]byte
386 _, _ = unix.Read(int(fd), buf[:])
387 return 0
388}
389
390func bind(client uintptr, data uintptr, version uint32, id uint32) {
391 if fn := live.binds[data]; fn != nil {
392 fn(Client{client}, version, id)
393 }
394}
395
396// closeRequestFDs takes ownership of fd arguments when there is no handler.
397func closeRequestFDs(positions []int, args unsafe.Pointer) {
398 for _, i := range positions {
399 _ = unix.Close((*Arg)(unsafe.Add(args, i*8)).Fd())
400 }
401}
402
403// argCount counts arguments in a libwayland signature ("?oii", "2n", ...).
404func argCount(sig string) int {
405 n := 0
406 for _, ch := range sig {
407 if ch != '?' && (ch < '0' || ch > '9') {
408 n++
409 }
410 }
411 return n
412}