1package wlturbo
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "sync"
8 "sync/atomic"
9
10 "golang.org/x/sys/unix"
11)
12
13// Context provides a compatibility layer for wl.Context
14type Context struct {
15 display *Display
16 proxies sync.Map // map[uint32]Proxy
17 closed atomic.Bool
18}
19
20// Proxy interface for Wayland protocol objects
21type Proxy interface {
22 Object
23 SetID(uint32)
24 Context() *Context
25 Dispatch(*Event)
26}
27
28// BaseProxy provides base implementation for protocol objects
29type BaseProxy struct {
30 id uint32
31 context *Context
32 version uint32
33}
34
35// eventPool recycles Events across dispatches; an Event is only valid for
36// the duration of the handler call.
37var eventPool = sync.Pool{New: func() any { return &Event{} }}
38
39// Event represents a Wayland protocol event
40type Event struct {
41 ProxyID uint32
42 Opcode uint16
43 data []byte
44 offset int
45 display *Display
46 fds []*OwnedFD
47 fdIndex int
48}
49
50// Data returns the raw event body. It aliases the connection's receive buffer
51// and is only valid until the handler returns; copy it to keep it.
52func (e *Event) Data() []byte {
53 return e.data
54}
55
56// Offset returns the current read offset
57func (e *Event) Offset() int {
58 return e.offset
59}
60
61// NewContext creates a new context from a display
62func NewContext(display *Display) *Context {
63 return &Context{
64 display: display,
65 }
66}
67
68// SendRequest sends a request through the context. The proxy is validated
69// under the connection send lock, so no request reaches the wire after a
70// destructor for the same proxy.
71func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
72 return c.SendRequestWithFDs(proxy, opcode, nil, args...)
73}
74
75// SendRequestWithFDs sends a request with file descriptors through the context.
76// On error the FDs remain owned by the caller.
77func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
78 return c.sendChecked(proxy, opcode, fds, c.CheckProxy, args, nil)
79}
80
81// CheckProxy rejects stale and foreign proxies before any bytes are written.
82func (c *Context) CheckProxy(proxy Proxy) error {
83 if c.closed.Load() {
84 return errors.New("context is closed")
85 }
86 if proxy == nil || proxy.ID() == 0 || proxy.Context() != c {
87 return errors.New("invalid proxy")
88 }
89 registered, ok := c.proxies.Load(proxy.ID())
90 if !ok || registered != proxy {
91 return errors.New("proxy is not registered")
92 }
93 return nil
94}
95
96// Request describes one protocol request for Context.Request and
97// Context.RequestArgs.
98type Request struct {
99 Proxy Proxy
100 Opcode uint32
101 Name string // interface.request, used in errors
102 Since uint32 // version that introduced the request; 0 or 1 means always
103 Destructor bool // claim the proxy exactly once, as SendDestructor does
104 Child Proxy // new_id object created by this request, or nil
105 FDs []int // descriptors to attach; closed after a successful send
106}
107
108// Request runs one request with its whole lifecycle: it checks the proxy and
109// version, allocates and registers Child (inheriting the parent's version),
110// sends, and then either closes the sent descriptors or, on error,
111// unregisters Child and leaves the descriptors with the caller. Child must
112// already have this context; it is passed in args at its wire position.
113//
114// Every argument is boxed into an interface, and a descriptor is a uintptr
115// placeholder that r.FDs is not checked against. Generated bindings use
116// RequestArgs instead, which marshals the same bytes without allocating.
117func (c *Context) Request(r Request, args ...interface{}) error {
118 return c.request(r, args, nil, false)
119}
120
121// RequestArgs is Request with typed arguments: same lifecycle, same wire
122// bytes, no per-argument interface boxing. Child, when set, must be passed as
123// an ArgObject at its wire position. The number of ArgFD markers must equal
124// len(r.FDs); a mismatch fails before the child is allocated, the send guard
125// runs or anything is written, and leaves the descriptors with the caller.
126func (c *Context) RequestArgs(r Request, args ...Arg) error {
127 return c.request(r, nil, args, true)
128}
129
130func (c *Context) request(r Request, args []interface{}, vals []Arg, typed bool) error {
131 if typed {
132 markers := 0
133 for i := range vals {
134 if vals[i].kind == argFD {
135 markers++
136 }
137 }
138 if markers != len(r.FDs) {
139 return fmt.Errorf("wlturbo: %s: %d fd arguments but %d descriptors", r.Name, markers, len(r.FDs))
140 }
141 }
142 if err := c.CheckProxy(r.Proxy); err != nil {
143 return err
144 }
145 if v, ok := r.Proxy.(interface{ Version() uint32 }); ok {
146 if err := CheckVersion(v.Version(), r.Since, r.Name); err != nil {
147 return err
148 }
149 }
150 if r.Child != nil {
151 r.Child.SetID(c.AllocateID())
152 if v, ok := r.Proxy.(interface{ Version() uint32 }); ok {
153 if s, ok := r.Child.(interface{ SetVersion(uint32) }); ok {
154 s.SetVersion(v.Version())
155 }
156 }
157 c.Register(r.Child)
158 }
159 var err error
160 if r.Destructor {
161 err = c.sendChecked(r.Proxy, r.Opcode, r.FDs, c.claimDestroy, args, vals)
162 } else {
163 err = c.sendChecked(r.Proxy, r.Opcode, r.FDs, c.CheckProxy, args, vals)
164 }
165 if err != nil {
166 if r.Child != nil {
167 c.Unregister(r.Child)
168 }
169 return err
170 }
171 for _, fd := range r.FDs {
172 _ = CloseSentFD(fd)
173 }
174 return nil
175}
176
177// sendChecked validates the proxy, then sends with check run under the send
178// lock (CheckProxy for plain requests, claimDestroy for destructors).
179func (c *Context) sendChecked(proxy Proxy, opcode uint32, fds []int, check func(Proxy) error, args []interface{}, vals []Arg) error {
180 if err := c.CheckProxy(proxy); err != nil {
181 return err
182 }
183 return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return check(proxy) }, args, vals)
184}
185
186// SendDestructor sends a destructor request exactly once. The proxy is
187// claimed (unregistered) under the connection send lock, after marshaling
188// and immediately before the write: concurrent destructors and later requests
189// on the proxy fail without writing. A marshaling error leaves the proxy
190// registered. A write error after the claim leaves the proxy unregistered;
191// such an error means the connection is broken and must be closed.
192func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
193 return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
194}
195
196// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
197// the FDs remain owned by the caller.
198func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
199 return c.sendChecked(proxy, opcode, fds, c.claimDestroy, args, nil)
200}
201
202func (c *Context) claimDestroy(proxy Proxy) error {
203 if err := c.CheckProxy(proxy); err != nil {
204 return err
205 }
206 if !c.proxies.CompareAndDelete(proxy.ID(), proxy) {
207 return errors.New("proxy is not registered")
208 }
209 // The compositor may already have sent events for this object. Keep a
210 // zombie until delete_id so those events are dropped, not fatal.
211 c.display.objects.CompareAndSwap(proxy.ID(), proxy, &zombie{object: proxy})
212 return nil
213}
214
215// zombie stands in for an object the client destroyed until the compositor
216// acknowledges the destruction with wl_display.delete_id. Events that were in
217// flight for it are discarded; their descriptors are closed.
218type zombie struct{ object Object }
219
220func (z *zombie) ID() uint32 { return z.object.ID() }
221
222// Register registers a proxy object
223func (c *Context) Register(proxy Proxy) {
224 if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
225 if proxy.Context() != c {
226 return
227 }
228 c.proxies.Store(proxy.ID(), proxy)
229 c.display.objects.Store(proxy.ID(), proxy)
230 }
231}
232
233// Unregister removes a proxy object
234// Only this proxy is removed: a zombie or a newer object that took the ID
235// is left in place.
236func (c *Context) Unregister(proxy Proxy) {
237 if proxy != nil {
238 c.proxies.CompareAndDelete(proxy.ID(), proxy)
239 c.display.objects.CompareAndDelete(proxy.ID(), proxy)
240 }
241}
242
243// UnregisterID removes a proxy object by ID (overloaded for compatibility)
244func (c *Context) UnregisterID(id uint32) {
245 c.proxies.Delete(id)
246 c.display.objects.Delete(id)
247}
248
249// AllocateID allocates a new object ID
250func (c *Context) AllocateID() uint32 {
251 return c.display.AllocateID()
252}
253
254// Close closes the context
255func (c *Context) Close() error {
256 c.closed.Store(true)
257 return c.display.Close()
258}
259
260// RunTill runs the event loop until the callback fires
261func (c *Context) RunTill(callback Object) error {
262 if c.closed.Load() {
263 return errors.New("context is closed")
264 }
265
266 // Special case: if callback is a sync object, do a roundtrip
267 if _, ok := callback.(*callbackObject); ok {
268 return c.display.Roundtrip()
269 }
270
271 // Otherwise, process events until the callback is unregistered
272 // This assumes the callback will unregister itself when done
273 for {
274 if err := c.display.Dispatch(); err != nil {
275 return err
276 }
277
278 // Check if callback is still registered
279 if _, ok := c.proxies.Load(callback.ID()); !ok {
280 // Callback has been unregistered, we're done
281 return nil
282 }
283 }
284}
285
286// BaseProxy methods
287
288// ID returns the proxy's object ID
289func (p *BaseProxy) ID() uint32 {
290 return p.id
291}
292
293// SetId sets the proxy's object ID
294func (p *BaseProxy) SetID(id uint32) {
295 p.id = id
296}
297
298// Context returns the proxy's context
299func (p *BaseProxy) Context() *Context {
300 return p.context
301}
302
303// SetContext sets the proxy's context
304func (p *BaseProxy) SetContext(ctx *Context) {
305 p.context = ctx
306}
307
308// Version returns the protocol version the object was bound or created at,
309// or 0 when it is unknown (for example after Registry.BindID).
310func (p *BaseProxy) Version() uint32 {
311 return p.version
312}
313
314// SetVersion records the object's protocol version. Registry.Bind sets it for
315// globals and generated requests copy it from parent to child.
316func (p *BaseProxy) SetVersion(v uint32) {
317 p.version = v
318}
319
320// ErrVersionTooLow reports a request the bound object version does not have.
321var ErrVersionTooLow = errors.New("wlturbo: request needs a newer object version")
322
323// CheckVersion rejects a request introduced in version since when the
324// object's known version is lower. An unknown version (0) is not checked.
325func CheckVersion(version, since uint32, request string) error {
326 if version != 0 && version < since {
327 return fmt.Errorf("%w: %s needs version %d, object has %d", ErrVersionTooLow, request, since, version)
328 }
329 return nil
330}
331
332// Dispatch default implementation (does nothing)
333func (p *BaseProxy) Dispatch(event *Event) {
334 // Default implementation does nothing
335}
336
337// Event methods for extracting data
338
339// Uint32 reads a uint32 from the event
340func (e *Event) Uint32() uint32 {
341 if e.offset+4 > len(e.data) {
342 return 0
343 }
344 val := binary.LittleEndian.Uint32(e.data[e.offset:])
345 e.offset += 4
346 return val
347}
348
349// Int32 reads an int32 from the event
350func (e *Event) Int32() int32 {
351 return int32(e.Uint32())
352}
353
354// Fixed reads a fixed-point value from the event
355func (e *Event) Fixed() Fixed {
356 return Fixed(e.Int32())
357}
358
359// String reads a string from the event
360func (e *Event) String() string {
361 if e.offset+4 > len(e.data) {
362 return ""
363 }
364 strlen := e.Uint32()
365 if strlen == 0 || e.offset+int(strlen) > len(e.data) {
366 return ""
367 }
368 // String includes null terminator in length
369 str := string(e.data[e.offset : e.offset+int(strlen)-1])
370 // Advance offset including padding
371 totalLen := strlen
372 padding := (4 - (totalLen % 4)) % 4
373 e.offset += int(totalLen + padding)
374 return str
375}
376
377// Array reads a byte array from the event
378func (e *Event) Array() []byte {
379 if e.offset+4 > len(e.data) {
380 return nil
381 }
382 arrlen := e.Uint32()
383 if arrlen == 0 || e.offset+int(arrlen) > len(e.data) {
384 return nil
385 }
386 arr := make([]byte, arrlen)
387 copy(arr, e.data[e.offset:e.offset+int(arrlen)])
388 // Advance offset including padding
389 padding := (4 - (arrlen % 4)) % 4
390 e.offset += int(arrlen + padding)
391 return arr
392}
393
394// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
395// ownership to the caller (who must close it). Descriptor zero is valid.
396type OwnedFD struct {
397 mu sync.Mutex
398 fd int
399}
400
401func (f *OwnedFD) Take() (int, error) {
402 if f == nil {
403 return -1, errors.New("missing descriptor")
404 }
405 f.mu.Lock()
406 defer f.mu.Unlock()
407 if f.fd < 0 {
408 return -1, errors.New("descriptor already taken or closed")
409 }
410 n := f.fd
411 f.fd = -1
412 return n, nil
413}
414func (f *OwnedFD) Close() error {
415 if f == nil {
416 return nil
417 }
418 f.mu.Lock()
419 defer f.mu.Unlock()
420 if f.fd < 0 {
421 return nil
422 }
423 n := f.fd
424 f.fd = -1
425 return unix.Close(n)
426}
427
428// FD transfers ownership of the next descriptor assigned to this event.
429// An unclaimed descriptor is closed when dispatch completes.
430func (e *Event) FD() *OwnedFD {
431 if e.fdIndex >= len(e.fds) {
432 return nil
433 }
434 fd := e.fds[e.fdIndex]
435 e.fds[e.fdIndex] = nil
436 e.fdIndex++
437 return fd
438}
439
440// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
441func (e *Event) Fd() uintptr {
442 fd := e.FD()
443 if fd == nil {
444 _ = e.Uint32()
445 return 0
446 }
447 n, _ := fd.Take()
448 return uintptr(n)
449}
450
451// NewId reads a new object ID from the event
452func (e *Event) NewID() Proxy {
453 id := e.Uint32()
454 // Return a basic proxy with just the ID
455 return &BaseProxy{id: id}
456}
457
458// Proxy reads an existing proxy reference from the event
459func (e *Event) Proxy() Proxy {
460 id := e.Uint32()
461 // Would need context reference to look up actual proxy
462 return &BaseProxy{id: id}
463}
464
465// Registry handler interfaces for compatibility
466
467// RegistryGlobalHandler interface
468type RegistryGlobalHandler interface {
469 HandleRegistryGlobal(event RegistryGlobalEvent)
470}
471
472// RegistryGlobalRemoveHandler interface
473type RegistryGlobalRemoveHandler interface {
474 HandleRegistryGlobalRemove(event RegistryGlobalRemoveEvent)
475}
476
477// RegistryGlobalEvent represents a registry global announcement
478type RegistryGlobalEvent struct {
479 Registry *Registry
480 Name uint32
481 Interface string
482 Version uint32
483}
484
485// RegistryGlobalRemoveEvent represents a registry global removal
486type RegistryGlobalRemoveEvent struct {
487 Registry *Registry
488 Name uint32
489}
490
491// AddGlobalHandler adds a global handler to the registry
492func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
493 if handler == nil {
494 return
495 }
496 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
497 if global, ok := r.FindGlobalByName(name); ok {
498 handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
499 }
500 })
501}
502
503// AddGlobalRemoveHandler registers a handler for registry removals.
504func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
505 if handler == nil {
506 return
507 }
508 r.mu.Lock()
509 r.removeHandlers = append(r.removeHandlers, handler)
510 r.mu.Unlock()
511}
512
513// FindGlobalByName finds a global by its name ID
514func (r *Registry) FindGlobalByName(name uint32) (Global, bool) {
515 r.mu.RLock()
516 defer r.mu.RUnlock()
517
518 if global, ok := r.globals[name]; ok {
519 return global, true
520 }
521 return Global{}, false
522}
523
524// Display compatibility methods
525
526// Context returns a context for this display
527func (d *Display) Context() *Context {
528 return d.context
529}
530
531// GetRegistry returns the registry (compatibility)
532func (d *Display) GetRegistry() *Registry {
533 return d.registry
534}
535
536// Sync creates a sync callback
537func (d *Display) Sync() (Object, error) {
538 callbackID := d.allocateID()
539 callback := &callbackObject{
540 BaseProxy: BaseProxy{
541 context: d.context,
542 id: callbackID,
543 },
544 display: d,
545 }
546
547 // Store before sending: the compositor may reply immediately.
548 d.context.Register(callback)
549 if err := d.SendRequest(1, 0, callbackID); err != nil {
550 d.context.Unregister(callback)
551 return nil, err
552 }
553
554 return callback, nil
555}