1// Package wlturbo is a Wayland client transport for Go: connection, framing,
2// descriptor passing, object lifecycle and the bootstrap registry. Protocol
3// bindings are generated into the protocol/ packages.
4package wlturbo
5
6import (
7 "bytes"
8 "encoding/binary"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "os"
14 "path/filepath"
15 "strings"
16 "sync"
17 "sync/atomic"
18)
19
20// Pre-allocated buffer pool for performance
21var bufferPool = sync.Pool{
22 New: func() interface{} {
23 return &bytes.Buffer{}
24 },
25}
26
27// Fixed represents a 24.8 fixed-point number
28type Fixed int32
29
30// Float64 converts Fixed to float64
31func (f Fixed) Float64() float64 {
32 return float64(f) / 256.0
33}
34
35// NewFixed creates a Fixed from float64
36func NewFixed(v float64) Fixed {
37 return Fixed(v * 256.0)
38}
39
40// Object represents a Wayland object
41type Object interface {
42 ID() uint32
43}
44
45// Display represents a connection to the Wayland display
46type Display struct {
47 conn net.Conn
48 unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49 objects sync.Map // map[uint32]Object
50 nextID uint32
51 idMu sync.Mutex
52 freeIDs []uint32
53 retiredIDs map[uint32]bool
54 sendMu sync.Mutex
55 recvMu sync.Mutex
56 dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
57 signatures sync.Map
58
59 // Core objects
60 registry *Registry
61 context *Context // Store context once
62
63 // Connection-local receive state. rbuf[rpos:] holds bytes read from the
64 // socket that have not been framed yet; recvBuf is the scratch buffer used
65 // for a single read. pendingFDs holds ancillary descriptors that arrived
66 // with those bytes and have not been consumed yet.
67 rbuf []byte
68 rpos int
69 recvBuf []byte
70 readErr error
71 maxMsgLen uint32
72 closed atomic.Bool
73 pendingFDs []int
74}
75
76// newDisplay builds a Display over an established connection.
77func newDisplay(conn net.Conn) *Display {
78 d := &Display{
79 conn: conn,
80 nextID: 2, // 1 is reserved for wl_display
81 }
82 if uc, ok := conn.(*net.UnixConn); ok {
83 d.unix = uc
84 }
85 d.context = NewContext(d)
86 // Register display object (ID 1)
87 d.objects.Store(uint32(displayObjectID), d)
88 // Initialize registry
89 d.registry = &Registry{
90 id: d.allocateID(),
91 display: d,
92 globals: make(map[uint32]Global),
93 handlers: make(map[string][]GlobalHandler),
94 }
95 d.objects.Store(d.registry.id, d.registry)
96 return d
97}
98
99// Registry represents the global registry
100type Registry struct {
101 id uint32
102 display *Display
103 globals map[uint32]Global
104 mu sync.RWMutex
105 handlers map[string][]GlobalHandler
106 removeHandlers []RegistryGlobalRemoveHandler
107}
108
109// callbackObject represents a wl_callback object
110type callbackObject struct {
111 BaseProxy
112 display *Display
113}
114
115func (c *callbackObject) ID() uint32 {
116 return c.id
117}
118func (c *callbackObject) EventSignature(op uint16) (string, bool) {
119 if op == 0 {
120 return "uint,", true
121 }
122 return "", false
123}
124
125// Dispatch handles callback events (opcode 0 = done)
126func (c *callbackObject) Dispatch(event *Event) {
127 if event.Opcode != 0 { // done event
128 return
129 }
130 c.display.context.Unregister(c)
131}
132
133// Global represents a global object
134type Global struct {
135 Name uint32
136 Interface string
137 Version uint32
138}
139
140// GlobalHandler is called when a global is announced
141type GlobalHandler func(registry *Registry, name uint32, version uint32)
142
143// ConnectFromConn attaches to an established Wayland socket and requests its registry.
144func ConnectFromConn(conn net.Conn) (*Display, error) {
145 d := newDisplay(conn)
146 if err := d.getRegistry(); err != nil {
147 _ = d.Close()
148 return nil, fmt.Errorf("get registry: %w", err)
149 }
150 return d, nil
151}
152
153// Connect connects to the Wayland display and requests the registry.
154func Connect(socketPath string) (*Display, error) {
155 if socketPath == "" {
156 socketPath = os.Getenv("WAYLAND_DISPLAY")
157 if socketPath == "" {
158 socketPath = "wayland-0"
159 }
160 }
161
162 // Resolve socket path
163 if !filepath.IsAbs(socketPath) {
164 runDir := os.Getenv("XDG_RUNTIME_DIR")
165 if runDir == "" {
166 return nil, errors.New("XDG_RUNTIME_DIR not set")
167 }
168 socketPath = filepath.Join(runDir, socketPath)
169 }
170
171 conn, err := net.Dial("unix", socketPath)
172 if err != nil {
173 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
174 }
175
176 return ConnectFromConn(conn)
177}
178
179// Close closes the display connection. It is safe to call more than once;
180// later calls are no-ops. After Close, Dispatch returns net.ErrClosed.
181func (d *Display) Close() error {
182 if !d.closed.CompareAndSwap(false, true) {
183 return nil
184 }
185 d.context.closed.Store(true)
186 err := d.conn.Close()
187 d.recvMu.Lock()
188 d.closePendingFDs()
189 d.recvMu.Unlock()
190 return err
191}
192
193// Closed reports whether the connection has been closed.
194func (d *Display) Closed() bool {
195 return d.closed.Load()
196}
197
198// ID returns the display's object ID (always 1)
199func (d *Display) ID() uint32 {
200 return 1
201}
202
203// allocateID allocates a new object ID
204func (d *Display) allocateID() uint32 {
205 d.idMu.Lock()
206 defer d.idMu.Unlock()
207 if n := len(d.freeIDs); n != 0 {
208 id := d.freeIDs[n-1]
209 d.freeIDs = d.freeIDs[:n-1]
210 delete(d.retiredIDs, id)
211 return id
212 }
213 if d.nextID == 0 || d.nextID >= 0xff000000 {
214 return 0 // exhausted; never allocate a reserved server ID
215 }
216 id := d.nextID
217 d.nextID++
218 return id
219}
220
221// AllocateID allocates a new object ID (public method)
222func (d *Display) AllocateID() uint32 {
223 return d.allocateID()
224}
225
226// SendRequest sends a request to the compositor
227func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{}) error {
228 return d.SendRequestWithFDs(objectID, opcode, nil, args...)
229}
230
231// SendRequestWithFDs sends a request with file descriptors
232func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
233 return d.sendRequest(objectID, opcode, fds, nil, args, nil)
234}
235
236// sendRequest marshals a request, then runs guard and writes the message
237// under the send lock. Guard failures and marshaling errors write nothing.
238// The body is args (boxed, legacy path) followed by vals (typed path); a
239// caller uses one of the two.
240func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}, vals []Arg) error {
241 // Get buffer from pool
242 buf := bufferPool.Get().(*bytes.Buffer)
243 defer func() {
244 buf.Reset()
245 bufferPool.Put(buf)
246 }()
247
248 // Write header placeholder
249 var header [HeaderSize]byte
250 _, _ = buf.Write(header[:])
251
252 // Marshal arguments
253 for _, arg := range args {
254 if err := d.marshalArg(buf, arg); err != nil {
255 return fmt.Errorf("failed to marshal argument: %w", err)
256 }
257 }
258 for i := range vals {
259 if err := marshalValue(buf, &vals[i]); err != nil {
260 return fmt.Errorf("failed to marshal argument: %w", err)
261 }
262 }
263
264 // Update header with actual size in bytes
265 bufLen := buf.Len()
266 if bufLen > 0xFFFF || bufLen%4 != 0 {
267 return fmt.Errorf("invalid message size: %d bytes", bufLen)
268 }
269 size := uint32(bufLen) // Safe: checked above
270 binary.LittleEndian.PutUint32(header[0:4], objectID)
271 // Upper 16 bits = size, lower 16 bits = opcode
272 binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
273
274 // Update buffer with correct header
275 data := buf.Bytes()
276 copy(data[0:8], header[:])
277
278 // Send message with optional file descriptors
279 return d.sendmsgWithFDs(data, fds, guard)
280}
281
282// putUint32 appends one little-endian wire word without allocating.
283func putUint32(buf *bytes.Buffer, v uint32) {
284 var w [4]byte
285 binary.LittleEndian.PutUint32(w[:], v)
286 _, _ = buf.Write(w[:])
287}
288
289// putString appends a wire string: length including the terminating NUL, the
290// bytes, the NUL, and padding to a 32-bit boundary.
291func putString(buf *bytes.Buffer, v string) error {
292 strlen := len(v) + 1
293 if strlen > 0xffff-HeaderSize {
294 return fmt.Errorf("string too long: %d bytes", strlen)
295 }
296 putUint32(buf, uint32(strlen)) // Safe: checked above
297 _, _ = buf.WriteString(v)
298 _ = buf.WriteByte(0)
299 for i := (4 - (strlen % 4)) % 4; i > 0; i-- {
300 _ = buf.WriteByte(0)
301 }
302 return nil
303}
304
305// putArray appends a wire array: length, the bytes and padding to a 32-bit
306// boundary.
307func putArray(buf *bytes.Buffer, v []byte) error {
308 arrlen := len(v)
309 if arrlen > 0xffff-HeaderSize {
310 return fmt.Errorf("array too long: %d bytes", arrlen)
311 }
312 putUint32(buf, uint32(arrlen))
313 _, _ = buf.Write(v)
314 for i := (4 - (arrlen % 4)) % 4; i > 0; i-- {
315 _ = buf.WriteByte(0)
316 }
317 return nil
318}
319
320// marshalValue marshals one typed argument. It shares the string and array
321// encoders with marshalArg, so both paths produce identical bytes.
322func marshalValue(buf *bytes.Buffer, a *Arg) error {
323 switch a.kind {
324 case argWord:
325 putUint32(buf, a.word)
326 case argString:
327 return putString(buf, a.str)
328 case argArray:
329 return putArray(buf, a.arr)
330 case argObject:
331 if a.obj != nil {
332 putUint32(buf, a.obj.ID())
333 } else {
334 putUint32(buf, 0)
335 }
336 case argFD:
337 // Carried by SCM_RIGHTS only; no body word.
338 default:
339 return errors.New("unsupported argument: zero or invalid Arg")
340 }
341 return nil
342}
343
344// marshalArg marshals a single argument
345func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
346 switch v := arg.(type) {
347 case uint32:
348 putUint32(buf, v)
349 case int32:
350 putUint32(buf, uint32(v))
351 case Fixed:
352 putUint32(buf, uint32(v))
353 case string:
354 return putString(buf, v)
355 case []byte:
356 return putArray(buf, v)
357 case Object:
358 if v != nil {
359 putUint32(buf, v.ID())
360 } else {
361 putUint32(buf, 0)
362 }
363 case nil:
364 // Null object
365 putUint32(buf, 0)
366 case uintptr:
367 // File descriptor as uintptr - NO placeholder in message for neurlang compatibility
368 // FD is ONLY sent via SCM_RIGHTS, not in the message body
369 return nil
370 default:
371 return fmt.Errorf("unsupported argument type: %T", arg)
372 }
373 return nil
374}
375
376// Dispatch reads one complete Wayland message and delivers it to the object
377// that owns it.
378//
379// Read boundaries never define message boundaries: a header split across
380// several reads and several messages arriving in one read are both handled.
381// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
382// malformed frame, io.ErrUnexpectedEOF when the peer closes mid-frame, and
383// os.ErrDeadlineExceeded (not sticky) when a read deadline expires.
384//
385// Event handlers run while Dispatch holds the dispatch lock, so a handler must
386// not call Dispatch or Roundtrip on the same Display: that call deadlocks.
387// Hand work that needs a roundtrip to another goroutine instead.
388func (d *Display) Dispatch() error {
389 d.dispatchMu.Lock()
390 defer d.dispatchMu.Unlock()
391 d.recvMu.Lock()
392
393 if d.closed.Load() {
394 d.recvMu.Unlock()
395 return net.ErrClosed
396 }
397
398 frame, err := d.nextFrame()
399 d.recvMu.Unlock()
400 if d.closed.Load() {
401 return net.ErrClosed
402 }
403 if err != nil {
404 return d.fail(err)
405 }
406 return d.fail(d.dispatchFrame(frame))
407}
408
409// fail releases resources that cannot be used once the connection is
410// unusable. A protocol violation invalidates the whole connection, so queued
411// descriptors are closed; transport errors may be transient and keep them.
412func (d *Display) fail(err error) error {
413 if err == nil {
414 return nil
415 }
416 d.recvMu.Lock()
417 defer d.recvMu.Unlock()
418 var perr *ProtocolError
419 if errors.As(err, &perr) {
420 d.closePendingFDs()
421 }
422 if errors.Is(err, io.ErrUnexpectedEOF) {
423 d.closePendingFDs()
424 }
425 return err
426}
427
428// maxMessageSize reports the largest accepted message size.
429func (d *Display) maxMessageSize() uint32 {
430 if d.maxMsgLen == 0 {
431 return DefaultMaxMessageSize
432 }
433 return d.maxMsgLen
434}
435
436// nextFrame returns the next complete frame, reading more bytes as needed.
437func (d *Display) nextFrame() (receivedFrame, error) {
438 for {
439 buf := d.rbuf[d.rpos:]
440 if len(buf) >= HeaderSize {
441 object, opcode, size, err := parseHeader(buf[:HeaderSize], d.maxMessageSize())
442 if err != nil {
443 return receivedFrame{}, err
444 }
445 if uint32(len(buf)) >= size {
446 // The body aliases the receive buffer. It stays valid until
447 // the next fill, which only happens in a later Dispatch.
448 frame := receivedFrame{object: object, opcode: opcode}
449 if size > HeaderSize {
450 frame.body = buf[HeaderSize:size:size]
451 }
452 d.rpos += int(size)
453 return frame, nil
454 }
455 }
456 if err := d.fill(); err != nil {
457 if d.closed.Load() {
458 return receivedFrame{}, net.ErrClosed
459 }
460 return receivedFrame{}, err
461 }
462 }
463}
464
465// unread reports whether received bytes remain that have not been framed.
466func (d *Display) unread() bool { return d.rpos < len(d.rbuf) }
467
468// fill reads one chunk from the connection into the receive buffer. Bytes that
469// arrive together with an error are kept so they are still parsed before the
470// error surfaces.
471func (d *Display) fill() error {
472 if d.readErr != nil {
473 return d.readErr
474 }
475 if d.recvBuf == nil {
476 d.recvBuf = make([]byte, readChunkSize)
477 }
478 // Compact once per read rather than once per frame, so framing cost does
479 // not grow with the number of buffered messages.
480 if d.rpos > 0 {
481 d.rbuf = d.rbuf[:copy(d.rbuf, d.rbuf[d.rpos:])]
482 d.rpos = 0
483 }
484 n, err := d.readChunk()
485 if n > 0 {
486 d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
487 }
488 if err != nil {
489 if errors.Is(err, io.EOF) {
490 err = io.ErrUnexpectedEOF
491 }
492 // A missed deadline is not a broken stream: report it without making
493 // it sticky, so a later Dispatch can read again.
494 if errors.Is(err, os.ErrDeadlineExceeded) {
495 return err
496 }
497 d.readErr = err
498 // A control-message failure corrupts the descriptor stream, so it is
499 // reported immediately instead of after the buffered bytes are used.
500 var perr *ProtocolError
501 if n == 0 || errors.As(err, &perr) {
502 return err
503 }
504 }
505 return nil
506}
507
508// signatureProvider is implemented by generated objects; every opcode must
509// have a known signature before any descriptor can be assigned or dispatched.
510type signatureProvider interface{ EventSignature(uint16) (string, bool) }
511
512// dispatchFrame delivers one complete frame with precisely its signature's FDs.
513func (d *Display) dispatchFrame(f receivedFrame) error {
514 d.recvMu.Lock()
515 obj, ev, err := d.prepareFrame(f)
516 d.recvMu.Unlock()
517 if err != nil {
518 return err
519 }
520 if obj == nil {
521 return d.handleDisplayEvent(f.opcode, f.body)
522 }
523 defer func() {
524 for _, fd := range ev.fds {
525 _ = fd.Close()
526 }
527 *ev = Event{}
528 eventPool.Put(ev)
529 }()
530 if _, isZombie := obj.(*zombie); isZombie {
531 return nil
532 }
533 // Another goroutine may have destroyed the object after prepareFrame
534 // looked it up; a destroyed object receives no further events.
535 if current, _ := d.objects.Load(f.object); current != obj {
536 return nil
537 }
538 if r, ok := obj.(eventReceiver); ok {
539 r.Dispatch(ev)
540 }
541 return nil
542}
543
544// eventReceiver is any registered object that handles its own events:
545// generated proxies, callbacks and the registry.
546type eventReceiver interface{ Dispatch(*Event) }
547
548// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
549// A nil object with a nil error means the frame targets wl_display.
550func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
551 if d.closed.Load() {
552 return nil, nil, net.ErrClosed
553 }
554 if f.object == displayObjectID {
555 if len(d.pendingFDs) != 0 && !d.unread() {
556 return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
557 }
558 return nil, nil, nil
559 }
560 obj, ok := d.objects.Load(f.object)
561 if !ok {
562 return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
563 }
564 sigObj := obj
565 z, isZombie := obj.(*zombie)
566 if isZombie {
567 sigObj = z.object
568 }
569 sig, valid := "", false
570 p, isGenerated := sigObj.(signatureProvider)
571 if isGenerated {
572 sig, valid = p.EventSignature(f.opcode)
573 }
574 // Legacy custom proxies may declare their signatures with RegisterEventSignature.
575 if !valid && !isGenerated {
576 if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
577 sig, valid = v.(string), true
578 }
579 }
580 if !valid {
581 return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
582 }
583 if err := validateEventBody(sig, f.body); err != nil {
584 return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
585 }
586 count := strings.Count(sig, "fd,")
587 if len(d.pendingFDs) < count {
588 return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
589 }
590 ev := eventPool.Get().(*Event)
591 *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
592 if count != 0 {
593 ev.fds = make([]*OwnedFD, count)
594 for i := range ev.fds {
595 ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
596 }
597 d.pendingFDs = d.pendingFDs[count:]
598 }
599 // FDs from a single recvmsg can accompany multiple coalesced frames. Once
600 // all buffered bytes have been framed, surplus descriptors are not legal.
601 if !d.unread() && len(d.pendingFDs) != 0 {
602 for _, fd := range ev.fds {
603 _ = fd.Close()
604 }
605 *ev = Event{}
606 eventPool.Put(ev)
607 return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
608 }
609 // For a zombie the descriptors were still consumed above so the stream
610 // stays aligned; dispatchFrame closes them without running any handler.
611 return obj.(Object), ev, nil
612}
613
614type signatureKey struct {
615 object uint32
616 opcode uint16
617}
618
619// RegisterEventSignature defines a signature for a legacy proxy. Generated
620// proxies supply it themselves and cannot be overridden.
621func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
622 if object == 0 || object == displayObjectID {
623 return
624 }
625 d.signatures.Store(signatureKey{object, opcode}, signature)
626}
627
628// handleDisplayEvent handles events on the display object
629func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
630 switch opcode {
631 case 0: // error
632 if err := validateEventBody("object,uint,string,", data); err != nil {
633 return &ProtocolError{Kind: "malformed_display_error", Err: err}
634 }
635 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
636 objectID := event.Uint32()
637 code := event.Uint32()
638 return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
639
640 case 1: // delete_id
641 if len(data) != 4 {
642 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
643 }
644 id := binary.LittleEndian.Uint32(data[0:4])
645 d.idMu.Lock()
646 obj, live := d.objects.Load(id)
647 if _, isZombie := obj.(*zombie); isZombie {
648 d.objects.CompareAndDelete(id, obj)
649 live = false
650 }
651 if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
652 d.idMu.Unlock()
653 return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
654 }
655 if d.retiredIDs == nil {
656 d.retiredIDs = make(map[uint32]bool)
657 }
658 d.retiredIDs[id] = true
659 d.freeIDs = append(d.freeIDs, id)
660 d.idMu.Unlock()
661 return nil
662
663 default:
664 return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
665 }
666}
667
668// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
669// must not call Roundtrip while another goroutine dispatches this connection.
670func (d *Display) Roundtrip() error {
671 callback, err := d.Sync()
672 if err != nil {
673 return err
674 }
675 for {
676 if err = d.Dispatch(); err != nil {
677 return err
678 }
679 if _, exists := d.objects.Load(callback.ID()); !exists {
680 return nil
681 }
682 }
683}
684
685// getRegistry sends wl_display.get_registry for the bootstrap registry.
686func (d *Display) getRegistry() error {
687 return d.SendRequest(displayObjectID, 1, d.registry.id)
688}
689
690// Registry returns the global registry
691func (d *Display) Registry() *Registry {
692 return d.registry
693}
694
695// ID returns the registry's object ID
696func (r *Registry) ID() uint32 {
697 return r.id
698}
699
700// EventSignature reports the wl_registry event signatures.
701func (r *Registry) EventSignature(opcode uint16) (string, bool) {
702 switch opcode {
703 case 0:
704 return "uint,string,uint,", true
705 case 1:
706 return "uint,", true
707 }
708 return "", false
709}
710
711// Dispatch handles wl_registry.global and wl_registry.global_remove. The
712// transport has already validated the body against EventSignature.
713func (r *Registry) Dispatch(event *Event) {
714 switch event.Opcode {
715 case 0:
716 r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
717 case 1:
718 r.handleGlobalRemove(event.Uint32())
719 }
720}
721
722func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
723 r.mu.Lock()
724 r.globals[name] = Global{Name: name, Interface: iface, Version: version}
725 handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
726 handlers = append(handlers, r.handlers["*"]...)
727 r.mu.Unlock()
728 for _, handler := range handlers {
729 handler(r, name, version)
730 }
731}
732
733func (r *Registry) handleGlobalRemove(name uint32) {
734 r.mu.Lock()
735 delete(r.globals, name)
736 handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
737 r.mu.Unlock()
738 for _, handler := range handlers {
739 handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
740 }
741}
742
743// AddHandler adds a handler for a specific interface, or "*" for every
744// interface. Handlers accumulate; each announced global calls all of them in
745// registration order.
746func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
747 if handler == nil {
748 return
749 }
750 r.mu.Lock()
751 defer r.mu.Unlock()
752 r.handlers[iface] = append(r.handlers[iface], handler)
753}
754
755// ErrGlobalNotFound reports that the compositor does not announce a global.
756var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
757
758// BindNegotiated binds the announced global for iface at
759// min(announced version, supported) and returns that version. It returns
760// ErrGlobalNotFound when the global is absent, so callers can decide whether
761// the capability is optional.
762func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
763 if supported == 0 {
764 return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
765 }
766 g, ok := r.FindGlobal(iface)
767 if !ok || g.Version == 0 {
768 return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
769 }
770 version := min(g.Version, supported)
771 if err := r.Bind(g.Name, iface, version, proxy); err != nil {
772 return 0, err
773 }
774 return version, nil
775}
776
777// Bind binds to a global object and returns a typed proxy
778func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
779 // Set the ID if not already set
780 if proxy.ID() == 0 {
781 proxy.SetID(r.display.allocateID())
782 }
783
784 // Bootstrap a generated proxy with this display's context.
785 if proxy.Context() == nil {
786 if baseProxy, ok := proxy.(*BaseProxy); ok {
787 baseProxy.SetContext(r.display.Context())
788 } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
789 setter.SetContext(r.display.Context())
790 } else {
791 return fmt.Errorf("proxy doesn't have context and can't set it")
792 }
793 }
794
795 if proxy.Context() != r.display.Context() {
796 return fmt.Errorf("proxy belongs to another display")
797 }
798 if proxy.ID() == displayObjectID || proxy.ID() == r.id {
799 return fmt.Errorf("reserved object ID")
800 }
801 if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
802 return fmt.Errorf("object ID %d already registered", proxy.ID())
803 }
804 if v, ok := proxy.(interface{ SetVersion(uint32) }); ok {
805 v.SetVersion(version)
806 }
807 // Register the proxy
808 proxy.Context().Register(proxy)
809
810 // Send bind request (opcode 0) with proper arguments
811 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
812 proxy.Context().Unregister(proxy)
813 return err
814 }
815
816 return nil
817}
818
819// BindID binds to a global object and returns just the ID (compatibility method)
820func (r *Registry) BindID(name uint32, iface string, version uint32) (uint32, error) {
821 newID := r.display.allocateID()
822
823 // Send bind request (opcode 0) with proper arguments
824 if err := r.display.SendRequest(r.id, 0, name, iface, version, newID); err != nil {
825 return 0, err
826 }
827
828 return newID, nil
829}
830
831// GetGlobals returns all announced globals
832func (r *Registry) GetGlobals() map[uint32]Global {
833 r.mu.RLock()
834 defer r.mu.RUnlock()
835
836 globals := make(map[uint32]Global)
837 for k, v := range r.globals {
838 globals[k] = v
839 }
840 return globals
841}
842
843// FindGlobal finds a global by interface name
844func (r *Registry) FindGlobal(iface string) (Global, bool) {
845 r.mu.RLock()
846 defer r.mu.RUnlock()
847
848 for _, global := range r.globals {
849 if global.Interface == iface {
850 return global, true
851 }
852 }
853 return Global{}, false
854}