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)
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.
238func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
239 // Get buffer from pool
240 buf := bufferPool.Get().(*bytes.Buffer)
241 defer func() {
242 buf.Reset()
243 bufferPool.Put(buf)
244 }()
245
246 // Write header placeholder
247 var header [HeaderSize]byte
248 _, _ = buf.Write(header[:])
249
250 // Marshal arguments
251 for _, arg := range args {
252 if err := d.marshalArg(buf, arg); err != nil {
253 return fmt.Errorf("failed to marshal argument: %w", err)
254 }
255 }
256
257 // Update header with actual size in bytes
258 bufLen := buf.Len()
259 if bufLen > 0xFFFF || bufLen%4 != 0 {
260 return fmt.Errorf("invalid message size: %d bytes", bufLen)
261 }
262 size := uint32(bufLen) // Safe: checked above
263 binary.LittleEndian.PutUint32(header[0:4], objectID)
264 // Upper 16 bits = size, lower 16 bits = opcode
265 binary.LittleEndian.PutUint32(header[4:8], (size<<16)|uint32(opcode))
266
267 // Update buffer with correct header
268 data := buf.Bytes()
269 copy(data[0:8], header[:])
270
271 // Send message with optional file descriptors
272 return d.sendmsgWithFDs(data, fds, guard)
273}
274
275// putUint32 appends one little-endian wire word without allocating.
276func putUint32(buf *bytes.Buffer, v uint32) {
277 var w [4]byte
278 binary.LittleEndian.PutUint32(w[:], v)
279 _, _ = buf.Write(w[:])
280}
281
282// marshalArg marshals a single argument
283func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
284 switch v := arg.(type) {
285 case uint32:
286 putUint32(buf, v)
287 case int32:
288 putUint32(buf, uint32(v))
289 case Fixed:
290 putUint32(buf, uint32(v))
291 case string:
292 // String format: length (including null) + string + null + padding
293 strlen := len(v) + 1
294 if strlen > 0xffff-HeaderSize {
295 return fmt.Errorf("string too long: %d bytes", strlen)
296 }
297 putUint32(buf, uint32(strlen)) // Safe: checked above
298 _, _ = buf.WriteString(v)
299 _ = buf.WriteByte(0)
300 // Pad to 32-bit boundary
301 padding := (4 - (strlen % 4)) % 4
302 for i := 0; i < padding; i++ {
303 _ = buf.WriteByte(0)
304 }
305 case []byte:
306 // Array format: length + data + padding
307 arrlen := len(v)
308 if arrlen > 0xffff-HeaderSize {
309 return fmt.Errorf("array too long: %d bytes", arrlen)
310 }
311 putUint32(buf, uint32(arrlen))
312 _, _ = buf.Write(v)
313 // Pad to 32-bit boundary
314 padding := (4 - (arrlen % 4)) % 4
315 for i := 0; i < padding; i++ {
316 _ = buf.WriteByte(0)
317 }
318 case Object:
319 if v != nil {
320 putUint32(buf, v.ID())
321 } else {
322 putUint32(buf, 0)
323 }
324 case nil:
325 // Null object
326 putUint32(buf, 0)
327 case uintptr:
328 // File descriptor as uintptr - NO placeholder in message for neurlang compatibility
329 // FD is ONLY sent via SCM_RIGHTS, not in the message body
330 return nil
331 default:
332 return fmt.Errorf("unsupported argument type: %T", arg)
333 }
334 return nil
335}
336
337// Dispatch reads one complete Wayland message and delivers it to the object
338// that owns it.
339//
340// Read boundaries never define message boundaries: a header split across
341// several reads and several messages arriving in one read are both handled.
342// Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
343// malformed frame, io.ErrUnexpectedEOF when the peer closes mid-frame, and
344// os.ErrDeadlineExceeded (not sticky) when a read deadline expires.
345//
346// Event handlers run while Dispatch holds the dispatch lock, so a handler must
347// not call Dispatch or Roundtrip on the same Display: that call deadlocks.
348// Hand work that needs a roundtrip to another goroutine instead.
349func (d *Display) Dispatch() error {
350 d.dispatchMu.Lock()
351 defer d.dispatchMu.Unlock()
352 d.recvMu.Lock()
353
354 if d.closed.Load() {
355 d.recvMu.Unlock()
356 return net.ErrClosed
357 }
358
359 frame, err := d.nextFrame()
360 d.recvMu.Unlock()
361 if d.closed.Load() {
362 return net.ErrClosed
363 }
364 if err != nil {
365 return d.fail(err)
366 }
367 return d.fail(d.dispatchFrame(frame))
368}
369
370// fail releases resources that cannot be used once the connection is
371// unusable. A protocol violation invalidates the whole connection, so queued
372// descriptors are closed; transport errors may be transient and keep them.
373func (d *Display) fail(err error) error {
374 if err == nil {
375 return nil
376 }
377 d.recvMu.Lock()
378 defer d.recvMu.Unlock()
379 var perr *ProtocolError
380 if errors.As(err, &perr) {
381 d.closePendingFDs()
382 }
383 if errors.Is(err, io.ErrUnexpectedEOF) {
384 d.closePendingFDs()
385 }
386 return err
387}
388
389// maxMessageSize reports the largest accepted message size.
390func (d *Display) maxMessageSize() uint32 {
391 if d.maxMsgLen == 0 {
392 return DefaultMaxMessageSize
393 }
394 return d.maxMsgLen
395}
396
397// nextFrame returns the next complete frame, reading more bytes as needed.
398func (d *Display) nextFrame() (receivedFrame, error) {
399 for {
400 buf := d.rbuf[d.rpos:]
401 if len(buf) >= HeaderSize {
402 object, opcode, size, err := parseHeader(buf[:HeaderSize], d.maxMessageSize())
403 if err != nil {
404 return receivedFrame{}, err
405 }
406 if uint32(len(buf)) >= size {
407 // The body aliases the receive buffer. It stays valid until
408 // the next fill, which only happens in a later Dispatch.
409 frame := receivedFrame{object: object, opcode: opcode}
410 if size > HeaderSize {
411 frame.body = buf[HeaderSize:size:size]
412 }
413 d.rpos += int(size)
414 return frame, nil
415 }
416 }
417 if err := d.fill(); err != nil {
418 if d.closed.Load() {
419 return receivedFrame{}, net.ErrClosed
420 }
421 return receivedFrame{}, err
422 }
423 }
424}
425
426// unread reports whether received bytes remain that have not been framed.
427func (d *Display) unread() bool { return d.rpos < len(d.rbuf) }
428
429// fill reads one chunk from the connection into the receive buffer. Bytes that
430// arrive together with an error are kept so they are still parsed before the
431// error surfaces.
432func (d *Display) fill() error {
433 if d.readErr != nil {
434 return d.readErr
435 }
436 if d.recvBuf == nil {
437 d.recvBuf = make([]byte, readChunkSize)
438 }
439 // Compact once per read rather than once per frame, so framing cost does
440 // not grow with the number of buffered messages.
441 if d.rpos > 0 {
442 d.rbuf = d.rbuf[:copy(d.rbuf, d.rbuf[d.rpos:])]
443 d.rpos = 0
444 }
445 n, err := d.readChunk()
446 if n > 0 {
447 d.rbuf = append(d.rbuf, d.recvBuf[:n]...)
448 }
449 if err != nil {
450 if errors.Is(err, io.EOF) {
451 err = io.ErrUnexpectedEOF
452 }
453 // A missed deadline is not a broken stream: report it without making
454 // it sticky, so a later Dispatch can read again.
455 if errors.Is(err, os.ErrDeadlineExceeded) {
456 return err
457 }
458 d.readErr = err
459 // A control-message failure corrupts the descriptor stream, so it is
460 // reported immediately instead of after the buffered bytes are used.
461 var perr *ProtocolError
462 if n == 0 || errors.As(err, &perr) {
463 return err
464 }
465 }
466 return nil
467}
468
469// signatureProvider is implemented by generated objects; every opcode must
470// have a known signature before any descriptor can be assigned or dispatched.
471type signatureProvider interface{ EventSignature(uint16) (string, bool) }
472
473// dispatchFrame delivers one complete frame with precisely its signature's FDs.
474func (d *Display) dispatchFrame(f receivedFrame) error {
475 d.recvMu.Lock()
476 obj, ev, err := d.prepareFrame(f)
477 d.recvMu.Unlock()
478 if err != nil {
479 return err
480 }
481 if obj == nil {
482 return d.handleDisplayEvent(f.opcode, f.body)
483 }
484 defer func() {
485 for _, fd := range ev.fds {
486 _ = fd.Close()
487 }
488 *ev = Event{}
489 eventPool.Put(ev)
490 }()
491 if _, isZombie := obj.(*zombie); isZombie {
492 return nil
493 }
494 // Another goroutine may have destroyed the object after prepareFrame
495 // looked it up; a destroyed object receives no further events.
496 if current, _ := d.objects.Load(f.object); current != obj {
497 return nil
498 }
499 if r, ok := obj.(eventReceiver); ok {
500 r.Dispatch(ev)
501 }
502 return nil
503}
504
505// eventReceiver is any registered object that handles its own events:
506// generated proxies, callbacks and the registry.
507type eventReceiver interface{ Dispatch(*Event) }
508
509// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
510// A nil object with a nil error means the frame targets wl_display.
511func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, error) {
512 if d.closed.Load() {
513 return nil, nil, net.ErrClosed
514 }
515 if f.object == displayObjectID {
516 if len(d.pendingFDs) != 0 && !d.unread() {
517 return nil, nil, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
518 }
519 return nil, nil, nil
520 }
521 obj, ok := d.objects.Load(f.object)
522 if !ok {
523 return nil, nil, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
524 }
525 sigObj := obj
526 z, isZombie := obj.(*zombie)
527 if isZombie {
528 sigObj = z.object
529 }
530 sig, valid := "", false
531 p, isGenerated := sigObj.(signatureProvider)
532 if isGenerated {
533 sig, valid = p.EventSignature(f.opcode)
534 }
535 // Legacy custom proxies may declare their signatures with RegisterEventSignature.
536 if !valid && !isGenerated {
537 if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
538 sig, valid = v.(string), true
539 }
540 }
541 if !valid {
542 return nil, nil, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
543 }
544 if err := validateEventBody(sig, f.body); err != nil {
545 return nil, nil, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
546 }
547 count := strings.Count(sig, "fd,")
548 if len(d.pendingFDs) < count {
549 return nil, nil, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
550 }
551 ev := eventPool.Get().(*Event)
552 *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
553 if count != 0 {
554 ev.fds = make([]*OwnedFD, count)
555 for i := range ev.fds {
556 ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
557 }
558 d.pendingFDs = d.pendingFDs[count:]
559 }
560 // FDs from a single recvmsg can accompany multiple coalesced frames. Once
561 // all buffered bytes have been framed, surplus descriptors are not legal.
562 if !d.unread() && len(d.pendingFDs) != 0 {
563 for _, fd := range ev.fds {
564 _ = fd.Close()
565 }
566 *ev = Event{}
567 eventPool.Put(ev)
568 return nil, nil, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
569 }
570 // For a zombie the descriptors were still consumed above so the stream
571 // stays aligned; dispatchFrame closes them without running any handler.
572 return obj.(Object), ev, nil
573}
574
575type signatureKey struct {
576 object uint32
577 opcode uint16
578}
579
580// RegisterEventSignature defines a signature for a legacy proxy. Generated
581// proxies supply it themselves and cannot be overridden.
582func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
583 if object == 0 || object == displayObjectID {
584 return
585 }
586 d.signatures.Store(signatureKey{object, opcode}, signature)
587}
588
589// handleDisplayEvent handles events on the display object
590func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
591 switch opcode {
592 case 0: // error
593 if err := validateEventBody("object,uint,string,", data); err != nil {
594 return &ProtocolError{Kind: "malformed_display_error", Err: err}
595 }
596 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
597 objectID := event.Uint32()
598 code := event.Uint32()
599 return &DisplayError{ObjectID: objectID, Code: code, Message: event.String()}
600
601 case 1: // delete_id
602 if len(data) != 4 {
603 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
604 }
605 id := binary.LittleEndian.Uint32(data[0:4])
606 d.idMu.Lock()
607 obj, live := d.objects.Load(id)
608 if _, isZombie := obj.(*zombie); isZombie {
609 d.objects.CompareAndDelete(id, obj)
610 live = false
611 }
612 if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
613 d.idMu.Unlock()
614 return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
615 }
616 if d.retiredIDs == nil {
617 d.retiredIDs = make(map[uint32]bool)
618 }
619 d.retiredIDs[id] = true
620 d.freeIDs = append(d.freeIDs, id)
621 d.idMu.Unlock()
622 return nil
623
624 default:
625 return &ProtocolError{Kind: "unknown_opcode", Object: displayObjectID, Opcode: opcode, Err: ErrUnknownOpcode}
626 }
627}
628
629// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
630// must not call Roundtrip while another goroutine dispatches this connection.
631func (d *Display) Roundtrip() error {
632 callback, err := d.Sync()
633 if err != nil {
634 return err
635 }
636 for {
637 if err = d.Dispatch(); err != nil {
638 return err
639 }
640 if _, exists := d.objects.Load(callback.ID()); !exists {
641 return nil
642 }
643 }
644}
645
646// getRegistry sends wl_display.get_registry for the bootstrap registry.
647func (d *Display) getRegistry() error {
648 return d.SendRequest(displayObjectID, 1, d.registry.id)
649}
650
651// Registry returns the global registry
652func (d *Display) Registry() *Registry {
653 return d.registry
654}
655
656// ID returns the registry's object ID
657func (r *Registry) ID() uint32 {
658 return r.id
659}
660
661// EventSignature reports the wl_registry event signatures.
662func (r *Registry) EventSignature(opcode uint16) (string, bool) {
663 switch opcode {
664 case 0:
665 return "uint,string,uint,", true
666 case 1:
667 return "uint,", true
668 }
669 return "", false
670}
671
672// Dispatch handles wl_registry.global and wl_registry.global_remove. The
673// transport has already validated the body against EventSignature.
674func (r *Registry) Dispatch(event *Event) {
675 switch event.Opcode {
676 case 0:
677 r.handleGlobal(event.Uint32(), event.String(), event.Uint32())
678 case 1:
679 r.handleGlobalRemove(event.Uint32())
680 }
681}
682
683func (r *Registry) handleGlobal(name uint32, iface string, version uint32) {
684 r.mu.Lock()
685 r.globals[name] = Global{Name: name, Interface: iface, Version: version}
686 handlers := append([]GlobalHandler(nil), r.handlers[iface]...)
687 handlers = append(handlers, r.handlers["*"]...)
688 r.mu.Unlock()
689 for _, handler := range handlers {
690 handler(r, name, version)
691 }
692}
693
694func (r *Registry) handleGlobalRemove(name uint32) {
695 r.mu.Lock()
696 delete(r.globals, name)
697 handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
698 r.mu.Unlock()
699 for _, handler := range handlers {
700 handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
701 }
702}
703
704// AddHandler adds a handler for a specific interface, or "*" for every
705// interface. Handlers accumulate; each announced global calls all of them in
706// registration order.
707func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
708 if handler == nil {
709 return
710 }
711 r.mu.Lock()
712 defer r.mu.Unlock()
713 r.handlers[iface] = append(r.handlers[iface], handler)
714}
715
716// ErrGlobalNotFound reports that the compositor does not announce a global.
717var ErrGlobalNotFound = errors.New("wlturbo: global not announced")
718
719// BindNegotiated binds the announced global for iface at
720// min(announced version, supported) and returns that version. It returns
721// ErrGlobalNotFound when the global is absent, so callers can decide whether
722// the capability is optional.
723func (r *Registry) BindNegotiated(iface string, supported uint32, proxy Proxy) (uint32, error) {
724 if supported == 0 {
725 return 0, fmt.Errorf("wlturbo: supported version for %s must be at least 1", iface)
726 }
727 g, ok := r.FindGlobal(iface)
728 if !ok || g.Version == 0 {
729 return 0, fmt.Errorf("%w: %s", ErrGlobalNotFound, iface)
730 }
731 version := min(g.Version, supported)
732 if err := r.Bind(g.Name, iface, version, proxy); err != nil {
733 return 0, err
734 }
735 return version, nil
736}
737
738// Bind binds to a global object and returns a typed proxy
739func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy) error {
740 // Set the ID if not already set
741 if proxy.ID() == 0 {
742 proxy.SetID(r.display.allocateID())
743 }
744
745 // Bootstrap a generated proxy with this display's context.
746 if proxy.Context() == nil {
747 if baseProxy, ok := proxy.(*BaseProxy); ok {
748 baseProxy.SetContext(r.display.Context())
749 } else if setter, ok := proxy.(interface{ SetContext(*Context) }); ok {
750 setter.SetContext(r.display.Context())
751 } else {
752 return fmt.Errorf("proxy doesn't have context and can't set it")
753 }
754 }
755
756 if proxy.Context() != r.display.Context() {
757 return fmt.Errorf("proxy belongs to another display")
758 }
759 if proxy.ID() == displayObjectID || proxy.ID() == r.id {
760 return fmt.Errorf("reserved object ID")
761 }
762 if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
763 return fmt.Errorf("object ID %d already registered", proxy.ID())
764 }
765 if v, ok := proxy.(interface{ SetVersion(uint32) }); ok {
766 v.SetVersion(version)
767 }
768 // Register the proxy
769 proxy.Context().Register(proxy)
770
771 // Send bind request (opcode 0) with proper arguments
772 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
773 proxy.Context().Unregister(proxy)
774 return err
775 }
776
777 return nil
778}
779
780// BindID binds to a global object and returns just the ID (compatibility method)
781func (r *Registry) BindID(name uint32, iface string, version uint32) (uint32, error) {
782 newID := r.display.allocateID()
783
784 // Send bind request (opcode 0) with proper arguments
785 if err := r.display.SendRequest(r.id, 0, name, iface, version, newID); err != nil {
786 return 0, err
787 }
788
789 return newID, nil
790}
791
792// GetGlobals returns all announced globals
793func (r *Registry) GetGlobals() map[uint32]Global {
794 r.mu.RLock()
795 defer r.mu.RUnlock()
796
797 globals := make(map[uint32]Global)
798 for k, v := range r.globals {
799 globals[k] = v
800 }
801 return globals
802}
803
804// FindGlobal finds a global by interface name
805func (r *Registry) FindGlobal(iface string) (Global, bool) {
806 r.mu.RLock()
807 defer r.mu.RUnlock()
808
809 for _, global := range r.globals {
810 if global.Interface == iface {
811 return global, true
812 }
813 }
814 return Global{}, false
815}