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