detached c83f653

merge: generated core bindings and hardened transport (Veya P2)

commit
c83f653555760c2695fdf83ac68e470d4d385fb0
author
brice · · 2026-09-28 07:48 UTC
merge of
ab733e5 + 1cc5bed · diff against the first parent
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
24 files · +10254 -762
A benchmark_test.go +26 -0
@@ -0,0 +1,26 @@
1+package wlturbo
2+
3+import "testing"
4+
5+// BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling.
6+func BenchmarkWireDispatch(b *testing.B) {
7+ d := newDisplay(&chunkConn{})
8+ p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}}
9+ d.context.Register(p)
10+ f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}}
11+ b.ReportAllocs()
12+ b.ResetTimer()
13+ for i := 0; i < b.N; i++ {
14+ if err := d.dispatchFrame(f); err != nil {
15+ b.Fatal(err)
16+ }
17+ }
18+}
19+
20+type benchProxy struct {
21+ BaseProxy
22+ value uint32
23+}
24+
25+func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 }
26+func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() }
@@ -0,0 +1,26 @@
1+package wlturbo
2+
3+import "testing"
4+
5+// BenchmarkWireDispatch isolates framed typed dispatch from socket scheduling.
6+func BenchmarkWireDispatch(b *testing.B) {
7+ d := newDisplay(&chunkConn{})
8+ p := &benchProxy{BaseProxy: BaseProxy{id: 7, context: d.context}}
9+ d.context.Register(p)
10+ f := receivedFrame{object: 7, opcode: 0, body: []byte{1, 0, 0, 0}}
11+ b.ReportAllocs()
12+ b.ResetTimer()
13+ for i := 0; i < b.N; i++ {
14+ if err := d.dispatchFrame(f); err != nil {
15+ b.Fatal(err)
16+ }
17+ }
18+}
19+
20+type benchProxy struct {
21+ BaseProxy
22+ value uint32
23+}
24+
25+func (p *benchProxy) EventSignature(op uint16) (string, bool) { return "uint,", op == 0 }
26+func (p *benchProxy) Dispatch(e *Event) { p.value = e.Uint32() }
M client.go +259 -228
@@ -13,6 +13,7 @@ import (
1313 "net"
1414 "os"
1515 "path/filepath"
16+ "strings"
1617 "sync"
1718 "sync/atomic"
1819 )
@@ -44,13 +45,19 @@ type Object interface {
4445
4546 // Display represents a connection to the Wayland display
4647 type 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- sendMu sync.Mutex
52- recvMu sync.Mutex
53- listeners sync.Map // map[uint32]map[uint16][]func([]byte)
48+ conn net.Conn
49+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
50+ objects sync.Map // map[uint32]Object
51+ nextID uint32
52+ idMu sync.Mutex
53+ freeIDs []uint32
54+ retiredIDs map[uint32]bool
55+ sendMu sync.Mutex
56+ recvMu sync.Mutex
57+ dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
58+ signatures sync.Map
59+ listenerMu sync.Mutex
60+ listeners sync.Map // map[uint32]map[uint16][]func([]byte)
5461
5562 // High-performance event dispatcher
5663 dispatcher *EventDispatcher
@@ -73,7 +80,6 @@ type Display struct {
7380 readErr error
7481 maxMsgLen uint32
7582 closed atomic.Bool
76- fdMu sync.Mutex
7783 pendingFDs []int
7884 }
7985
@@ -103,11 +109,12 @@ func newDisplay(conn net.Conn) *Display {
103109
104110 // Registry represents the global registry
105111 type Registry struct {
106- id uint32
107- display *Display
108- globals map[uint32]Global
109- mu sync.RWMutex
110- handlers map[string]GlobalHandler
112+ id uint32
113+ display *Display
114+ globals map[uint32]Global
115+ mu sync.RWMutex
116+ handlers map[string]GlobalHandler
117+ removeHandlers []RegistryGlobalRemoveHandler
111118 }
112119
113120 // callbackObject represents a wl_callback object
@@ -119,6 +126,12 @@ type callbackObject struct {
119126 func (c *callbackObject) ID() uint32 {
120127 return c.id
121128 }
129+func (c *callbackObject) EventSignature(op uint16) (string, bool) {
130+ if op == 0 {
131+ return "uint,", true
132+ }
133+ return "", false
134+}
122135
123136 // Dispatch handles callback events (opcode 0 = done)
124137 func (c *callbackObject) Dispatch(event *Event) {
@@ -126,6 +139,7 @@ func (c *callbackObject) Dispatch(event *Event) {
126139 return
127140 }
128141 c.display.notifyListeners(c.id, 0, event.data)
142+ c.display.context.Unregister(c)
129143 }
130144
131145 // Global represents a global object
@@ -138,6 +152,16 @@ type Global struct {
138152 // GlobalHandler is called when a global is announced
139153 type GlobalHandler func(registry *Registry, name uint32, version uint32)
140154
155+// ConnectFromConn attaches to an established Wayland socket and requests its registry.
156+func ConnectFromConn(conn net.Conn) (*Display, error) {
157+ d := newDisplay(conn)
158+ if err := d.getRegistry(); err != nil {
159+ _ = d.Close()
160+ return nil, fmt.Errorf("get registry: %w", err)
161+ }
162+ return d, nil
163+}
164+
141165 // Connect connects to the Wayland display and requests the registry.
142166 func Connect(socketPath string) (*Display, error) {
143167 if socketPath == "" {
@@ -161,16 +185,7 @@ func Connect(socketPath string) (*Display, error) {
161185 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
162186 }
163187
164- d := newDisplay(conn)
165-
166- // Get registry
167- if err := d.getRegistry(); err != nil {
168- _ = d.Close()
169- return nil, fmt.Errorf("failed to get registry: %w", err)
170- }
171-
172- // Don't do initial roundtrip here - let the caller do it after setting up handlers
173- return d, nil
188+ return ConnectFromConn(conn)
174189 }
175190
176191 // Close closes the display connection. It is safe to call more than once;
@@ -179,8 +194,12 @@ func (d *Display) Close() error {
179194 if !d.closed.CompareAndSwap(false, true) {
180195 return nil
181196 }
197+ d.context.closed.Store(true)
198+ err := d.conn.Close()
199+ d.recvMu.Lock()
182200 d.closePendingFDs()
183- return d.conn.Close()
201+ d.recvMu.Unlock()
202+ return err
184203 }
185204
186205 // Closed reports whether the connection has been closed.
@@ -200,7 +219,20 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E
200219
201220 // allocateID allocates a new object ID
202221 func (d *Display) allocateID() uint32 {
203- return atomic.AddUint32(&d.nextID, 1) - 1
222+ d.idMu.Lock()
223+ defer d.idMu.Unlock()
224+ if n := len(d.freeIDs); n != 0 {
225+ id := d.freeIDs[n-1]
226+ d.freeIDs = d.freeIDs[:n-1]
227+ delete(d.retiredIDs, id)
228+ return id
229+ }
230+ if d.nextID == 0 || d.nextID >= 0xff000000 {
231+ return 0 // exhausted; never allocate a reserved server ID
232+ }
233+ id := d.nextID
234+ d.nextID++
235+ return id
204236 }
205237
206238 // AllocateID allocates a new object ID (public method)
@@ -215,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{
215247
216248 // SendRequestWithFDs sends a request with file descriptors
217249 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
250+ return d.sendRequest(objectID, opcode, fds, nil, args)
251+}
252+
253+// sendRequest marshals a request, then runs guard and writes the message
254+// under the send lock. Guard failures and marshaling errors write nothing.
255+func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
218256 // Get buffer from pool
219257 buf := bufferPool.Get().(*bytes.Buffer)
220258 defer func() {
@@ -235,8 +273,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
235273
236274 // Update header with actual size in bytes
237275 bufLen := buf.Len()
238- if bufLen > 0xFFFF {
239- return fmt.Errorf("message too large: %d bytes", bufLen)
276+ if bufLen > 0xFFFF || bufLen%4 != 0 {
277+ return fmt.Errorf("invalid message size: %d bytes", bufLen)
240278 }
241279 size := uint32(bufLen) // Safe: checked above
242280 binary.LittleEndian.PutUint32(header[0:4], objectID)
@@ -248,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
248286 copy(data[0:8], header)
249287
250288 // Send message with optional file descriptors
251- return d.sendmsgWithFDs(data, fds)
289+ return d.sendmsgWithFDs(data, fds, guard)
252290 }
253291
254292 // marshalArg marshals a single argument
@@ -263,7 +301,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
263301 case string:
264302 // String format: length (including null) + string + null + padding
265303 strlen := len(v) + 1
266- if strlen > 0xFFFFFFFF {
304+ if strlen > 0xffff-HeaderSize {
267305 return fmt.Errorf("string too long: %d bytes", strlen)
268306 }
269307 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
@@ -279,7 +317,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
279317 case []byte:
280318 // Array format: length + data + padding
281319 arrlen := len(v)
282- if arrlen > 0xFFFFFFFF {
320+ if arrlen > 0xffff-HeaderSize {
283321 return fmt.Errorf("array too long: %d bytes", arrlen)
284322 }
285323 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
@@ -321,14 +359,20 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
321359 // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
322360 // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.
323361 func (d *Display) Dispatch() error {
362+ d.dispatchMu.Lock()
363+ defer d.dispatchMu.Unlock()
324364 d.recvMu.Lock()
325- defer d.recvMu.Unlock()
326365
327366 if d.closed.Load() {
367+ d.recvMu.Unlock()
328368 return net.ErrClosed
329369 }
330370
331371 frame, err := d.nextFrame()
372+ d.recvMu.Unlock()
373+ if d.closed.Load() {
374+ return net.ErrClosed
375+ }
332376 if err != nil {
333377 return d.fail(err)
334378 }
@@ -342,10 +386,15 @@ func (d *Display) fail(err error) error {
342386 if err == nil {
343387 return nil
344388 }
389+ d.recvMu.Lock()
390+ defer d.recvMu.Unlock()
345391 var perr *ProtocolError
346392 if errors.As(err, &perr) {
347393 d.closePendingFDs()
348394 }
395+ if errors.Is(err, io.ErrUnexpectedEOF) {
396+ d.closePendingFDs()
397+ }
349398 return err
350399 }
351400
@@ -376,6 +425,9 @@ func (d *Display) nextFrame() (receivedFrame, error) {
376425 }
377426 }
378427 if err := d.fill(); err != nil {
428+ if d.closed.Load() {
429+ return receivedFrame{}, net.ErrClosed
430+ }
379431 return receivedFrame{}, err
380432 }
381433 }
@@ -416,40 +468,129 @@ func (d *Display) fill() error {
416468 return nil
417469 }
418470
419-// dispatchFrame delivers one complete frame to the object that owns it.
471+// signatureProvider is implemented by generated objects; every opcode must
472+// have a known signature before any descriptor can be assigned or dispatched.
473+type signatureProvider interface{ EventSignature(uint16) (string, bool) }
474+
475+// dispatchFrame delivers one complete frame with precisely its signature's FDs.
420476 func (d *Display) dispatchFrame(f receivedFrame) error {
421- if f.object == displayObjectID {
477+ d.recvMu.Lock()
478+ obj, ev, kind, err := d.prepareFrame(f)
479+ d.recvMu.Unlock()
480+ if err != nil {
481+ return err
482+ }
483+ switch kind {
484+ case 1:
422485 return d.handleDisplayEvent(f.opcode, f.body)
486+ case 2:
487+ d.notifyListeners(f.object, f.opcode, f.body)
488+ return nil
489+ }
490+ defer func() {
491+ for _, fd := range ev.fds {
492+ _ = fd.Close()
493+ }
494+ *ev = Event{}
495+ eventPool.Put(ev)
496+ }()
497+ if proxy, ok := obj.(Proxy); ok {
498+ proxy.Dispatch(ev)
499+ } else {
500+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
501+ d.notifyListeners(f.object, f.opcode, f.body)
423502 }
503+ return nil
504+}
424505
506+// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
507+func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
508+ if d.closed.Load() {
509+ return nil, nil, 0, net.ErrClosed
510+ }
511+ if f.object == displayObjectID {
512+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
513+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
514+ }
515+ return nil, nil, 1, nil
516+ }
517+ if f.object == d.registry.id {
518+ var sig string
519+ switch f.opcode {
520+ case 0:
521+ sig = "uint,string,uint,"
522+ case 1:
523+ sig = "uint,"
524+ default:
525+ return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
526+ }
527+ if err := validateEventBody(sig, f.body); err != nil {
528+ return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
529+ }
530+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
531+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
532+ }
533+ return nil, nil, 2, nil
534+ }
425535 obj, ok := d.objects.Load(f.object)
426536 if !ok {
427- return &ProtocolError{
428- Kind: "unknown_object",
429- Object: f.object,
430- Opcode: f.opcode,
431- Size: uint32(len(f.body)) + HeaderSize,
432- Err: ErrUnknownObject,
537+ return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
538+ }
539+ sig, valid := "", false
540+ p, isGenerated := obj.(signatureProvider)
541+ if isGenerated {
542+ sig, valid = p.EventSignature(f.opcode)
543+ }
544+ // Legacy custom proxies may declare their signatures with RegisterEventSignature.
545+ if !valid && !isGenerated {
546+ if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
547+ sig, valid = v.(string), true
548+ }
549+ }
550+ if !valid {
551+ return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
552+ }
553+ if err := validateEventBody(sig, f.body); err != nil {
554+ return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
555+ }
556+ count := strings.Count(sig, "fd,")
557+ if len(d.pendingFDs) < count {
558+ return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
559+ }
560+ ev := eventPool.Get().(*Event)
561+ *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
562+ if count != 0 {
563+ ev.fds = make([]*OwnedFD, count)
564+ for i := range ev.fds {
565+ ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
566+ }
567+ d.pendingFDs = d.pendingFDs[count:]
568+ }
569+ // FDs from a single recvmsg can accompany multiple coalesced frames. Once
570+ // all buffered bytes have been framed, surplus descriptors are not legal.
571+ if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 {
572+ for _, fd := range ev.fds {
573+ _ = fd.Close()
433574 }
575+ *ev = Event{}
576+ eventPool.Put(ev)
577+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
434578 }
579+ return obj.(Object), ev, 0, nil
580+}
435581
436- // Server-created objects are announced by an event on a known object.
437- d.handleServerObject(f.object, f.opcode, f.body)
582+type signatureKey struct {
583+ object uint32
584+ opcode uint16
585+}
438586
439- if proxy, ok := obj.(Proxy); ok && proxy != nil {
440- proxy.Dispatch(&Event{
441- ProxyID: f.object,
442- Opcode: f.opcode,
443- data: f.body,
444- display: d,
445- })
446- return nil
587+// RegisterEventSignature defines a signature for a legacy proxy. Generated
588+// proxies supply it themselves and cannot be overridden.
589+func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
590+ if object == 0 || object == displayObjectID {
591+ return
447592 }
448-
449- // High-performance dispatcher, then compatibility listeners.
450- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
451- d.notifyListeners(f.object, f.opcode, f.body)
452- return nil
593+ d.signatures.Store(signatureKey{object, opcode}, signature)
453594 }
454595
455596 // notifyListeners runs the listeners registered for an object and opcode.
@@ -466,11 +607,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
466607 if !ok {
467608 return
468609 }
469- handlerSlice, ok := handlers.(*[]func([]byte))
470- if !ok || handlerSlice == nil {
471- return
472- }
473- for _, handler := range *handlerSlice {
610+ for _, handler := range handlers.([]func([]byte)) {
474611 if handler != nil {
475612 handler(body)
476613 }
@@ -481,8 +618,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
481618 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
482619 switch opcode {
483620 case 0: // error
484- if len(data) < 8 {
485- return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
621+ if err := validateEventBody("object,uint,string,", data); err != nil {
622+ return &ProtocolError{Kind: "malformed_display_error", Err: err}
486623 }
487624 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
488625 objectID := event.Uint32()
@@ -495,11 +632,22 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
495632 return d.lastError
496633
497634 case 1: // delete_id
498- if len(data) < 4 {
635+ if len(data) != 4 {
499636 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
500637 }
501638 id := binary.LittleEndian.Uint32(data[0:4])
502- d.objects.Delete(id)
639+ d.idMu.Lock()
640+ _, live := d.objects.Load(id)
641+ if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
642+ d.idMu.Unlock()
643+ return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
644+ }
645+ if d.retiredIDs == nil {
646+ d.retiredIDs = make(map[uint32]bool)
647+ }
648+ d.retiredIDs[id] = true
649+ d.freeIDs = append(d.freeIDs, id)
650+ d.idMu.Unlock()
503651 return nil
504652
505653 default:
@@ -507,55 +655,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
507655 }
508656 }
509657
510-// Roundtrip performs a synchronous roundtrip to the compositor
658+// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
659+// must not call Roundtrip while another goroutine dispatches this connection.
511660 func (d *Display) Roundtrip() error {
512- callbackID := d.allocateID()
513- done := make(chan error, 1)
514-
515- d.AddListener(callbackID, 0, func(_ []byte) {
516- d.objects.Delete(callbackID)
517- done <- nil
518- })
519-
520- // Send sync request (opcode 0)
521- if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
661+ callback, err := d.Sync()
662+ if err != nil {
522663 return err
523664 }
524-
525- d.objects.Store(callbackID, &callbackObject{
526- BaseProxy: BaseProxy{
527- context: d.Context(),
528- id: callbackID,
529- },
530- display: d,
531- })
532-
533- for i := 0; i < 1000; i++ {
534- if err := d.Dispatch(); err != nil {
665+ for {
666+ if err = d.Dispatch(); err != nil {
535667 return err
536668 }
537- select {
538- case err := <-done:
539- return err
540- default:
669+ if _, exists := d.objects.Load(callback.ID()); !exists {
670+ return nil
541671 }
542672 }
543-
544- return fmt.Errorf("roundtrip failed: max iterations reached")
545673 }
546674
547675 // AddListener adds an event listener for an object
548676 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
677+ if handler == nil {
678+ return
679+ }
549680 // Load or create listener map for object
550681 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
551682 opcodeMap := listeners.(*sync.Map)
552683
553- // Load or create handlers slice for opcode
554- handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})
555- handlerSlice := handlers.(*[]func([]byte))
556-
557- // Add handler (thread-safe)
558- *handlerSlice = append(*handlerSlice, handler)
684+ d.listenerMu.Lock()
685+ defer d.listenerMu.Unlock()
686+ var handlers []func([]byte)
687+ if old, ok := opcodeMap.Load(opcode); ok {
688+ handlers = old.([]func([]byte))
689+ }
690+ next := make([]func([]byte), len(handlers)+1)
691+ copy(next, handlers)
692+ next[len(handlers)] = handler
693+ opcodeMap.Store(opcode, next)
559694 }
560695
561696 // getRegistry gets the global registry
@@ -581,15 +716,13 @@ func (r *Registry) ID() uint32 {
581716 // handleGlobal handles global announcements
582717 func (r *Registry) handleGlobal(data []byte) {
583718 if len(data) < 8 {
584- // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
585719 return
586720 }
587721
588722 name := binary.LittleEndian.Uint32(data[0:4])
589723 ifaceLen := binary.LittleEndian.Uint32(data[4:8])
590724
591- if len(data) < 8+int(ifaceLen)+4 {
592- // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
725+ if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
593726 return
594727 }
595728
@@ -600,15 +733,12 @@ func (r *Registry) handleGlobal(data []byte) {
600733 padding := (4 - (ifaceLen % 4)) % 4
601734 versionOffset := 8 + int(ifaceLen) + int(padding)
602735
603- if len(data) < versionOffset+4 {
604- // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
736+ if len(data) != versionOffset+4 {
605737 return
606738 }
607739
608740 version := binary.LittleEndian.Uint32(data[versionOffset:])
609741
610- // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
611-
612742 // Store global
613743 r.mu.Lock()
614744 r.globals[name] = Global{
@@ -619,15 +749,19 @@ func (r *Registry) handleGlobal(data []byte) {
619749 r.mu.Unlock()
620750
621751 // Call specific handler if registered
622- if handler, ok := r.handlers[iface]; ok {
623- // log.Printf("wlclient: calling handler for interface %s", iface)
752+ r.mu.RLock()
753+ handler, specific := r.handlers[iface]
754+ wildcard, all := r.handlers["*"]
755+ r.mu.RUnlock()
756+ if specific {
757+
624758 handler(r, name, version)
625759 }
626760
627761 // Call wildcard handler if registered
628- if handler, ok := r.handlers["*"]; ok {
629- // log.Printf("wlclient: calling wildcard handler for interface %s", iface)
630- handler(r, name, version)
762+ if all {
763+
764+ wildcard(r, name, version)
631765 }
632766 }
633767
@@ -641,11 +775,17 @@ func (r *Registry) handleGlobalRemove(data []byte) {
641775
642776 r.mu.Lock()
643777 delete(r.globals, name)
778+ handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
644779 r.mu.Unlock()
780+ for _, handler := range handlers {
781+ handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
782+ }
645783 }
646784
647785 // AddHandler adds a handler for a specific interface
648786 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
787+ r.mu.Lock()
788+ defer r.mu.Unlock()
649789 r.handlers[iface] = handler
650790 }
651791
@@ -656,7 +796,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
656796 proxy.SetID(r.display.allocateID())
657797 }
658798
659- // Ensure proxy has a context set - CRITICAL FIX
799+ // Bootstrap a generated proxy with this display's context.
660800 if proxy.Context() == nil {
661801 if baseProxy, ok := proxy.(*BaseProxy); ok {
662802 baseProxy.SetContext(r.display.Context())
@@ -667,12 +807,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
667807 }
668808 }
669809
810+ if proxy.Context() != r.display.Context() {
811+ return fmt.Errorf("proxy belongs to another display")
812+ }
813+ if proxy.ID() == displayObjectID || proxy.ID() == r.id {
814+ return fmt.Errorf("reserved object ID")
815+ }
816+ if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
817+ return fmt.Errorf("object ID %d already registered", proxy.ID())
818+ }
670819 // Register the proxy
671820 proxy.Context().Register(proxy)
672821
673- // Also register directly in display objects map
674- r.display.objects.Store(proxy.ID(), proxy)
675-
676822 // Send bind request (opcode 0) with proper arguments
677823 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
678824 proxy.Context().Unregister(proxy)
@@ -718,118 +864,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) {
718864 }
719865 return Global{}, false
720866 }
721-
722-// handleServerObject creates and registers server-allocated objects from events
723-func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool {
724- // Handle specific known protocols that create server objects
725- obj, ok := d.objects.Load(objectID)
726- if !ok {
727- return false
728- }
729-
730- // Check if this is a known object type that creates server objects
731- switch obj.(type) {
732- case *Registry:
733- // Registry doesn't create server objects directly
734- return false
735-
736- default:
737- // Check if this might be an output manager (interface zwlr_output_manager_v1)
738- if objectID == 5 { // This is likely the output manager based on your logs
739- return d.handleOutputManagerEvent(objectID, opcode, body)
740- }
741- }
742-
743- return false
744-}
745-
746-// handleOutputManagerEvent handles events from zwlr_output_manager_v1
747-func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool {
748- switch opcode {
749- case 0: // head event - creates new zwlr_output_head_v1 object
750- if len(body) < 4 {
751- return false
752- }
753-
754- // Parse the new_id for the head object
755- headID := binary.LittleEndian.Uint32(body[0:4])
756-
757- // Create a proper OutputHead object
758- headProxy := &OutputHead{
759- BaseProxy: BaseProxy{
760- id: headID,
761- context: d.Context(),
762- },
763- }
764-
765- // Register the new head object
766- d.objects.Store(headID, headProxy)
767- return true
768-
769- case 1: // done event
770- return false
771-
772- case 2: // finished event
773- return false
774- }
775-
776- return false
777-}
778-
779-// OutputHead represents a zwlr_output_head_v1 object
780-type OutputHead struct {
781- BaseProxy
782- name string
783- description string
784- width int32
785- height int32
786-}
787-
788-// Dispatch handles head events
789-func (h *OutputHead) Dispatch(event *Event) {
790- switch event.Opcode {
791- case 0: // name
792- h.name = event.String()
793- case 1: // description
794- h.description = event.String()
795- case 2: // physical_size
796- h.width = event.Int32()
797- h.height = event.Int32()
798- case 3: // mode (creates new mode object)
799- modeID := event.Uint32()
800- // Create mode object
801- mode := &OutputMode{
802- BaseProxy: BaseProxy{
803- id: modeID,
804- context: h.context,
805- },
806- }
807- h.context.display.objects.Store(modeID, mode)
808- case 9: // finished
809- h.context.Unregister(h)
810- default:
811- }
812-}
813-
814-// OutputMode represents a zwlr_output_mode_v1 object
815-type OutputMode struct {
816- BaseProxy
817- width int32
818- height int32
819- refresh int32
820-}
821-
822-// Dispatch handles mode events
823-func (m *OutputMode) Dispatch(event *Event) {
824- switch event.Opcode {
825- case 0: // size
826- m.width = event.Int32()
827- m.height = event.Int32()
828- case 1: // refresh
829- m.refresh = event.Int32()
830- case 2: // preferred
831- case 3: // finished
832- m.context.Unregister(m)
833- default:
834- }
835-}
@@ -13,6 +13,7 @@ import (
13 "net"13 "net"
14 "os"14 "os"
15 "path/filepath"15 "path/filepath"
16+ "strings"
16 "sync"17 "sync"
17 "sync/atomic"18 "sync/atomic"
18 )19 )
@@ -44,13 +45,19 @@ type Object interface {
44 45
45 // Display represents a connection to the Wayland display46 // Display represents a connection to the Wayland display
46 type Display struct {47 type Display struct {
47- conn net.Conn48+ conn net.Conn
48- unix *net.UnixConn // set when conn is a Unix socket, nil otherwise49+ unix *net.UnixConn // set when conn is a Unix socket, nil otherwise
49- objects sync.Map // map[uint32]Object50+ objects sync.Map // map[uint32]Object
50- nextID uint3251+ nextID uint32
51- sendMu sync.Mutex52+ idMu sync.Mutex
52- recvMu sync.Mutex53+ freeIDs []uint32
53- listeners sync.Map // map[uint32]map[uint16][]func([]byte)54+ retiredIDs map[uint32]bool
55+ sendMu sync.Mutex
56+ recvMu sync.Mutex
57+ dispatchMu sync.Mutex // serialize readers without holding receive state across handlers
58+ signatures sync.Map
59+ listenerMu sync.Mutex
60+ listeners sync.Map // map[uint32]map[uint16][]func([]byte)
54 61
55 // High-performance event dispatcher62 // High-performance event dispatcher
56 dispatcher *EventDispatcher63 dispatcher *EventDispatcher
@@ -73,7 +80,6 @@ type Display struct {
73 readErr error80 readErr error
74 maxMsgLen uint3281 maxMsgLen uint32
75 closed atomic.Bool82 closed atomic.Bool
76- fdMu sync.Mutex
77 pendingFDs []int83 pendingFDs []int
78 }84 }
79 85
@@ -103,11 +109,12 @@ func newDisplay(conn net.Conn) *Display {
103 109
104 // Registry represents the global registry110 // Registry represents the global registry
105 type Registry struct {111 type Registry struct {
106- id uint32112+ id uint32
107- display *Display113+ display *Display
108- globals map[uint32]Global114+ globals map[uint32]Global
109- mu sync.RWMutex115+ mu sync.RWMutex
110- handlers map[string]GlobalHandler116+ handlers map[string]GlobalHandler
117+ removeHandlers []RegistryGlobalRemoveHandler
111 }118 }
112 119
113 // callbackObject represents a wl_callback object120 // callbackObject represents a wl_callback object
@@ -119,6 +126,12 @@ type callbackObject struct {
119 func (c *callbackObject) ID() uint32 {126 func (c *callbackObject) ID() uint32 {
120 return c.id127 return c.id
121 }128 }
129+func (c *callbackObject) EventSignature(op uint16) (string, bool) {
130+ if op == 0 {
131+ return "uint,", true
132+ }
133+ return "", false
134+}
122 135
123 // Dispatch handles callback events (opcode 0 = done)136 // Dispatch handles callback events (opcode 0 = done)
124 func (c *callbackObject) Dispatch(event *Event) {137 func (c *callbackObject) Dispatch(event *Event) {
@@ -126,6 +139,7 @@ func (c *callbackObject) Dispatch(event *Event) {
126 return139 return
127 }140 }
128 c.display.notifyListeners(c.id, 0, event.data)141 c.display.notifyListeners(c.id, 0, event.data)
142+ c.display.context.Unregister(c)
129 }143 }
130 144
131 // Global represents a global object145 // Global represents a global object
@@ -138,6 +152,16 @@ type Global struct {
138 // GlobalHandler is called when a global is announced152 // GlobalHandler is called when a global is announced
139 type GlobalHandler func(registry *Registry, name uint32, version uint32)153 type GlobalHandler func(registry *Registry, name uint32, version uint32)
140 154
155+// ConnectFromConn attaches to an established Wayland socket and requests its registry.
156+func ConnectFromConn(conn net.Conn) (*Display, error) {
157+ d := newDisplay(conn)
158+ if err := d.getRegistry(); err != nil {
159+ _ = d.Close()
160+ return nil, fmt.Errorf("get registry: %w", err)
161+ }
162+ return d, nil
163+}
164+
141 // Connect connects to the Wayland display and requests the registry.165 // Connect connects to the Wayland display and requests the registry.
142 func Connect(socketPath string) (*Display, error) {166 func Connect(socketPath string) (*Display, error) {
143 if socketPath == "" {167 if socketPath == "" {
@@ -161,16 +185,7 @@ func Connect(socketPath string) (*Display, error) {
161 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)185 return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
162 }186 }
163 187
164- d := newDisplay(conn)188+ return ConnectFromConn(conn)
165-
166- // Get registry
167- if err := d.getRegistry(); err != nil {
168- _ = d.Close()
169- return nil, fmt.Errorf("failed to get registry: %w", err)
170- }
171-
172- // Don't do initial roundtrip here - let the caller do it after setting up handlers
173- return d, nil
174 }189 }
175 190
176 // Close closes the display connection. It is safe to call more than once;191 // Close closes the display connection. It is safe to call more than once;
@@ -179,8 +194,12 @@ func (d *Display) Close() error {
179 if !d.closed.CompareAndSwap(false, true) {194 if !d.closed.CompareAndSwap(false, true) {
180 return nil195 return nil
181 }196 }
197+ d.context.closed.Store(true)
198+ err := d.conn.Close()
199+ d.recvMu.Lock()
182 d.closePendingFDs()200 d.closePendingFDs()
183- return d.conn.Close()201+ d.recvMu.Unlock()
202+ return err
184 }203 }
185 204
186 // Closed reports whether the connection has been closed.205 // Closed reports whether the connection has been closed.
@@ -200,7 +219,20 @@ func (d *Display) RegisterEventHandler(objectID uint32, opcode uint16, handler E
200 219
201 // allocateID allocates a new object ID220 // allocateID allocates a new object ID
202 func (d *Display) allocateID() uint32 {221 func (d *Display) allocateID() uint32 {
203- return atomic.AddUint32(&d.nextID, 1) - 1222+ d.idMu.Lock()
223+ defer d.idMu.Unlock()
224+ if n := len(d.freeIDs); n != 0 {
225+ id := d.freeIDs[n-1]
226+ d.freeIDs = d.freeIDs[:n-1]
227+ delete(d.retiredIDs, id)
228+ return id
229+ }
230+ if d.nextID == 0 || d.nextID >= 0xff000000 {
231+ return 0 // exhausted; never allocate a reserved server ID
232+ }
233+ id := d.nextID
234+ d.nextID++
235+ return id
204 }236 }
205 237
206 // AllocateID allocates a new object ID (public method)238 // AllocateID allocates a new object ID (public method)
@@ -215,6 +247,12 @@ func (d *Display) SendRequest(objectID uint32, opcode uint16, args ...interface{
215 247
216 // SendRequestWithFDs sends a request with file descriptors248 // SendRequestWithFDs sends a request with file descriptors
217 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {249 func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int, args ...interface{}) error {
250+ return d.sendRequest(objectID, opcode, fds, nil, args)
251+}
252+
253+// sendRequest marshals a request, then runs guard and writes the message
254+// under the send lock. Guard failures and marshaling errors write nothing.
255+func (d *Display) sendRequest(objectID uint32, opcode uint16, fds []int, guard func() error, args []interface{}) error {
218 // Get buffer from pool256 // Get buffer from pool
219 buf := bufferPool.Get().(*bytes.Buffer)257 buf := bufferPool.Get().(*bytes.Buffer)
220 defer func() {258 defer func() {
@@ -235,8 +273,8 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
235 273
236 // Update header with actual size in bytes274 // Update header with actual size in bytes
237 bufLen := buf.Len()275 bufLen := buf.Len()
238- if bufLen > 0xFFFF {276+ if bufLen > 0xFFFF || bufLen%4 != 0 {
239- return fmt.Errorf("message too large: %d bytes", bufLen)277+ return fmt.Errorf("invalid message size: %d bytes", bufLen)
240 }278 }
241 size := uint32(bufLen) // Safe: checked above279 size := uint32(bufLen) // Safe: checked above
242 binary.LittleEndian.PutUint32(header[0:4], objectID)280 binary.LittleEndian.PutUint32(header[0:4], objectID)
@@ -248,7 +286,7 @@ func (d *Display) SendRequestWithFDs(objectID uint32, opcode uint16, fds []int,
248 copy(data[0:8], header)286 copy(data[0:8], header)
249 287
250 // Send message with optional file descriptors288 // Send message with optional file descriptors
251- return d.sendmsgWithFDs(data, fds)289+ return d.sendmsgWithFDs(data, fds, guard)
252 }290 }
253 291
254 // marshalArg marshals a single argument292 // marshalArg marshals a single argument
@@ -263,7 +301,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
263 case string:301 case string:
264 // String format: length (including null) + string + null + padding302 // String format: length (including null) + string + null + padding
265 strlen := len(v) + 1303 strlen := len(v) + 1
266- if strlen > 0xFFFFFFFF {304+ if strlen > 0xffff-HeaderSize {
267 return fmt.Errorf("string too long: %d bytes", strlen)305 return fmt.Errorf("string too long: %d bytes", strlen)
268 }306 }
269 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above307 if err := binary.Write(buf, binary.LittleEndian, uint32(strlen)); err != nil { // Safe: checked above
@@ -279,7 +317,7 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
279 case []byte:317 case []byte:
280 // Array format: length + data + padding318 // Array format: length + data + padding
281 arrlen := len(v)319 arrlen := len(v)
282- if arrlen > 0xFFFFFFFF {320+ if arrlen > 0xffff-HeaderSize {
283 return fmt.Errorf("array too long: %d bytes", arrlen)321 return fmt.Errorf("array too long: %d bytes", arrlen)
284 }322 }
285 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {323 if err := binary.Write(buf, binary.LittleEndian, uint32(arrlen)); err != nil {
@@ -321,14 +359,20 @@ func (d *Display) marshalArg(buf *bytes.Buffer, arg interface{}) error {
321 // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a359 // Dispatch returns net.ErrClosed after Close, a typed ProtocolError for a
322 // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.360 // malformed frame, and io.ErrUnexpectedEOF when the peer closes mid-frame.
323 func (d *Display) Dispatch() error {361 func (d *Display) Dispatch() error {
362+ d.dispatchMu.Lock()
363+ defer d.dispatchMu.Unlock()
324 d.recvMu.Lock()364 d.recvMu.Lock()
325- defer d.recvMu.Unlock()
326 365
327 if d.closed.Load() {366 if d.closed.Load() {
367+ d.recvMu.Unlock()
328 return net.ErrClosed368 return net.ErrClosed
329 }369 }
330 370
331 frame, err := d.nextFrame()371 frame, err := d.nextFrame()
372+ d.recvMu.Unlock()
373+ if d.closed.Load() {
374+ return net.ErrClosed
375+ }
332 if err != nil {376 if err != nil {
333 return d.fail(err)377 return d.fail(err)
334 }378 }
@@ -342,10 +386,15 @@ func (d *Display) fail(err error) error {
342 if err == nil {386 if err == nil {
343 return nil387 return nil
344 }388 }
389+ d.recvMu.Lock()
390+ defer d.recvMu.Unlock()
345 var perr *ProtocolError391 var perr *ProtocolError
346 if errors.As(err, &perr) {392 if errors.As(err, &perr) {
347 d.closePendingFDs()393 d.closePendingFDs()
348 }394 }
395+ if errors.Is(err, io.ErrUnexpectedEOF) {
396+ d.closePendingFDs()
397+ }
349 return err398 return err
350 }399 }
351 400
@@ -376,6 +425,9 @@ func (d *Display) nextFrame() (receivedFrame, error) {
376 }425 }
377 }426 }
378 if err := d.fill(); err != nil {427 if err := d.fill(); err != nil {
428+ if d.closed.Load() {
429+ return receivedFrame{}, net.ErrClosed
430+ }
379 return receivedFrame{}, err431 return receivedFrame{}, err
380 }432 }
381 }433 }
@@ -416,40 +468,129 @@ func (d *Display) fill() error {
416 return nil468 return nil
417 }469 }
418 470
419-// dispatchFrame delivers one complete frame to the object that owns it.471+// signatureProvider is implemented by generated objects; every opcode must
472+// have a known signature before any descriptor can be assigned or dispatched.
473+type signatureProvider interface{ EventSignature(uint16) (string, bool) }
474+
475+// dispatchFrame delivers one complete frame with precisely its signature's FDs.
420 func (d *Display) dispatchFrame(f receivedFrame) error {476 func (d *Display) dispatchFrame(f receivedFrame) error {
421- if f.object == displayObjectID {477+ d.recvMu.Lock()
478+ obj, ev, kind, err := d.prepareFrame(f)
479+ d.recvMu.Unlock()
480+ if err != nil {
481+ return err
482+ }
483+ switch kind {
484+ case 1:
422 return d.handleDisplayEvent(f.opcode, f.body)485 return d.handleDisplayEvent(f.opcode, f.body)
486+ case 2:
487+ d.notifyListeners(f.object, f.opcode, f.body)
488+ return nil
489+ }
490+ defer func() {
491+ for _, fd := range ev.fds {
492+ _ = fd.Close()
493+ }
494+ *ev = Event{}
495+ eventPool.Put(ev)
496+ }()
497+ if proxy, ok := obj.(Proxy); ok {
498+ proxy.Dispatch(ev)
499+ } else {
500+ d.dispatcher.Dispatch(f.object, f.opcode, f.body)
501+ d.notifyListeners(f.object, f.opcode, f.body)
423 }502 }
503+ return nil
504+}
424 505
506+// prepareFrame assigns owned descriptors under recvMu, but never calls user code.
507+func (d *Display) prepareFrame(f receivedFrame) (Object, *Event, uint8, error) {
508+ if d.closed.Load() {
509+ return nil, nil, 0, net.ErrClosed
510+ }
511+ if f.object == displayObjectID {
512+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
513+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Err: ErrMalformedFrame}
514+ }
515+ return nil, nil, 1, nil
516+ }
517+ if f.object == d.registry.id {
518+ var sig string
519+ switch f.opcode {
520+ case 0:
521+ sig = "uint,string,uint,"
522+ case 1:
523+ sig = "uint,"
524+ default:
525+ return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
526+ }
527+ if err := validateEventBody(sig, f.body); err != nil {
528+ return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
529+ }
530+ if len(d.pendingFDs) != 0 && len(d.rbuf) == 0 {
531+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
532+ }
533+ return nil, nil, 2, nil
534+ }
425 obj, ok := d.objects.Load(f.object)535 obj, ok := d.objects.Load(f.object)
426 if !ok {536 if !ok {
427- return &ProtocolError{537+ return nil, nil, 0, &ProtocolError{Kind: "unknown_object", Object: f.object, Opcode: f.opcode, Err: ErrUnknownObject}
428- Kind: "unknown_object",538+ }
429- Object: f.object,539+ sig, valid := "", false
430- Opcode: f.opcode,540+ p, isGenerated := obj.(signatureProvider)
431- Size: uint32(len(f.body)) + HeaderSize,541+ if isGenerated {
432- Err: ErrUnknownObject,542+ sig, valid = p.EventSignature(f.opcode)
543+ }
544+ // Legacy custom proxies may declare their signatures with RegisterEventSignature.
545+ if !valid && !isGenerated {
546+ if v, ok := d.signatures.Load(signatureKey{f.object, f.opcode}); ok {
547+ sig, valid = v.(string), true
548+ }
549+ }
550+ if !valid {
551+ return nil, nil, 0, &ProtocolError{Kind: "unknown_opcode", Object: f.object, Opcode: f.opcode, Err: ErrUnknownOpcode}
552+ }
553+ if err := validateEventBody(sig, f.body); err != nil {
554+ return nil, nil, 0, &ProtocolError{Kind: "malformed_payload", Object: f.object, Opcode: f.opcode, Err: err}
555+ }
556+ count := strings.Count(sig, "fd,")
557+ if len(d.pendingFDs) < count {
558+ return nil, nil, 0, &ProtocolError{Kind: "missing_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
559+ }
560+ ev := eventPool.Get().(*Event)
561+ *ev = Event{ProxyID: f.object, Opcode: f.opcode, data: f.body, display: d}
562+ if count != 0 {
563+ ev.fds = make([]*OwnedFD, count)
564+ for i := range ev.fds {
565+ ev.fds[i] = &OwnedFD{fd: d.pendingFDs[i]}
566+ }
567+ d.pendingFDs = d.pendingFDs[count:]
568+ }
569+ // FDs from a single recvmsg can accompany multiple coalesced frames. Once
570+ // all buffered bytes have been framed, surplus descriptors are not legal.
571+ if len(d.rbuf) == 0 && len(d.pendingFDs) != 0 {
572+ for _, fd := range ev.fds {
573+ _ = fd.Close()
433 }574 }
575+ *ev = Event{}
576+ eventPool.Put(ev)
577+ return nil, nil, 0, &ProtocolError{Kind: "extra_fd", Object: f.object, Opcode: f.opcode, Err: ErrMalformedFrame}
434 }578 }
579+ return obj.(Object), ev, 0, nil
580+}
435 581
436- // Server-created objects are announced by an event on a known object.582+type signatureKey struct {
437- d.handleServerObject(f.object, f.opcode, f.body)583+ object uint32
584+ opcode uint16
585+}
438 586
439- if proxy, ok := obj.(Proxy); ok && proxy != nil {587+// RegisterEventSignature defines a signature for a legacy proxy. Generated
440- proxy.Dispatch(&Event{588+// proxies supply it themselves and cannot be overridden.
441- ProxyID: f.object,589+func (d *Display) RegisterEventSignature(object uint32, opcode uint16, signature string) {
442- Opcode: f.opcode,590+ if object == 0 || object == displayObjectID {
443- data: f.body,591+ return
444- display: d,
445- })
446- return nil
447 }592 }
448-593+ d.signatures.Store(signatureKey{object, opcode}, signature)
449- // High-performance dispatcher, then compatibility listeners.
450- d.dispatcher.Dispatch(f.object, f.opcode, f.body)
451- d.notifyListeners(f.object, f.opcode, f.body)
452- return nil
453 }594 }
454 595
455 // notifyListeners runs the listeners registered for an object and opcode.596 // notifyListeners runs the listeners registered for an object and opcode.
@@ -466,11 +607,7 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
466 if !ok {607 if !ok {
467 return608 return
468 }609 }
469- handlerSlice, ok := handlers.(*[]func([]byte))610+ for _, handler := range handlers.([]func([]byte)) {
470- if !ok || handlerSlice == nil {
471- return
472- }
473- for _, handler := range *handlerSlice {
474 if handler != nil {611 if handler != nil {
475 handler(body)612 handler(body)
476 }613 }
@@ -481,8 +618,8 @@ func (d *Display) notifyListeners(objectID uint32, opcode uint16, body []byte) {
481 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {618 func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
482 switch opcode {619 switch opcode {
483 case 0: // error620 case 0: // error
484- if len(data) < 8 {621+ if err := validateEventBody("object,uint,string,", data); err != nil {
485- return &ProtocolError{Kind: "short_display_error", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}622+ return &ProtocolError{Kind: "malformed_display_error", Err: err}
486 }623 }
487 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}624 event := &Event{ProxyID: displayObjectID, Opcode: opcode, data: data, display: d}
488 objectID := event.Uint32()625 objectID := event.Uint32()
@@ -495,11 +632,22 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
495 return d.lastError632 return d.lastError
496 633
497 case 1: // delete_id634 case 1: // delete_id
498- if len(data) < 4 {635+ if len(data) != 4 {
499 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}636 return &ProtocolError{Kind: "short_delete_id", Object: displayObjectID, Opcode: opcode, Size: uint32(len(data)) + HeaderSize, Err: ErrMalformedFrame}
500 }637 }
501 id := binary.LittleEndian.Uint32(data[0:4])638 id := binary.LittleEndian.Uint32(data[0:4])
502- d.objects.Delete(id)639+ d.idMu.Lock()
640+ _, live := d.objects.Load(id)
641+ if id < 2 || id >= 0xff000000 || id >= d.nextID || live || d.retiredIDs[id] {
642+ d.idMu.Unlock()
643+ return &ProtocolError{Kind: "invalid_delete_id", Object: id, Err: ErrMalformedFrame}
644+ }
645+ if d.retiredIDs == nil {
646+ d.retiredIDs = make(map[uint32]bool)
647+ }
648+ d.retiredIDs[id] = true
649+ d.freeIDs = append(d.freeIDs, id)
650+ d.idMu.Unlock()
503 return nil651 return nil
504 652
505 default:653 default:
@@ -507,55 +655,42 @@ func (d *Display) handleDisplayEvent(opcode uint16, data []byte) error {
507 }655 }
508 }656 }
509 657
510-// Roundtrip performs a synchronous roundtrip to the compositor658+// Roundtrip waits for one wl_display.sync callback. Concurrent event loops
659+// must not call Roundtrip while another goroutine dispatches this connection.
511 func (d *Display) Roundtrip() error {660 func (d *Display) Roundtrip() error {
512- callbackID := d.allocateID()661+ callback, err := d.Sync()
513- done := make(chan error, 1)662+ if err != nil {
514-
515- d.AddListener(callbackID, 0, func(_ []byte) {
516- d.objects.Delete(callbackID)
517- done <- nil
518- })
519-
520- // Send sync request (opcode 0)
521- if err := d.SendRequest(displayObjectID, 0, callbackID); err != nil {
522 return err663 return err
523 }664 }
524-665+ for {
525- d.objects.Store(callbackID, &callbackObject{666+ if err = d.Dispatch(); err != nil {
526- BaseProxy: BaseProxy{
527- context: d.Context(),
528- id: callbackID,
529- },
530- display: d,
531- })
532-
533- for i := 0; i < 1000; i++ {
534- if err := d.Dispatch(); err != nil {
535 return err667 return err
536 }668 }
537- select {669+ if _, exists := d.objects.Load(callback.ID()); !exists {
538- case err := <-done:670+ return nil
539- return err
540- default:
541 }671 }
542 }672 }
543-
544- return fmt.Errorf("roundtrip failed: max iterations reached")
545 }673 }
546 674
547 // AddListener adds an event listener for an object675 // AddListener adds an event listener for an object
548 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {676 func (d *Display) AddListener(objectID uint32, opcode uint16, handler func([]byte)) {
677+ if handler == nil {
678+ return
679+ }
549 // Load or create listener map for object680 // Load or create listener map for object
550 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})681 listeners, _ := d.listeners.LoadOrStore(objectID, &sync.Map{})
551 opcodeMap := listeners.(*sync.Map)682 opcodeMap := listeners.(*sync.Map)
552 683
553- // Load or create handlers slice for opcode684+ d.listenerMu.Lock()
554- handlers, _ := opcodeMap.LoadOrStore(opcode, &[]func([]byte){})685+ defer d.listenerMu.Unlock()
555- handlerSlice := handlers.(*[]func([]byte))686+ var handlers []func([]byte)
556-687+ if old, ok := opcodeMap.Load(opcode); ok {
557- // Add handler (thread-safe)688+ handlers = old.([]func([]byte))
558- *handlerSlice = append(*handlerSlice, handler)689+ }
690+ next := make([]func([]byte), len(handlers)+1)
691+ copy(next, handlers)
692+ next[len(handlers)] = handler
693+ opcodeMap.Store(opcode, next)
559 }694 }
560 695
561 // getRegistry gets the global registry696 // getRegistry gets the global registry
@@ -581,15 +716,13 @@ func (r *Registry) ID() uint32 {
581 // handleGlobal handles global announcements716 // handleGlobal handles global announcements
582 func (r *Registry) handleGlobal(data []byte) {717 func (r *Registry) handleGlobal(data []byte) {
583 if len(data) < 8 {718 if len(data) < 8 {
584- // log.Printf("wlclient: handleGlobal: data too short (%d bytes)", len(data))
585 return719 return
586 }720 }
587 721
588 name := binary.LittleEndian.Uint32(data[0:4])722 name := binary.LittleEndian.Uint32(data[0:4])
589 ifaceLen := binary.LittleEndian.Uint32(data[4:8])723 ifaceLen := binary.LittleEndian.Uint32(data[4:8])
590 724
591- if len(data) < 8+int(ifaceLen)+4 {725+ if ifaceLen == 0 || uint64(ifaceLen)+12 > uint64(len(data)) || data[8+ifaceLen-1] != 0 {
592- // log.Printf("wlclient: handleGlobal: insufficient data for interface string (need %d, have %d)", 8+int(ifaceLen)+4, len(data))
593 return726 return
594 }727 }
595 728
@@ -600,15 +733,12 @@ func (r *Registry) handleGlobal(data []byte) {
600 padding := (4 - (ifaceLen % 4)) % 4733 padding := (4 - (ifaceLen % 4)) % 4
601 versionOffset := 8 + int(ifaceLen) + int(padding)734 versionOffset := 8 + int(ifaceLen) + int(padding)
602 735
603- if len(data) < versionOffset+4 {736+ if len(data) != versionOffset+4 {
604- // log.Printf("wlclient: handleGlobal: insufficient data for version (need %d, have %d)", versionOffset+4, len(data))
605 return737 return
606 }738 }
607 739
608 version := binary.LittleEndian.Uint32(data[versionOffset:])740 version := binary.LittleEndian.Uint32(data[versionOffset:])
609 741
610- // log.Printf("wlclient: global announced: %s v%d (name=%d)", iface, version, name)
611-
612 // Store global742 // Store global
613 r.mu.Lock()743 r.mu.Lock()
614 r.globals[name] = Global{744 r.globals[name] = Global{
@@ -619,15 +749,19 @@ func (r *Registry) handleGlobal(data []byte) {
619 r.mu.Unlock()749 r.mu.Unlock()
620 750
621 // Call specific handler if registered751 // Call specific handler if registered
622- if handler, ok := r.handlers[iface]; ok {752+ r.mu.RLock()
623- // log.Printf("wlclient: calling handler for interface %s", iface)753+ handler, specific := r.handlers[iface]
754+ wildcard, all := r.handlers["*"]
755+ r.mu.RUnlock()
756+ if specific {
757+
624 handler(r, name, version)758 handler(r, name, version)
625 }759 }
626 760
627 // Call wildcard handler if registered761 // Call wildcard handler if registered
628- if handler, ok := r.handlers["*"]; ok {762+ if all {
629- // log.Printf("wlclient: calling wildcard handler for interface %s", iface)763+
630- handler(r, name, version)764+ wildcard(r, name, version)
631 }765 }
632 }766 }
633 767
@@ -641,11 +775,17 @@ func (r *Registry) handleGlobalRemove(data []byte) {
641 775
642 r.mu.Lock()776 r.mu.Lock()
643 delete(r.globals, name)777 delete(r.globals, name)
778+ handlers := append([]RegistryGlobalRemoveHandler(nil), r.removeHandlers...)
644 r.mu.Unlock()779 r.mu.Unlock()
780+ for _, handler := range handlers {
781+ handler.HandleRegistryGlobalRemove(RegistryGlobalRemoveEvent{Registry: r, Name: name})
782+ }
645 }783 }
646 784
647 // AddHandler adds a handler for a specific interface785 // AddHandler adds a handler for a specific interface
648 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {786 func (r *Registry) AddHandler(iface string, handler GlobalHandler) {
787+ r.mu.Lock()
788+ defer r.mu.Unlock()
649 r.handlers[iface] = handler789 r.handlers[iface] = handler
650 }790 }
651 791
@@ -656,7 +796,7 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
656 proxy.SetID(r.display.allocateID())796 proxy.SetID(r.display.allocateID())
657 }797 }
658 798
659- // Ensure proxy has a context set - CRITICAL FIX799+ // Bootstrap a generated proxy with this display's context.
660 if proxy.Context() == nil {800 if proxy.Context() == nil {
661 if baseProxy, ok := proxy.(*BaseProxy); ok {801 if baseProxy, ok := proxy.(*BaseProxy); ok {
662 baseProxy.SetContext(r.display.Context())802 baseProxy.SetContext(r.display.Context())
@@ -667,12 +807,18 @@ func (r *Registry) Bind(name uint32, iface string, version uint32, proxy Proxy)
667 }807 }
668 }808 }
669 809
810+ if proxy.Context() != r.display.Context() {
811+ return fmt.Errorf("proxy belongs to another display")
812+ }
813+ if proxy.ID() == displayObjectID || proxy.ID() == r.id {
814+ return fmt.Errorf("reserved object ID")
815+ }
816+ if old, exists := r.display.objects.Load(proxy.ID()); exists && old != proxy {
817+ return fmt.Errorf("object ID %d already registered", proxy.ID())
818+ }
670 // Register the proxy819 // Register the proxy
671 proxy.Context().Register(proxy)820 proxy.Context().Register(proxy)
672 821
673- // Also register directly in display objects map
674- r.display.objects.Store(proxy.ID(), proxy)
675-
676 // Send bind request (opcode 0) with proper arguments822 // Send bind request (opcode 0) with proper arguments
677 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {823 if err := r.display.SendRequest(r.id, 0, name, iface, version, proxy.ID()); err != nil {
678 proxy.Context().Unregister(proxy)824 proxy.Context().Unregister(proxy)
@@ -718,118 +864,3 @@ func (r *Registry) FindGlobal(iface string) (Global, bool) {
718 }864 }
719 return Global{}, false865 return Global{}, false
720 }866 }
721-
722-// handleServerObject creates and registers server-allocated objects from events
723-func (d *Display) handleServerObject(objectID uint32, opcode uint16, body []byte) bool {
724- // Handle specific known protocols that create server objects
725- obj, ok := d.objects.Load(objectID)
726- if !ok {
727- return false
728- }
729-
730- // Check if this is a known object type that creates server objects
731- switch obj.(type) {
732- case *Registry:
733- // Registry doesn't create server objects directly
734- return false
735-
736- default:
737- // Check if this might be an output manager (interface zwlr_output_manager_v1)
738- if objectID == 5 { // This is likely the output manager based on your logs
739- return d.handleOutputManagerEvent(objectID, opcode, body)
740- }
741- }
742-
743- return false
744-}
745-
746-// handleOutputManagerEvent handles events from zwlr_output_manager_v1
747-func (d *Display) handleOutputManagerEvent(objectID uint32, opcode uint16, body []byte) bool {
748- switch opcode {
749- case 0: // head event - creates new zwlr_output_head_v1 object
750- if len(body) < 4 {
751- return false
752- }
753-
754- // Parse the new_id for the head object
755- headID := binary.LittleEndian.Uint32(body[0:4])
756-
757- // Create a proper OutputHead object
758- headProxy := &OutputHead{
759- BaseProxy: BaseProxy{
760- id: headID,
761- context: d.Context(),
762- },
763- }
764-
765- // Register the new head object
766- d.objects.Store(headID, headProxy)
767- return true
768-
769- case 1: // done event
770- return false
771-
772- case 2: // finished event
773- return false
774- }
775-
776- return false
777-}
778-
779-// OutputHead represents a zwlr_output_head_v1 object
780-type OutputHead struct {
781- BaseProxy
782- name string
783- description string
784- width int32
785- height int32
786-}
787-
788-// Dispatch handles head events
789-func (h *OutputHead) Dispatch(event *Event) {
790- switch event.Opcode {
791- case 0: // name
792- h.name = event.String()
793- case 1: // description
794- h.description = event.String()
795- case 2: // physical_size
796- h.width = event.Int32()
797- h.height = event.Int32()
798- case 3: // mode (creates new mode object)
799- modeID := event.Uint32()
800- // Create mode object
801- mode := &OutputMode{
802- BaseProxy: BaseProxy{
803- id: modeID,
804- context: h.context,
805- },
806- }
807- h.context.display.objects.Store(modeID, mode)
808- case 9: // finished
809- h.context.Unregister(h)
810- default:
811- }
812-}
813-
814-// OutputMode represents a zwlr_output_mode_v1 object
815-type OutputMode struct {
816- BaseProxy
817- width int32
818- height int32
819- refresh int32
820-}
821-
822-// Dispatch handles mode events
823-func (m *OutputMode) Dispatch(event *Event) {
824- switch event.Opcode {
825- case 0: // size
826- m.width = event.Int32()
827- m.height = event.Int32()
828- case 1: // refresh
829- m.refresh = event.Int32()
830- case 2: // preferred
831- case 3: // finished
832- m.context.Unregister(m)
833- default:
834- }
835-}
A cmd/wlturbo-scanner/main.go +132 -0
@@ -0,0 +1,132 @@
1+// Command wlturbo-scanner generates Go bindings from Wayland protocol XML files.
2+//
3+// Usage:
4+//
5+// wlturbo-scanner [flags] protocol.xml
6+//
7+// Flags:
8+//
9+// -o, --output Output file path (default: <protocol>_generated.go)
10+// -p, --package Go package name (default: derived from protocol name)
11+// -h, --help Show help
12+//
13+// Examples:
14+//
15+// # Generate bindings for virtual pointer protocol
16+// wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml
17+//
18+// # Generate with custom package name
19+// wlturbo-scanner -p vpointer -o vpointer.go protocol.xml
20+package main
21+
22+import (
23+ "flag"
24+ "fmt"
25+ "log"
26+ "os"
27+ "path/filepath"
28+ "strings"
29+
30+ "github.com/bnema/wlturbo/internal/scanner"
31+)
32+
33+func main() {
34+ var (
35+ outputPath string
36+ packageName string
37+ showHelp bool
38+ crossImports crossFlags
39+ )
40+
41+ flag.StringVar(&outputPath, "o", "", "Output file path")
42+ flag.StringVar(&outputPath, "output", "", "Output file path")
43+ flag.StringVar(&packageName, "p", "", "Go package name")
44+ flag.StringVar(&packageName, "package", "", "Go package name")
45+ flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)")
46+ flag.BoolVar(&showHelp, "h", false, "Show help")
47+ flag.BoolVar(&showHelp, "help", false, "Show help")
48+
49+ flag.Usage = func() {
50+ fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0])
51+ fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n")
52+ fmt.Fprintf(os.Stderr, "Flags:\n")
53+ flag.PrintDefaults()
54+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
55+ fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n")
56+ fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0])
57+ fmt.Fprintf(os.Stderr, " # Generate with custom package name\n")
58+ fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0])
59+ }
60+
61+ flag.Parse()
62+
63+ if showHelp || flag.NArg() == 0 {
64+ flag.Usage()
65+ if showHelp {
66+ os.Exit(0)
67+ }
68+ os.Exit(1)
69+ }
70+
71+ xmlPath := flag.Arg(0)
72+
73+ // Derive output path if not specified
74+ if outputPath == "" {
75+ base := filepath.Base(xmlPath)
76+ base = strings.TrimSuffix(base, filepath.Ext(base))
77+ outputPath = base + "_generated.go"
78+ }
79+
80+ // Derive package name if not specified
81+ if packageName == "" {
82+ base := filepath.Base(xmlPath)
83+ base = strings.TrimSuffix(base, filepath.Ext(base))
84+
85+ // Clean up common prefixes/suffixes
86+ base = strings.TrimPrefix(base, "wlr-")
87+ base = strings.TrimPrefix(base, "zwlr-")
88+ base = strings.TrimSuffix(base, "-unstable-v1")
89+ base = strings.TrimSuffix(base, "-unstable-v2")
90+ base = strings.TrimSuffix(base, "-stable-v1")
91+
92+ // Convert to valid Go package name
93+ packageName = strings.ReplaceAll(base, "-", "_")
94+ }
95+
96+ // Create scanner
97+ s := scanner.NewScanner()
98+ s.CrossPackage = crossImports
99+
100+ // Parse XML
101+ if err := s.ParseXML(xmlPath); err != nil {
102+ log.Fatalf("Failed to parse XML: %v", err)
103+ }
104+
105+ // Generate code
106+ code, err := s.Generate(packageName)
107+ if err != nil {
108+ log.Fatalf("Failed to generate code: %v", err)
109+ }
110+
111+ // Write output with secure permissions
112+ if err := os.WriteFile(outputPath, code, 0600); err != nil {
113+ log.Fatalf("Failed to write output: %v", err)
114+ }
115+
116+ fmt.Printf("Generated %s (package %s)\n", outputPath, packageName)
117+}
118+
119+type crossFlags map[string]string
120+
121+func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) }
122+func (f *crossFlags) Set(v string) error {
123+ parts := strings.SplitN(v, "=", 2)
124+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
125+ return fmt.Errorf("-import must be interface=GoImportPath")
126+ }
127+ if *f == nil {
128+ *f = make(map[string]string)
129+ }
130+ (*f)[parts[0]] = parts[1]
131+ return nil
132+}
@@ -0,0 +1,132 @@
1+// Command wlturbo-scanner generates Go bindings from Wayland protocol XML files.
2+//
3+// Usage:
4+//
5+// wlturbo-scanner [flags] protocol.xml
6+//
7+// Flags:
8+//
9+// -o, --output Output file path (default: <protocol>_generated.go)
10+// -p, --package Go package name (default: derived from protocol name)
11+// -h, --help Show help
12+//
13+// Examples:
14+//
15+// # Generate bindings for virtual pointer protocol
16+// wlturbo-scanner -o virtual_pointer.go ../wlr-protocols/unstable/wlr-virtual-pointer-unstable-v1.xml
17+//
18+// # Generate with custom package name
19+// wlturbo-scanner -p vpointer -o vpointer.go protocol.xml
20+package main
21+
22+import (
23+ "flag"
24+ "fmt"
25+ "log"
26+ "os"
27+ "path/filepath"
28+ "strings"
29+
30+ "github.com/bnema/wlturbo/internal/scanner"
31+)
32+
33+func main() {
34+ var (
35+ outputPath string
36+ packageName string
37+ showHelp bool
38+ crossImports crossFlags
39+ )
40+
41+ flag.StringVar(&outputPath, "o", "", "Output file path")
42+ flag.StringVar(&outputPath, "output", "", "Output file path")
43+ flag.StringVar(&packageName, "p", "", "Go package name")
44+ flag.StringVar(&packageName, "package", "", "Go package name")
45+ flag.Var(&crossImports, "import", "Interface=Go import path (repeatable)")
46+ flag.BoolVar(&showHelp, "h", false, "Show help")
47+ flag.BoolVar(&showHelp, "help", false, "Show help")
48+
49+ flag.Usage = func() {
50+ fmt.Fprintf(os.Stderr, "Usage: %s [flags] protocol.xml\n\n", os.Args[0])
51+ fmt.Fprintf(os.Stderr, "Generates Go bindings from Wayland protocol XML files.\n\n")
52+ fmt.Fprintf(os.Stderr, "Flags:\n")
53+ flag.PrintDefaults()
54+ fmt.Fprintf(os.Stderr, "\nExamples:\n")
55+ fmt.Fprintf(os.Stderr, " # Generate bindings for virtual pointer protocol\n")
56+ fmt.Fprintf(os.Stderr, " %s -o virtual_pointer.go wlr-virtual-pointer-unstable-v1.xml\n\n", os.Args[0])
57+ fmt.Fprintf(os.Stderr, " # Generate with custom package name\n")
58+ fmt.Fprintf(os.Stderr, " %s -p vpointer -o vpointer.go protocol.xml\n", os.Args[0])
59+ }
60+
61+ flag.Parse()
62+
63+ if showHelp || flag.NArg() == 0 {
64+ flag.Usage()
65+ if showHelp {
66+ os.Exit(0)
67+ }
68+ os.Exit(1)
69+ }
70+
71+ xmlPath := flag.Arg(0)
72+
73+ // Derive output path if not specified
74+ if outputPath == "" {
75+ base := filepath.Base(xmlPath)
76+ base = strings.TrimSuffix(base, filepath.Ext(base))
77+ outputPath = base + "_generated.go"
78+ }
79+
80+ // Derive package name if not specified
81+ if packageName == "" {
82+ base := filepath.Base(xmlPath)
83+ base = strings.TrimSuffix(base, filepath.Ext(base))
84+
85+ // Clean up common prefixes/suffixes
86+ base = strings.TrimPrefix(base, "wlr-")
87+ base = strings.TrimPrefix(base, "zwlr-")
88+ base = strings.TrimSuffix(base, "-unstable-v1")
89+ base = strings.TrimSuffix(base, "-unstable-v2")
90+ base = strings.TrimSuffix(base, "-stable-v1")
91+
92+ // Convert to valid Go package name
93+ packageName = strings.ReplaceAll(base, "-", "_")
94+ }
95+
96+ // Create scanner
97+ s := scanner.NewScanner()
98+ s.CrossPackage = crossImports
99+
100+ // Parse XML
101+ if err := s.ParseXML(xmlPath); err != nil {
102+ log.Fatalf("Failed to parse XML: %v", err)
103+ }
104+
105+ // Generate code
106+ code, err := s.Generate(packageName)
107+ if err != nil {
108+ log.Fatalf("Failed to generate code: %v", err)
109+ }
110+
111+ // Write output with secure permissions
112+ if err := os.WriteFile(outputPath, code, 0600); err != nil {
113+ log.Fatalf("Failed to write output: %v", err)
114+ }
115+
116+ fmt.Printf("Generated %s (package %s)\n", outputPath, packageName)
117+}
118+
119+type crossFlags map[string]string
120+
121+func (f *crossFlags) String() string { return fmt.Sprint(map[string]string(*f)) }
122+func (f *crossFlags) Set(v string) error {
123+ parts := strings.SplitN(v, "=", 2)
124+ if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
125+ return fmt.Errorf("-import must be interface=GoImportPath")
126+ }
127+ if *f == nil {
128+ *f = make(map[string]string)
129+ }
130+ (*f)[parts[0]] = parts[1]
131+ return nil
132+}
A consumer_test.go +44 -0
@@ -0,0 +1,44 @@
1+//go:build linux
2+
3+package wlturbo
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "path/filepath"
9+ "testing"
10+)
11+
12+// Run the public socketpair integration against a separate module, with no
13+// imports from internal/ and no worktree replacement outside its temp go.mod.
14+func TestExternalConsumer(t *testing.T) {
15+ goBin, err := exec.LookPath("go")
16+ if err != nil {
17+ t.Skip("go executable unavailable: external module integration requires go")
18+ }
19+ root, err := os.Getwd()
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ temp := t.TempDir()
24+ mod := "module example.com/wlturbo-consumer\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n"
25+ if err := os.WriteFile(filepath.Join(temp, "go.mod"), []byte(mod), 0600); err != nil {
26+ t.Fatal(err)
27+ }
28+ source, err := os.ReadFile(filepath.Join(root, "protocol/core/core_test.go"))
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ // The external module has no access to the package's test helpers except
33+ // the ones copied into this temporary module. Compile and exercise only
34+ // the public integration scenarios, including typed key, frame and release.
35+ if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil {
36+ t.Fatal(err)
37+ }
38+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...")
39+ cmd.Dir = temp
40+ cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
41+ if output, err := cmd.CombinedOutput(); err != nil {
42+ t.Fatalf("external consumer: %v\n%s", err, output)
43+ }
44+}
@@ -0,0 +1,44 @@
1+//go:build linux
2+
3+package wlturbo
4+
5+import (
6+ "os"
7+ "os/exec"
8+ "path/filepath"
9+ "testing"
10+)
11+
12+// Run the public socketpair integration against a separate module, with no
13+// imports from internal/ and no worktree replacement outside its temp go.mod.
14+func TestExternalConsumer(t *testing.T) {
15+ goBin, err := exec.LookPath("go")
16+ if err != nil {
17+ t.Skip("go executable unavailable: external module integration requires go")
18+ }
19+ root, err := os.Getwd()
20+ if err != nil {
21+ t.Fatal(err)
22+ }
23+ temp := t.TempDir()
24+ mod := "module example.com/wlturbo-consumer\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n"
25+ if err := os.WriteFile(filepath.Join(temp, "go.mod"), []byte(mod), 0600); err != nil {
26+ t.Fatal(err)
27+ }
28+ source, err := os.ReadFile(filepath.Join(root, "protocol/core/core_test.go"))
29+ if err != nil {
30+ t.Fatal(err)
31+ }
32+ // The external module has no access to the package's test helpers except
33+ // the ones copied into this temporary module. Compile and exercise only
34+ // the public integration scenarios, including typed key, frame and release.
35+ if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil {
36+ t.Fatal(err)
37+ }
38+ cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...")
39+ cmd.Dir = temp
40+ cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
41+ if output, err := cmd.CombinedOutput(); err != nil {
42+ t.Fatalf("external consumer: %v\n%s", err, output)
43+ }
44+}
M core.go +127 -379
@@ -3,10 +3,10 @@ package wlturbo
33 import (
44 "encoding/binary"
55 "errors"
6-
7- // "log"
86 "sync"
97 "sync/atomic"
8+
9+ "golang.org/x/sys/unix"
1010 )
1111
1212 // Context provides a compatibility layer for wl.Context
@@ -37,6 +37,8 @@ type Event struct {
3737 data []byte
3838 offset int
3939 display *Display
40+ fds []*OwnedFD
41+ fdIndex int
4042 }
4143
4244 // Data returns the raw event data
@@ -56,33 +58,75 @@ func NewContext(display *Display) *Context {
5658 }
5759 }
5860
59-// SendRequest sends a request through the context
61+// SendRequest sends a request through the context. The proxy is validated
62+// under the connection send lock, so no request reaches the wire after a
63+// destructor for the same proxy.
6064 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
61- if c.closed.Load() {
62- return errors.New("context is closed")
63- }
64- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
65+ return c.SendRequestWithFDs(proxy, opcode, nil, args...)
6566 }
6667
67-// SendRequestWithFDs sends a request with file descriptors through the context
68+// SendRequestWithFDs sends a request with file descriptors through the context.
69+// On error the FDs remain owned by the caller.
6870 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
71+ if err := c.CheckProxy(proxy); err != nil {
72+ return err
73+ }
74+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args)
75+}
76+
77+// CheckProxy rejects stale and foreign proxies before any bytes are written.
78+func (c *Context) CheckProxy(proxy Proxy) error {
6979 if c.closed.Load() {
7080 return errors.New("context is closed")
7181 }
72- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)
82+ if proxy == nil || proxy.ID() == 0 || proxy.Context() != c {
83+ return errors.New("invalid proxy")
84+ }
85+ registered, ok := c.proxies.Load(proxy.ID())
86+ if !ok || registered != proxy {
87+ return errors.New("proxy is not registered")
88+ }
89+ return nil
90+}
91+
92+// SendDestructor sends a destructor request exactly once. The proxy is
93+// claimed (unregistered) under the connection send lock, after marshaling
94+// and immediately before the write: concurrent destructors and later requests
95+// on the proxy fail without writing. A marshaling error leaves the proxy
96+// registered. A write error after the claim leaves the proxy unregistered;
97+// such an error means the connection is broken and must be closed.
98+func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
99+ return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
100+}
101+
102+// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
103+// the FDs remain owned by the caller.
104+func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
105+ if err := c.CheckProxy(proxy); err != nil {
106+ return err
107+ }
108+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args)
109+}
110+
111+func (c *Context) claimDestroy(proxy Proxy) error {
112+ if err := c.CheckProxy(proxy); err != nil {
113+ return err
114+ }
115+ if !c.proxies.CompareAndDelete(proxy.ID(), proxy) {
116+ return errors.New("proxy is not registered")
117+ }
118+ c.display.objects.CompareAndDelete(proxy.ID(), proxy)
119+ return nil
73120 }
74121
75122 // Register registers a proxy object
76123 func (c *Context) Register(proxy Proxy) {
77- if proxy != nil && proxy.ID() != 0 {
124+ if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
125+ if proxy.Context() != c {
126+ return
127+ }
78128 c.proxies.Store(proxy.ID(), proxy)
79129 c.display.objects.Store(proxy.ID(), proxy)
80- // Debug large IDs
81- // if proxy.ID() > 1000000 {
82- // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID())
83- // } else if proxy.ID() == 5 {
84- // log.Printf("wlclient: Registered output manager proxy with ID 5")
85- // }
86130 }
87131 }
88132
@@ -221,19 +265,61 @@ func (e *Event) Array() []byte {
221265 return arr
222266 }
223267
224-// Fd reads a file descriptor that was delivered out-of-band for this event.
225-// Descriptors are queued on the connection in arrival order, as the Wayland
226-// wire protocol requires. The returned descriptor is owned by the caller,
227-// which must close it. It returns 0 when the event carried no descriptor.
268+// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
269+// ownership to the caller (who must close it). Descriptor zero is valid.
270+type OwnedFD struct {
271+ mu sync.Mutex
272+ fd int
273+}
274+
275+func (f *OwnedFD) Take() (int, error) {
276+ if f == nil {
277+ return -1, errors.New("missing descriptor")
278+ }
279+ f.mu.Lock()
280+ defer f.mu.Unlock()
281+ if f.fd < 0 {
282+ return -1, errors.New("descriptor already taken or closed")
283+ }
284+ n := f.fd
285+ f.fd = -1
286+ return n, nil
287+}
288+func (f *OwnedFD) Close() error {
289+ if f == nil {
290+ return nil
291+ }
292+ f.mu.Lock()
293+ defer f.mu.Unlock()
294+ if f.fd < 0 {
295+ return nil
296+ }
297+ n := f.fd
298+ f.fd = -1
299+ return unix.Close(n)
300+}
301+
302+// FD transfers ownership of the next descriptor assigned to this event.
303+// An unclaimed descriptor is closed when dispatch completes.
304+func (e *Event) FD() *OwnedFD {
305+ if e.fdIndex >= len(e.fds) {
306+ return nil
307+ }
308+ fd := e.fds[e.fdIndex]
309+ e.fds[e.fdIndex] = nil
310+ e.fdIndex++
311+ return fd
312+}
313+
314+// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
228315 func (e *Event) Fd() uintptr {
229- if e.display != nil {
230- if fd, ok := e.display.nextFD(); ok {
231- return uintptr(fd)
232- }
316+ fd := e.FD()
317+ if fd == nil {
318+ _ = e.Uint32()
319+ return 0
233320 }
234- // No descriptor was delivered: consume the placeholder argument.
235- _ = e.Uint32()
236- return 0
321+ n, _ := fd.Take()
322+ return uintptr(n)
237323 }
238324
239325 // NewId reads a new object ID from the event
@@ -278,32 +364,24 @@ type RegistryGlobalRemoveEvent struct {
278364
279365 // AddGlobalHandler adds a global handler to the registry
280366 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
281- // Store the handler to be called for ALL globals
282- r.mu.Lock()
283- defer r.mu.Unlock()
284-
285- // Initialize handlers map if needed
286- if r.handlers == nil {
287- r.handlers = make(map[string]GlobalHandler)
367+ if handler == nil {
368+ return
288369 }
289-
290370 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
291- global, ok := r.FindGlobalByName(name)
292- if ok {
293- event := RegistryGlobalEvent{
294- Registry: r,
295- Name: name,
296- Interface: global.Interface,
297- Version: version,
298- }
299- handler.HandleRegistryGlobal(event)
371+ if global, ok := r.FindGlobalByName(name); ok {
372+ handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
300373 }
301374 })
302375 }
303376
304-// AddGlobalRemoveHandler adds a global remove handler (placeholder)
377+// AddGlobalRemoveHandler registers a handler for registry removals.
305378 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
306- // Would need to implement removal handling
379+ if handler == nil {
380+ return
381+ }
382+ r.mu.Lock()
383+ r.removeHandlers = append(r.removeHandlers, handler)
384+ r.mu.Unlock()
307385 }
308386
309387 // FindGlobalByName finds a global by its name ID
@@ -340,342 +418,12 @@ func (d *Display) Sync() (Object, error) {
340418 display: d,
341419 }
342420
343- // Send sync request (opcode 0)
421+ // Store before sending: the compositor may reply immediately.
422+ d.context.Register(callback)
344423 if err := d.SendRequest(1, 0, callbackID); err != nil {
345- return nil, err
346- }
347-
348- // Store the callback object
349- d.objects.Store(callbackID, callback)
350-
351- return callback, nil
352-}
353-
354-// Additional protocol object types needed for compatibility
355-
356-// Seat capability constants
357-const (
358- SeatCapabilityPointer = 1
359- SeatCapabilityKeyboard = 2
360- SeatCapabilityTouch = 4
361-)
362-
363-// Seat represents a wl_seat
364-type Seat struct {
365- BaseProxy
366- capabilities uint32
367- name string
368-}
369-
370-// SeatCapabilitiesHandler handles seat capabilities events
371-type SeatCapabilitiesHandler interface {
372- HandleSeatCapabilities(seat *Seat, capabilities uint32)
373-}
374-
375-// SeatNameHandler handles seat name events
376-type SeatNameHandler interface {
377- HandleSeatName(seat *Seat, name string)
378-}
379-
380-// NewSeat creates a new seat proxy
381-func NewSeat(ctx *Context) *Seat {
382- return &Seat{
383- BaseProxy: BaseProxy{
384- context: ctx,
385- },
386- }
387-}
388-
389-// GetPointer gets the pointer device
390-func (s *Seat) GetPointer() (*Pointer, error) {
391- pointer := &Pointer{
392- BaseProxy: BaseProxy{
393- context: s.context,
394- id: s.context.display.allocateID(),
395- },
396- }
397-
398- // Register pointer before sending request
399- s.context.Register(pointer)
400-
401- // Send get_pointer request (opcode 0)
402- err := s.context.SendRequest(s, 0, pointer.id)
403- if err != nil {
404- s.context.Unregister(pointer)
405- return nil, err
406- }
407-
408- return pointer, nil
409-}
410-
411-// GetKeyboard gets the keyboard device
412-func (s *Seat) GetKeyboard() (*Keyboard, error) {
413- keyboard := &Keyboard{
414- BaseProxy: BaseProxy{
415- context: s.context,
416- id: s.context.display.allocateID(),
417- },
418- }
419-
420- // Register keyboard before sending request
421- s.context.Register(keyboard)
422-
423- // Send get_keyboard request (opcode 1)
424- err := s.context.SendRequest(s, 1, keyboard.id)
425- if err != nil {
426- s.context.Unregister(keyboard)
427- return nil, err
428- }
429-
430- return keyboard, nil
431-}
432-
433-// GetTouch gets the touch device
434-func (s *Seat) GetTouch() (*Touch, error) {
435- touch := &Touch{
436- BaseProxy: BaseProxy{
437- context: s.context,
438- id: s.context.display.allocateID(),
439- },
440- }
441-
442- // Register touch before sending request
443- s.context.Register(touch)
444-
445- // Send get_touch request (opcode 2)
446- err := s.context.SendRequest(s, 2, touch.id)
447- if err != nil {
448- s.context.Unregister(touch)
449- return nil, err
450- }
451-
452- return touch, nil
453-}
454-
455-// Release releases the seat
456-func (s *Seat) Release() error {
457- err := s.context.SendRequest(s, 3) // opcode 3
458- if err == nil {
459- s.context.Unregister(s)
460- }
461- return err
462-}
463-
464-// Capabilities returns the seat capabilities
465-func (s *Seat) Capabilities() uint32 {
466- return s.capabilities
467-}
468-
469-// Name returns the seat name
470-func (s *Seat) Name() string {
471- return s.name
472-}
473-
474-// Dispatch handles events for the seat
475-func (s *Seat) Dispatch(event *Event) {
476- // Handle seat events
477- switch event.Opcode {
478- case 0: // capabilities
479- s.capabilities = event.Uint32()
480- // TODO: Call registered handlers
481- case 1: // name
482- s.name = event.String()
483- // TODO: Call registered handlers
484- }
485-}
486-
487-// Surface represents a wl_surface
488-type Surface struct {
489- BaseProxy
490-}
491-
492-// NewSurface creates a new surface proxy
493-func NewSurface(ctx *Context) *Surface {
494- return &Surface{
495- BaseProxy: BaseProxy{
496- context: ctx,
497- },
498- }
499-}
500-
501-// Destroy destroys the surface
502-func (s *Surface) Destroy() error {
503- err := s.context.SendRequest(s, 0) // opcode 0
504- if err == nil {
505- s.context.Unregister(s)
506- }
507- return err
508-}
509-
510-// Attach attaches a buffer to the surface
511-func (s *Surface) Attach(buffer Object, x, y int32) error {
512- return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1
513-}
514-
515-// Damage marks a region of the surface as damaged
516-func (s *Surface) Damage(x, y, width, height int32) error {
517- return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2
518-}
519-
520-// Frame requests a frame callback
521-func (s *Surface) Frame() (Object, error) {
522- callback := &callbackObject{
523- BaseProxy: BaseProxy{
524- context: s.context,
525- id: s.context.display.allocateID(),
526- },
527- display: s.context.display,
528- }
529- s.context.Register(callback)
530-
531- err := s.context.SendRequest(s, 3, callback.id) // opcode 3
532- if err != nil {
533- s.context.Unregister(callback)
424+ d.context.Unregister(callback)
534425 return nil, err
535426 }
536427
537428 return callback, nil
538429 }
539-
540-// SetOpaqueRegion sets the opaque region
541-func (s *Surface) SetOpaqueRegion(region *Region) error {
542- return s.context.SendRequest(s, 4, region) // opcode 4
543-}
544-
545-// SetInputRegion sets the input region
546-func (s *Surface) SetInputRegion(region *Region) error {
547- return s.context.SendRequest(s, 5, region) // opcode 5
548-}
549-
550-// Commit commits pending surface state
551-func (s *Surface) Commit() error {
552- return s.context.SendRequest(s, 6) // opcode 6
553-}
554-
555-// SetBufferTransform sets the buffer transform
556-func (s *Surface) SetBufferTransform(transform int32) error {
557- return s.context.SendRequest(s, 7, transform) // opcode 7
558-}
559-
560-// SetBufferScale sets the buffer scale
561-func (s *Surface) SetBufferScale(scale int32) error {
562- return s.context.SendRequest(s, 8, scale) // opcode 8
563-}
564-
565-// DamageBuffer marks a region of the buffer as damaged
566-func (s *Surface) DamageBuffer(x, y, width, height int32) error {
567- return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9
568-}
569-
570-// Offset sets the buffer offset
571-func (s *Surface) Offset(x, y int32) error {
572- return s.context.SendRequest(s, 10, x, y) // opcode 10
573-}
574-
575-// Dispatch handles surface events
576-func (s *Surface) Dispatch(event *Event) {
577- // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events
578- // But for now we don't handle them
579-}
580-
581-// Pointer represents a wl_pointer
582-type Pointer struct {
583- BaseProxy
584-}
585-
586-// Keyboard represents a wl_keyboard
587-type Keyboard struct {
588- BaseProxy
589-}
590-
591-// Touch represents a wl_touch
592-type Touch struct {
593- BaseProxy
594-}
595-
596-// Output represents a wl_output
597-type Output struct {
598- BaseProxy
599-}
600-
601-// Region represents a wl_region
602-type Region struct {
603- BaseProxy
604-}
605-
606-// Add adds a rectangle to the region
607-func (r *Region) Add(x, y, width, height int32) error {
608- return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0
609-}
610-
611-// Subtract subtracts a rectangle from the region
612-func (r *Region) Subtract(x, y, width, height int32) error {
613- return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1
614-}
615-
616-// Destroy destroys the region
617-func (r *Region) Destroy() error {
618- err := r.context.SendRequest(r, 2) // opcode 2
619- if err == nil {
620- r.context.Unregister(r)
621- }
622- return err
623-}
624-
625-// Compositor represents a wl_compositor
626-type Compositor struct {
627- BaseProxy
628-}
629-
630-// NewCompositor creates a new compositor proxy
631-func NewCompositor(ctx *Context) *Compositor {
632- return &Compositor{
633- BaseProxy: BaseProxy{
634- context: ctx,
635- },
636- }
637-}
638-
639-// CreateSurface creates a new surface
640-func (c *Compositor) CreateSurface() (*Surface, error) {
641- surface := &Surface{
642- BaseProxy: BaseProxy{
643- context: c.context,
644- id: c.context.display.allocateID(),
645- },
646- }
647-
648- // Register surface before sending request
649- c.context.Register(surface)
650-
651- // Send create_surface request (opcode 0)
652- err := c.context.SendRequest(c, 0, surface.id)
653- if err != nil {
654- c.context.Unregister(surface)
655- return nil, err
656- }
657-
658- return surface, nil
659-}
660-
661-// CreateRegion creates a new region
662-func (c *Compositor) CreateRegion() (*Region, error) {
663- region := &Region{
664- BaseProxy: BaseProxy{
665- context: c.context,
666- id: c.context.display.allocateID(),
667- },
668- }
669-
670- // Register region before sending request
671- c.context.Register(region)
672-
673- // Send create_region request (opcode 1)
674- err := c.context.SendRequest(c, 1, region.id)
675- if err != nil {
676- c.context.Unregister(region)
677- return nil, err
678- }
679-
680- return region, nil
681-}
@@ -3,10 +3,10 @@ package wlturbo
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5 "errors"5 "errors"
6-
7- // "log"
8 "sync"6 "sync"
9 "sync/atomic"7 "sync/atomic"
8+
9+ "golang.org/x/sys/unix"
10 )10 )
11 11
12 // Context provides a compatibility layer for wl.Context12 // Context provides a compatibility layer for wl.Context
@@ -37,6 +37,8 @@ type Event struct {
37 data []byte37 data []byte
38 offset int38 offset int
39 display *Display39 display *Display
40+ fds []*OwnedFD
41+ fdIndex int
40 }42 }
41 43
42 // Data returns the raw event data44 // Data returns the raw event data
@@ -56,33 +58,75 @@ func NewContext(display *Display) *Context {
56 }58 }
57 }59 }
58 60
59-// SendRequest sends a request through the context61+// SendRequest sends a request through the context. The proxy is validated
62+// under the connection send lock, so no request reaches the wire after a
63+// destructor for the same proxy.
60 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {64 func (c *Context) SendRequest(proxy Proxy, opcode uint32, args ...interface{}) error {
61- if c.closed.Load() {65+ return c.SendRequestWithFDs(proxy, opcode, nil, args...)
62- return errors.New("context is closed")
63- }
64- return c.display.SendRequest(proxy.ID(), uint16(opcode), args...)
65 }66 }
66 67
67-// SendRequestWithFDs sends a request with file descriptors through the context68+// SendRequestWithFDs sends a request with file descriptors through the context.
69+// On error the FDs remain owned by the caller.
68 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {70 func (c *Context) SendRequestWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
71+ if err := c.CheckProxy(proxy); err != nil {
72+ return err
73+ }
74+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.CheckProxy(proxy) }, args)
75+}
76+
77+// CheckProxy rejects stale and foreign proxies before any bytes are written.
78+func (c *Context) CheckProxy(proxy Proxy) error {
69 if c.closed.Load() {79 if c.closed.Load() {
70 return errors.New("context is closed")80 return errors.New("context is closed")
71 }81 }
72- return c.display.SendRequestWithFDs(proxy.ID(), uint16(opcode), fds, args...)82+ if proxy == nil || proxy.ID() == 0 || proxy.Context() != c {
83+ return errors.New("invalid proxy")
84+ }
85+ registered, ok := c.proxies.Load(proxy.ID())
86+ if !ok || registered != proxy {
87+ return errors.New("proxy is not registered")
88+ }
89+ return nil
90+}
91+
92+// SendDestructor sends a destructor request exactly once. The proxy is
93+// claimed (unregistered) under the connection send lock, after marshaling
94+// and immediately before the write: concurrent destructors and later requests
95+// on the proxy fail without writing. A marshaling error leaves the proxy
96+// registered. A write error after the claim leaves the proxy unregistered;
97+// such an error means the connection is broken and must be closed.
98+func (c *Context) SendDestructor(proxy Proxy, opcode uint32, args ...interface{}) error {
99+ return c.SendDestructorWithFDs(proxy, opcode, nil, args...)
100+}
101+
102+// SendDestructorWithFDs is SendDestructor for requests carrying FDs. On error
103+// the FDs remain owned by the caller.
104+func (c *Context) SendDestructorWithFDs(proxy Proxy, opcode uint32, fds []int, args ...interface{}) error {
105+ if err := c.CheckProxy(proxy); err != nil {
106+ return err
107+ }
108+ return c.display.sendRequest(proxy.ID(), uint16(opcode), fds, func() error { return c.claimDestroy(proxy) }, args)
109+}
110+
111+func (c *Context) claimDestroy(proxy Proxy) error {
112+ if err := c.CheckProxy(proxy); err != nil {
113+ return err
114+ }
115+ if !c.proxies.CompareAndDelete(proxy.ID(), proxy) {
116+ return errors.New("proxy is not registered")
117+ }
118+ c.display.objects.CompareAndDelete(proxy.ID(), proxy)
119+ return nil
73 }120 }
74 121
75 // Register registers a proxy object122 // Register registers a proxy object
76 func (c *Context) Register(proxy Proxy) {123 func (c *Context) Register(proxy Proxy) {
77- if proxy != nil && proxy.ID() != 0 {124+ if proxy != nil && proxy.ID() != 0 && !c.closed.Load() {
125+ if proxy.Context() != c {
126+ return
127+ }
78 c.proxies.Store(proxy.ID(), proxy)128 c.proxies.Store(proxy.ID(), proxy)
79 c.display.objects.Store(proxy.ID(), proxy)129 c.display.objects.Store(proxy.ID(), proxy)
80- // Debug large IDs
81- // if proxy.ID() > 1000000 {
82- // log.Printf("wlclient: WARNING: Registering proxy with suspicious ID %d (0x%x)", proxy.ID(), proxy.ID())
83- // } else if proxy.ID() == 5 {
84- // log.Printf("wlclient: Registered output manager proxy with ID 5")
85- // }
86 }130 }
87 }131 }
88 132
@@ -221,19 +265,61 @@ func (e *Event) Array() []byte {
221 return arr265 return arr
222 }266 }
223 267
224-// Fd reads a file descriptor that was delivered out-of-band for this event.268+// OwnedFD owns one received descriptor. Close is idempotent; Take transfers
225-// Descriptors are queued on the connection in arrival order, as the Wayland269+// ownership to the caller (who must close it). Descriptor zero is valid.
226-// wire protocol requires. The returned descriptor is owned by the caller,270+type OwnedFD struct {
227-// which must close it. It returns 0 when the event carried no descriptor.271+ mu sync.Mutex
272+ fd int
273+}
274+
275+func (f *OwnedFD) Take() (int, error) {
276+ if f == nil {
277+ return -1, errors.New("missing descriptor")
278+ }
279+ f.mu.Lock()
280+ defer f.mu.Unlock()
281+ if f.fd < 0 {
282+ return -1, errors.New("descriptor already taken or closed")
283+ }
284+ n := f.fd
285+ f.fd = -1
286+ return n, nil
287+}
288+func (f *OwnedFD) Close() error {
289+ if f == nil {
290+ return nil
291+ }
292+ f.mu.Lock()
293+ defer f.mu.Unlock()
294+ if f.fd < 0 {
295+ return nil
296+ }
297+ n := f.fd
298+ f.fd = -1
299+ return unix.Close(n)
300+}
301+
302+// FD transfers ownership of the next descriptor assigned to this event.
303+// An unclaimed descriptor is closed when dispatch completes.
304+func (e *Event) FD() *OwnedFD {
305+ if e.fdIndex >= len(e.fds) {
306+ return nil
307+ }
308+ fd := e.fds[e.fdIndex]
309+ e.fds[e.fdIndex] = nil
310+ e.fdIndex++
311+ return fd
312+}
313+
314+// Fd is the legacy raw descriptor API; prefer FD for explicit ownership.
228 func (e *Event) Fd() uintptr {315 func (e *Event) Fd() uintptr {
229- if e.display != nil {316+ fd := e.FD()
230- if fd, ok := e.display.nextFD(); ok {317+ if fd == nil {
231- return uintptr(fd)318+ _ = e.Uint32()
232- }319+ return 0
233 }320 }
234- // No descriptor was delivered: consume the placeholder argument.321+ n, _ := fd.Take()
235- _ = e.Uint32()322+ return uintptr(n)
236- return 0
237 }323 }
238 324
239 // NewId reads a new object ID from the event325 // NewId reads a new object ID from the event
@@ -278,32 +364,24 @@ type RegistryGlobalRemoveEvent struct {
278 364
279 // AddGlobalHandler adds a global handler to the registry365 // AddGlobalHandler adds a global handler to the registry
280 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {366 func (r *Registry) AddGlobalHandler(handler RegistryGlobalHandler) {
281- // Store the handler to be called for ALL globals367+ if handler == nil {
282- r.mu.Lock()368+ return
283- defer r.mu.Unlock()
284-
285- // Initialize handlers map if needed
286- if r.handlers == nil {
287- r.handlers = make(map[string]GlobalHandler)
288 }369 }
289-
290 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {370 r.AddHandler("*", func(registry *Registry, name uint32, version uint32) {
291- global, ok := r.FindGlobalByName(name)371+ if global, ok := r.FindGlobalByName(name); ok {
292- if ok {372+ handler.HandleRegistryGlobal(RegistryGlobalEvent{Registry: r, Name: name, Interface: global.Interface, Version: version})
293- event := RegistryGlobalEvent{
294- Registry: r,
295- Name: name,
296- Interface: global.Interface,
297- Version: version,
298- }
299- handler.HandleRegistryGlobal(event)
300 }373 }
301 })374 })
302 }375 }
303 376
304-// AddGlobalRemoveHandler adds a global remove handler (placeholder)377+// AddGlobalRemoveHandler registers a handler for registry removals.
305 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {378 func (r *Registry) AddGlobalRemoveHandler(handler RegistryGlobalRemoveHandler) {
306- // Would need to implement removal handling379+ if handler == nil {
380+ return
381+ }
382+ r.mu.Lock()
383+ r.removeHandlers = append(r.removeHandlers, handler)
384+ r.mu.Unlock()
307 }385 }
308 386
309 // FindGlobalByName finds a global by its name ID387 // FindGlobalByName finds a global by its name ID
@@ -340,342 +418,12 @@ func (d *Display) Sync() (Object, error) {
340 display: d,418 display: d,
341 }419 }
342 420
343- // Send sync request (opcode 0)421+ // Store before sending: the compositor may reply immediately.
422+ d.context.Register(callback)
344 if err := d.SendRequest(1, 0, callbackID); err != nil {423 if err := d.SendRequest(1, 0, callbackID); err != nil {
345- return nil, err424+ d.context.Unregister(callback)
346- }
347-
348- // Store the callback object
349- d.objects.Store(callbackID, callback)
350-
351- return callback, nil
352-}
353-
354-// Additional protocol object types needed for compatibility
355-
356-// Seat capability constants
357-const (
358- SeatCapabilityPointer = 1
359- SeatCapabilityKeyboard = 2
360- SeatCapabilityTouch = 4
361-)
362-
363-// Seat represents a wl_seat
364-type Seat struct {
365- BaseProxy
366- capabilities uint32
367- name string
368-}
369-
370-// SeatCapabilitiesHandler handles seat capabilities events
371-type SeatCapabilitiesHandler interface {
372- HandleSeatCapabilities(seat *Seat, capabilities uint32)
373-}
374-
375-// SeatNameHandler handles seat name events
376-type SeatNameHandler interface {
377- HandleSeatName(seat *Seat, name string)
378-}
379-
380-// NewSeat creates a new seat proxy
381-func NewSeat(ctx *Context) *Seat {
382- return &Seat{
383- BaseProxy: BaseProxy{
384- context: ctx,
385- },
386- }
387-}
388-
389-// GetPointer gets the pointer device
390-func (s *Seat) GetPointer() (*Pointer, error) {
391- pointer := &Pointer{
392- BaseProxy: BaseProxy{
393- context: s.context,
394- id: s.context.display.allocateID(),
395- },
396- }
397-
398- // Register pointer before sending request
399- s.context.Register(pointer)
400-
401- // Send get_pointer request (opcode 0)
402- err := s.context.SendRequest(s, 0, pointer.id)
403- if err != nil {
404- s.context.Unregister(pointer)
405- return nil, err
406- }
407-
408- return pointer, nil
409-}
410-
411-// GetKeyboard gets the keyboard device
412-func (s *Seat) GetKeyboard() (*Keyboard, error) {
413- keyboard := &Keyboard{
414- BaseProxy: BaseProxy{
415- context: s.context,
416- id: s.context.display.allocateID(),
417- },
418- }
419-
420- // Register keyboard before sending request
421- s.context.Register(keyboard)
422-
423- // Send get_keyboard request (opcode 1)
424- err := s.context.SendRequest(s, 1, keyboard.id)
425- if err != nil {
426- s.context.Unregister(keyboard)
427- return nil, err
428- }
429-
430- return keyboard, nil
431-}
432-
433-// GetTouch gets the touch device
434-func (s *Seat) GetTouch() (*Touch, error) {
435- touch := &Touch{
436- BaseProxy: BaseProxy{
437- context: s.context,
438- id: s.context.display.allocateID(),
439- },
440- }
441-
442- // Register touch before sending request
443- s.context.Register(touch)
444-
445- // Send get_touch request (opcode 2)
446- err := s.context.SendRequest(s, 2, touch.id)
447- if err != nil {
448- s.context.Unregister(touch)
449- return nil, err
450- }
451-
452- return touch, nil
453-}
454-
455-// Release releases the seat
456-func (s *Seat) Release() error {
457- err := s.context.SendRequest(s, 3) // opcode 3
458- if err == nil {
459- s.context.Unregister(s)
460- }
461- return err
462-}
463-
464-// Capabilities returns the seat capabilities
465-func (s *Seat) Capabilities() uint32 {
466- return s.capabilities
467-}
468-
469-// Name returns the seat name
470-func (s *Seat) Name() string {
471- return s.name
472-}
473-
474-// Dispatch handles events for the seat
475-func (s *Seat) Dispatch(event *Event) {
476- // Handle seat events
477- switch event.Opcode {
478- case 0: // capabilities
479- s.capabilities = event.Uint32()
480- // TODO: Call registered handlers
481- case 1: // name
482- s.name = event.String()
483- // TODO: Call registered handlers
484- }
485-}
486-
487-// Surface represents a wl_surface
488-type Surface struct {
489- BaseProxy
490-}
491-
492-// NewSurface creates a new surface proxy
493-func NewSurface(ctx *Context) *Surface {
494- return &Surface{
495- BaseProxy: BaseProxy{
496- context: ctx,
497- },
498- }
499-}
500-
501-// Destroy destroys the surface
502-func (s *Surface) Destroy() error {
503- err := s.context.SendRequest(s, 0) // opcode 0
504- if err == nil {
505- s.context.Unregister(s)
506- }
507- return err
508-}
509-
510-// Attach attaches a buffer to the surface
511-func (s *Surface) Attach(buffer Object, x, y int32) error {
512- return s.context.SendRequest(s, 1, buffer, x, y) // opcode 1
513-}
514-
515-// Damage marks a region of the surface as damaged
516-func (s *Surface) Damage(x, y, width, height int32) error {
517- return s.context.SendRequest(s, 2, x, y, width, height) // opcode 2
518-}
519-
520-// Frame requests a frame callback
521-func (s *Surface) Frame() (Object, error) {
522- callback := &callbackObject{
523- BaseProxy: BaseProxy{
524- context: s.context,
525- id: s.context.display.allocateID(),
526- },
527- display: s.context.display,
528- }
529- s.context.Register(callback)
530-
531- err := s.context.SendRequest(s, 3, callback.id) // opcode 3
532- if err != nil {
533- s.context.Unregister(callback)
534 return nil, err425 return nil, err
535 }426 }
536 427
537 return callback, nil428 return callback, nil
538 }429 }
539-
540-// SetOpaqueRegion sets the opaque region
541-func (s *Surface) SetOpaqueRegion(region *Region) error {
542- return s.context.SendRequest(s, 4, region) // opcode 4
543-}
544-
545-// SetInputRegion sets the input region
546-func (s *Surface) SetInputRegion(region *Region) error {
547- return s.context.SendRequest(s, 5, region) // opcode 5
548-}
549-
550-// Commit commits pending surface state
551-func (s *Surface) Commit() error {
552- return s.context.SendRequest(s, 6) // opcode 6
553-}
554-
555-// SetBufferTransform sets the buffer transform
556-func (s *Surface) SetBufferTransform(transform int32) error {
557- return s.context.SendRequest(s, 7, transform) // opcode 7
558-}
559-
560-// SetBufferScale sets the buffer scale
561-func (s *Surface) SetBufferScale(scale int32) error {
562- return s.context.SendRequest(s, 8, scale) // opcode 8
563-}
564-
565-// DamageBuffer marks a region of the buffer as damaged
566-func (s *Surface) DamageBuffer(x, y, width, height int32) error {
567- return s.context.SendRequest(s, 9, x, y, width, height) // opcode 9
568-}
569-
570-// Offset sets the buffer offset
571-func (s *Surface) Offset(x, y int32) error {
572- return s.context.SendRequest(s, 10, x, y) // opcode 10
573-}
574-
575-// Dispatch handles surface events
576-func (s *Surface) Dispatch(event *Event) {
577- // Surface can receive enter/leave/preferred_buffer_scale/preferred_buffer_transform events
578- // But for now we don't handle them
579-}
580-
581-// Pointer represents a wl_pointer
582-type Pointer struct {
583- BaseProxy
584-}
585-
586-// Keyboard represents a wl_keyboard
587-type Keyboard struct {
588- BaseProxy
589-}
590-
591-// Touch represents a wl_touch
592-type Touch struct {
593- BaseProxy
594-}
595-
596-// Output represents a wl_output
597-type Output struct {
598- BaseProxy
599-}
600-
601-// Region represents a wl_region
602-type Region struct {
603- BaseProxy
604-}
605-
606-// Add adds a rectangle to the region
607-func (r *Region) Add(x, y, width, height int32) error {
608- return r.context.SendRequest(r, 0, x, y, width, height) // opcode 0
609-}
610-
611-// Subtract subtracts a rectangle from the region
612-func (r *Region) Subtract(x, y, width, height int32) error {
613- return r.context.SendRequest(r, 1, x, y, width, height) // opcode 1
614-}
615-
616-// Destroy destroys the region
617-func (r *Region) Destroy() error {
618- err := r.context.SendRequest(r, 2) // opcode 2
619- if err == nil {
620- r.context.Unregister(r)
621- }
622- return err
623-}
624-
625-// Compositor represents a wl_compositor
626-type Compositor struct {
627- BaseProxy
628-}
629-
630-// NewCompositor creates a new compositor proxy
631-func NewCompositor(ctx *Context) *Compositor {
632- return &Compositor{
633- BaseProxy: BaseProxy{
634- context: ctx,
635- },
636- }
637-}
638-
639-// CreateSurface creates a new surface
640-func (c *Compositor) CreateSurface() (*Surface, error) {
641- surface := &Surface{
642- BaseProxy: BaseProxy{
643- context: c.context,
644- id: c.context.display.allocateID(),
645- },
646- }
647-
648- // Register surface before sending request
649- c.context.Register(surface)
650-
651- // Send create_surface request (opcode 0)
652- err := c.context.SendRequest(c, 0, surface.id)
653- if err != nil {
654- c.context.Unregister(surface)
655- return nil, err
656- }
657-
658- return surface, nil
659-}
660-
661-// CreateRegion creates a new region
662-func (c *Compositor) CreateRegion() (*Region, error) {
663- region := &Region{
664- BaseProxy: BaseProxy{
665- context: c.context,
666- id: c.context.display.allocateID(),
667- },
668- }
669-
670- // Register region before sending request
671- c.context.Register(region)
672-
673- // Send create_region request (opcode 1)
674- err := c.context.SendRequest(c, 1, region.id)
675- if err != nil {
676- c.context.Unregister(region)
677- return nil, err
678- }
679-
680- return region, nil
681-}
A docs/protocol-migration.md +29 -0
@@ -0,0 +1,29 @@
1+# Canonical core / migration (v0.x)
2+
3+`protocol/core` replaces root-package and `wl` shim core object types. Bootstrap
4+`Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl`
5+transport. Bind globals using `core.NewCompositor(display.Context())`,
6+`core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name,
7+core.CompositorInterface, version, compositor)`. Requests create typed registered
8+children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and
9+server-side `new_id` events register concrete objects before invoking handlers.
10+Do not overwrite registered objects with a second wrapper.
11+
12+Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`,
13+`Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat
14+capability aliases have moved to `protocol/core`. Generated methods accept
15+concrete core objects and return concrete children, not `wl.Object`. Legacy
16+raw `Event.Fd()` is not for generated events; generated FD handlers receive
17+`*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later
18+handlers receive nil FD. On a successful full send a generated FD request
19+closes the local descriptor; on error the caller keeps it. `Registry.BindID`
20+remains for legacy callers but cannot register typed event dispatch: prefer
21+`Bind` and generated constructors. Unrecognized custom-proxy event opcodes
22+now fail unless declared via `Display.RegisterEventSignature`.
23+
24+The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core`
25+(repeat for each external type); local XML definitions take precedence, then
26+explicit external mapping, then only bootstrap display/registry. Generate with
27+`GOWORK=off go generate ./...`; metadata and XML license are in
28+`protocol/SOURCE.md`. P3 extensions will use the same scanner and public core
29+mappings; no generator-owned compositor policy is introduced here.
@@ -0,0 +1,29 @@
1+# Canonical core / migration (v0.x)
2+
3+`protocol/core` replaces root-package and `wl` shim core object types. Bootstrap
4+`Display`, `Registry`, `Context`, and `BaseProxy` remain in the root/`wl`
5+transport. Bind globals using `core.NewCompositor(display.Context())`,
6+`core.NewSeat(display.Context())`, etc. and `display.Registry().Bind(name,
7+core.CompositorInterface, version, compositor)`. Requests create typed registered
8+children (`seat.GetKeyboard()`, `surface.Frame()`, `shm.CreatePool()`) and
9+server-side `new_id` events register concrete objects before invoking handlers.
10+Do not overwrite registered objects with a second wrapper.
11+
12+Breaking changes: `wlturbo`/`wl` `Seat`, `Surface`, `Pointer`, `Keyboard`,
13+`Touch`, `Output`, `Region`, `Compositor`, their constructors, and seat
14+capability aliases have moved to `protocol/core`. Generated methods accept
15+concrete core objects and return concrete children, not `wl.Object`. Legacy
16+raw `Event.Fd()` is not for generated events; generated FD handlers receive
17+`*wlturbo.OwnedFD` with `Take() (int,error)` or `Close()`; second and later
18+handlers receive nil FD. On a successful full send a generated FD request
19+closes the local descriptor; on error the caller keeps it. `Registry.BindID`
20+remains for legacy callers but cannot register typed event dispatch: prefer
21+`Bind` and generated constructors. Unrecognized custom-proxy event opcodes
22+now fail unless declared via `Display.RegisterEventSignature`.
23+
24+The scanner accepts `-import wl_surface=github.com/bnema/wlturbo/protocol/core`
25+(repeat for each external type); local XML definitions take precedence, then
26+explicit external mapping, then only bootstrap display/registry. Generate with
27+`GOWORK=off go generate ./...`; metadata and XML license are in
28+`protocol/SOURCE.md`. P3 extensions will use the same scanner and public core
29+mappings; no generator-owned compositor policy is introduced here.
M event_pool.go +0 -34
@@ -27,10 +27,6 @@ type EventDispatcher struct {
2727
2828 // Extended handlers for object IDs >= 1024 (rare case)
2929 extHandlers sync.Map
30-
31- // Pre-allocated event objects to avoid allocations
32- eventCache [64]Event
33- eventIndex atomic.Uint32
3430 }
3531
3632 // handlerEntry stores handlers for a specific object
@@ -166,36 +162,6 @@ type RawEvent struct {
166162 Data []byte
167163 }
168164
169-// DirectDispatcher provides the absolute fastest dispatch path for hot events
170-type DirectDispatcher struct {
171- // Direct function pointers for the hottest paths (e.g., pointer motion)
172- pointerMotion func(surfaceX, surfaceY Fixed)
173- pointerButton func(button, state uint32)
174- keyboardKey func(key, state uint32)
175- frameCallback func(callbackData uint32)
176-
177- // Fallback to regular dispatcher
178- fallback *EventDispatcher
179-}
180-
181-// DispatchPointerMotion dispatches pointer motion events with zero overhead
182-//
183-//go:inline
184-func (d *DirectDispatcher) DispatchPointerMotion(surfaceX, surfaceY Fixed) {
185- if d.pointerMotion != nil {
186- d.pointerMotion(surfaceX, surfaceY)
187- }
188-}
189-
190-// DispatchPointerButton dispatches pointer button events with zero overhead
191-//
192-//go:inline
193-func (d *DirectDispatcher) DispatchPointerButton(button, state uint32) {
194- if d.pointerButton != nil {
195- d.pointerButton(button, state)
196- }
197-}
198-
199165 // Ensure cache line alignment for hot data structures
200166 type cacheLinePad [64]byte
201167
@@ -27,10 +27,6 @@ type EventDispatcher struct {
27 27
28 // Extended handlers for object IDs >= 1024 (rare case)28 // Extended handlers for object IDs >= 1024 (rare case)
29 extHandlers sync.Map29 extHandlers sync.Map
30-
31- // Pre-allocated event objects to avoid allocations
32- eventCache [64]Event
33- eventIndex atomic.Uint32
34 }30 }
35 31
36 // handlerEntry stores handlers for a specific object32 // handlerEntry stores handlers for a specific object
@@ -166,36 +162,6 @@ type RawEvent struct {
166 Data []byte162 Data []byte
167 }163 }
168 164
169-// DirectDispatcher provides the absolute fastest dispatch path for hot events
170-type DirectDispatcher struct {
171- // Direct function pointers for the hottest paths (e.g., pointer motion)
172- pointerMotion func(surfaceX, surfaceY Fixed)
173- pointerButton func(button, state uint32)
174- keyboardKey func(key, state uint32)
175- frameCallback func(callbackData uint32)
176-
177- // Fallback to regular dispatcher
178- fallback *EventDispatcher
179-}
180-
181-// DispatchPointerMotion dispatches pointer motion events with zero overhead
182-//
183-//go:inline
184-func (d *DirectDispatcher) DispatchPointerMotion(surfaceX, surfaceY Fixed) {
185- if d.pointerMotion != nil {
186- d.pointerMotion(surfaceX, surfaceY)
187- }
188-}
189-
190-// DispatchPointerButton dispatches pointer button events with zero overhead
191-//
192-//go:inline
193-func (d *DirectDispatcher) DispatchPointerButton(button, state uint32) {
194- if d.pointerButton != nil {
195- d.pointerButton(button, state)
196- }
197-}
198-
199 // Ensure cache line alignment for hot data structures165 // Ensure cache line alignment for hot data structures
200 type cacheLinePad [64]byte166 type cacheLinePad [64]byte
201 167
M fd_test.go +92 -48
@@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) {
9090
9191 d := newDisplay(client)
9292 fds := []int{}
93+ d.RegisterEventSignature(7, 3, "uint,fd,")
94+ d.RegisterEventSignature(7, 4, "uint,")
9395 d.objects.Store(uint32(7), &fdRecordingProxy{
9496 BaseProxy: BaseProxy{id: 7, context: d.context},
9597 fds: &fds,
@@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) {
146148 readSentinel(t, r, "single-fd-sentinel")
147149 }
148150
149-func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) {
151+func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) {
150152 client, peer := unixSocketPair(t)
151- d, received := newFDDisplay(t, client)
152-
153- firstR, firstW, err := os.Pipe()
154- if err != nil {
155- t.Fatalf("pipe: %v", err)
156- }
157- defer firstR.Close()
158- secondR, secondW, err := os.Pipe()
153+ d, _ := newFDDisplay(t, client)
154+ r, w, err := os.Pipe()
159155 if err != nil {
160- t.Fatalf("pipe: %v", err)
156+ t.Fatal(err)
161157 }
162- defer secondR.Close()
163-
164- sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}),
165- []int{int(firstW.Fd()), int(secondW.Fd())})
166- firstW.Close()
167- secondW.Close()
168-
169- if err := d.Dispatch(); err != nil {
170- t.Fatalf("Dispatch: %v", err)
171- }
172-
173- // A handler consumes one descriptor per fd argument, so the test drains
174- // whatever the connection still holds, in arrival order.
175- for {
176- fd, ok := d.nextFD()
177- if !ok {
178- break
179- }
180- *received = append(*received, fd)
181- }
182-
183- if len(*received) != 2 {
184- t.Fatalf("received %d descriptors, want 2", len(*received))
158+ defer r.Close()
159+ defer w.Close()
160+ sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())})
161+ if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) {
162+ t.Fatalf("extra FD: %v", err)
185163 }
186-
187- writeSentinel(t, uintptr((*received)[0]), "first-pipe")
188- writeSentinel(t, uintptr((*received)[1]), "second-pipe")
189-
190- readSentinel(t, firstR, "first-pipe")
191- readSentinel(t, secondR, "second-pipe")
192164 }
193165
194166 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
@@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
304276 }
305277
306278 // A truncated control message must not leave descriptors queued.
307- d.fdMu.Lock()
279+ d.recvMu.Lock()
308280 pending := len(d.pendingFDs)
309- d.fdMu.Unlock()
281+ d.recvMu.Unlock()
310282 if pending != 0 {
311283 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
312284 }
@@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
315287 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
316288 client, peer := unixSocketPair(t)
317289 d := newDisplay(client)
290+ d.RegisterEventSignature(7, 3, "uint,fd,")
318291 d.objects.Store(uint32(7), &fdIgnoringProxy{
319292 BaseProxy: BaseProxy{id: 7, context: d.context},
320293 })
@@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
333306 t.Fatalf("Dispatch: %v", err)
334307 }
335308
336- // While the display still holds the descriptor the pipe stays open.
337- if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {
338- t.Fatalf("SetReadDeadline: %v", err)
309+ // An ignored FD is closed at the end of the event, not at shutdown.
310+ if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
311+ t.Fatal(err)
339312 }
340- if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) {
341- t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded)
313+ if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
314+ t.Fatalf("ignored FD: %v", err)
342315 }
343316
344317 // Closing the display must release it, so the read end sees EOF.
@@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
421394
422395 client, peer := rawSocketPair(t)
423396 d := newDisplay(client)
397+ d.RegisterEventSignature(7, 3, "uint,fd,")
424398 d.objects.Store(uint32(7), &fdIgnoringProxy{
425399 BaseProxy: BaseProxy{id: 7, context: d.context},
426400 })
@@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
448422 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
449423 client, peer := unixSocketPair(t)
450424 d := newDisplay(client)
425+ d.RegisterEventSignature(7, 3, "uint,fd,")
451426 d.objects.Store(uint32(7), &fdIgnoringProxy{
452427 BaseProxy: BaseProxy{id: 7, context: d.context},
453428 })
@@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
467442 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
468443 }
469444
470- d.fdMu.Lock()
445+ d.recvMu.Lock()
471446 pending := len(d.pendingFDs)
472- d.fdMu.Unlock()
447+ d.recvMu.Unlock()
473448 if pending != 0 {
474449 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
475450 }
@@ -481,3 +456,72 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
481456 t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF)
482457 }
483458 }
459+
460+// Closing in a callback must not wait for the dispatch reader's state lock.
461+type closingProxy struct {
462+ BaseProxy
463+ d *Display
464+ returned bool
465+}
466+
467+func (p *closingProxy) EventSignature(op uint16) (string, bool) { return "fd,", op == 0 }
468+func (p *closingProxy) Dispatch(*Event) { p.returned = p.d.Close() == nil }
469+
470+func TestCloseInsideHandlerClosesQueuedFDs(t *testing.T) {
471+ client, peer := unixSocketPair(t)
472+ d := newDisplay(client)
473+ proxy := &closingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, d: d}
474+ d.context.Register(proxy)
475+ r, w, err := os.Pipe()
476+ if err != nil {
477+ t.Fatal(err)
478+ }
479+ defer r.Close()
480+ extra, xw, err := os.Pipe()
481+ if err != nil {
482+ t.Fatal(err)
483+ }
484+ defer extra.Close()
485+ // Two frames in one read; second FD remains queued while first handler runs.
486+ payload := append(message(7, 0, nil), message(7, 0, nil)...)
487+ sendMessageWithFDs(t, peer, payload, []int{int(w.Fd()), int(xw.Fd())})
488+ w.Close()
489+ xw.Close()
490+ if err := d.Dispatch(); err != nil {
491+ t.Fatal(err)
492+ }
493+ if !proxy.returned {
494+ t.Fatal("Close did not return from handler")
495+ }
496+ for _, reader := range []*os.File{r, extra} {
497+ if err := reader.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
498+ t.Fatal(err)
499+ }
500+ if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
501+ t.Fatalf("fd not closed: %v", err)
502+ }
503+ }
504+}
505+
506+func TestParseFDsKeepsRightsBeforeMalformedHeader(t *testing.T) {
507+ r, w, err := os.Pipe()
508+ if err != nil {
509+ t.Fatal(err)
510+ }
511+ defer r.Close()
512+ defer w.Close()
513+ rights := unix.UnixRights(int(w.Fd()))
514+ oob := append(rights, make([]byte, unix.SizeofCmsghdr)...)
515+ fds, err := parseFDs(oob)
516+ if err == nil || len(fds) != 1 {
517+ t.Fatalf("fds=%v err=%v", fds, err)
518+ }
519+ if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != nil {
520+ t.Fatal(err)
521+ }
522+ for _, fd := range fds {
523+ if err := unix.Close(fd); err != nil {
524+ t.Fatal(err)
525+ }
526+ }
527+}
@@ -90,6 +90,8 @@ func newFDDisplay(t *testing.T, client *net.UnixConn) (*Display, *[]int) {
90 90
91 d := newDisplay(client)91 d := newDisplay(client)
92 fds := []int{}92 fds := []int{}
93+ d.RegisterEventSignature(7, 3, "uint,fd,")
94+ d.RegisterEventSignature(7, 4, "uint,")
93 d.objects.Store(uint32(7), &fdRecordingProxy{95 d.objects.Store(uint32(7), &fdRecordingProxy{
94 BaseProxy: BaseProxy{id: 7, context: d.context},96 BaseProxy: BaseProxy{id: 7, context: d.context},
95 fds: &fds,97 fds: &fds,
@@ -146,49 +148,19 @@ func TestDisplayFD_Single(t *testing.T) {
146 readSentinel(t, r, "single-fd-sentinel")148 readSentinel(t, r, "single-fd-sentinel")
147 }149 }
148 150
149-func TestDisplayFD_MultipleInOneControlMessage(t *testing.T) {151+func TestDisplayFD_ExtraDescriptorRejected(t *testing.T) {
150 client, peer := unixSocketPair(t)152 client, peer := unixSocketPair(t)
151- d, received := newFDDisplay(t, client)153+ d, _ := newFDDisplay(t, client)
152-154+ r, w, err := os.Pipe()
153- firstR, firstW, err := os.Pipe()
154- if err != nil {
155- t.Fatalf("pipe: %v", err)
156- }
157- defer firstR.Close()
158- secondR, secondW, err := os.Pipe()
159 if err != nil {155 if err != nil {
160- t.Fatalf("pipe: %v", err)156+ t.Fatal(err)
161 }157 }
162- defer secondR.Close()158+ defer r.Close()
163-159+ defer w.Close()
164- sendMessageWithFDs(t, peer, message(7, 3, []byte{0, 0, 0, 0}),160+ sendMessageWithFDs(t, peer, message(7, 3, nil), []int{int(w.Fd()), int(w.Fd())})
165- []int{int(firstW.Fd()), int(secondW.Fd())})161+ if err := d.Dispatch(); !errors.Is(err, ErrMalformedFrame) {
166- firstW.Close()162+ t.Fatalf("extra FD: %v", err)
167- secondW.Close()
168-
169- if err := d.Dispatch(); err != nil {
170- t.Fatalf("Dispatch: %v", err)
171- }
172-
173- // A handler consumes one descriptor per fd argument, so the test drains
174- // whatever the connection still holds, in arrival order.
175- for {
176- fd, ok := d.nextFD()
177- if !ok {
178- break
179- }
180- *received = append(*received, fd)
181- }
182-
183- if len(*received) != 2 {
184- t.Fatalf("received %d descriptors, want 2", len(*received))
185 }163 }
186-
187- writeSentinel(t, uintptr((*received)[0]), "first-pipe")
188- writeSentinel(t, uintptr((*received)[1]), "second-pipe")
189-
190- readSentinel(t, firstR, "first-pipe")
191- readSentinel(t, secondR, "second-pipe")
192 }164 }
193 165
194 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {166 func TestDisplayFD_EventWithoutFDsAfterOneWithFDs(t *testing.T) {
@@ -304,9 +276,9 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
304 }276 }
305 277
306 // A truncated control message must not leave descriptors queued.278 // A truncated control message must not leave descriptors queued.
307- d.fdMu.Lock()279+ d.recvMu.Lock()
308 pending := len(d.pendingFDs)280 pending := len(d.pendingFDs)
309- d.fdMu.Unlock()281+ d.recvMu.Unlock()
310 if pending != 0 {282 if pending != 0 {
311 t.Fatalf("%d descriptors still queued after a truncated control message", pending)283 t.Fatalf("%d descriptors still queued after a truncated control message", pending)
312 }284 }
@@ -315,6 +287,7 @@ func TestDisplayFD_TruncatedControlData(t *testing.T) {
315 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {287 func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
316 client, peer := unixSocketPair(t)288 client, peer := unixSocketPair(t)
317 d := newDisplay(client)289 d := newDisplay(client)
290+ d.RegisterEventSignature(7, 3, "uint,fd,")
318 d.objects.Store(uint32(7), &fdIgnoringProxy{291 d.objects.Store(uint32(7), &fdIgnoringProxy{
319 BaseProxy: BaseProxy{id: 7, context: d.context},292 BaseProxy: BaseProxy{id: 7, context: d.context},
320 })293 })
@@ -333,12 +306,12 @@ func TestDisplayFD_UnconsumedDescriptorClosedOnShutdown(t *testing.T) {
333 t.Fatalf("Dispatch: %v", err)306 t.Fatalf("Dispatch: %v", err)
334 }307 }
335 308
336- // While the display still holds the descriptor the pipe stays open.309+ // An ignored FD is closed at the end of the event, not at shutdown.
337- if err := r.SetReadDeadline(time.Now().Add(200 * time.Millisecond)); err != nil {310+ if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
338- t.Fatalf("SetReadDeadline: %v", err)311+ t.Fatal(err)
339 }312 }
340- if _, err := r.Read(make([]byte, 1)); !errors.Is(err, os.ErrDeadlineExceeded) {313+ if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
341- t.Fatalf("read with the display holding the descriptor = %v, want %v", err, os.ErrDeadlineExceeded)314+ t.Fatalf("ignored FD: %v", err)
342 }315 }
343 316
344 // Closing the display must release it, so the read end sees EOF.317 // Closing the display must release it, so the read end sees EOF.
@@ -421,6 +394,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
421 394
422 client, peer := rawSocketPair(t)395 client, peer := rawSocketPair(t)
423 d := newDisplay(client)396 d := newDisplay(client)
397+ d.RegisterEventSignature(7, 3, "uint,fd,")
424 d.objects.Store(uint32(7), &fdIgnoringProxy{398 d.objects.Store(uint32(7), &fdIgnoringProxy{
425 BaseProxy: BaseProxy{id: 7, context: d.context},399 BaseProxy: BaseProxy{id: 7, context: d.context},
426 })400 })
@@ -448,6 +422,7 @@ func runFDLeakIteration(t *testing.T, devNull *os.File) {
448 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {422 func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
449 client, peer := unixSocketPair(t)423 client, peer := unixSocketPair(t)
450 d := newDisplay(client)424 d := newDisplay(client)
425+ d.RegisterEventSignature(7, 3, "uint,fd,")
451 d.objects.Store(uint32(7), &fdIgnoringProxy{426 d.objects.Store(uint32(7), &fdIgnoringProxy{
452 BaseProxy: BaseProxy{id: 7, context: d.context},427 BaseProxy: BaseProxy{id: 7, context: d.context},
453 })428 })
@@ -467,9 +442,9 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
467 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)442 t.Fatalf("Dispatch error = %v, want %v", err, ErrMalformedFrame)
468 }443 }
469 444
470- d.fdMu.Lock()445+ d.recvMu.Lock()
471 pending := len(d.pendingFDs)446 pending := len(d.pendingFDs)
472- d.fdMu.Unlock()447+ d.recvMu.Unlock()
473 if pending != 0 {448 if pending != 0 {
474 t.Fatalf("%d descriptors still queued after a malformed frame", pending)449 t.Fatalf("%d descriptors still queued after a malformed frame", pending)
475 }450 }
@@ -481,3 +456,72 @@ func TestDisplayFD_ParseFailureClosesQueuedDescriptor(t *testing.T) {
481 t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF)456 t.Fatalf("read after malformed frame = %v, want %v", err, io.EOF)
482 }457 }
483 }458 }
459+
460+// Closing in a callback must not wait for the dispatch reader's state lock.
461+type closingProxy struct {
462+ BaseProxy
463+ d *Display
464+ returned bool
465+}
466+
467+func (p *closingProxy) EventSignature(op uint16) (string, bool) { return "fd,", op == 0 }
468+func (p *closingProxy) Dispatch(*Event) { p.returned = p.d.Close() == nil }
469+
470+func TestCloseInsideHandlerClosesQueuedFDs(t *testing.T) {
471+ client, peer := unixSocketPair(t)
472+ d := newDisplay(client)
473+ proxy := &closingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, d: d}
474+ d.context.Register(proxy)
475+ r, w, err := os.Pipe()
476+ if err != nil {
477+ t.Fatal(err)
478+ }
479+ defer r.Close()
480+ extra, xw, err := os.Pipe()
481+ if err != nil {
482+ t.Fatal(err)
483+ }
484+ defer extra.Close()
485+ // Two frames in one read; second FD remains queued while first handler runs.
486+ payload := append(message(7, 0, nil), message(7, 0, nil)...)
487+ sendMessageWithFDs(t, peer, payload, []int{int(w.Fd()), int(xw.Fd())})
488+ w.Close()
489+ xw.Close()
490+ if err := d.Dispatch(); err != nil {
491+ t.Fatal(err)
492+ }
493+ if !proxy.returned {
494+ t.Fatal("Close did not return from handler")
495+ }
496+ for _, reader := range []*os.File{r, extra} {
497+ if err := reader.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
498+ t.Fatal(err)
499+ }
500+ if _, err := reader.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
501+ t.Fatalf("fd not closed: %v", err)
502+ }
503+ }
504+}
505+
506+func TestParseFDsKeepsRightsBeforeMalformedHeader(t *testing.T) {
507+ r, w, err := os.Pipe()
508+ if err != nil {
509+ t.Fatal(err)
510+ }
511+ defer r.Close()
512+ defer w.Close()
513+ rights := unix.UnixRights(int(w.Fd()))
514+ oob := append(rights, make([]byte, unix.SizeofCmsghdr)...)
515+ fds, err := parseFDs(oob)
516+ if err == nil || len(fds) != 1 {
517+ t.Fatalf("fds=%v err=%v", fds, err)
518+ }
519+ if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != nil {
520+ t.Fatal(err)
521+ }
522+ for _, fd := range fds {
523+ if err := unix.Close(fd); err != nil {
524+ t.Fatal(err)
525+ }
526+ }
527+}
A fd_zero_test.go +14 -0
@@ -0,0 +1,14 @@
1+package wlturbo
2+
3+import "testing"
4+
5+func TestOwnedFDZero(t *testing.T) {
6+ fd := &OwnedFD{fd: 0}
7+ n, e := fd.Take()
8+ if e != nil || n != 0 {
9+ t.Fatalf("Take fd zero = %d %v", n, e)
10+ }
11+ if e := fd.Close(); e != nil {
12+ t.Fatal(e)
13+ }
14+}
@@ -0,0 +1,14 @@
1+package wlturbo
2+
3+import "testing"
4+
5+func TestOwnedFDZero(t *testing.T) {
6+ fd := &OwnedFD{fd: 0}
7+ n, e := fd.Take()
8+ if e != nil || n != 0 {
9+ t.Fatalf("Take fd zero = %d %v", n, e)
10+ }
11+ if e := fd.Close(); e != nil {
12+ t.Fatal(e)
13+ }
14+}
A internal/scanner/scanner.go +731 -0
@@ -0,0 +1,731 @@
1+// Package scanner generates WLTurbo-backed Go bindings from Wayland protocol
2+// XML files.
3+//
4+// The generated code owns protocol semantics only: it embeds wl.BaseProxy,
5+// allocates and registers child objects through the wl.Context, sends requests
6+// through that context, and dispatches typed events to registered handlers.
7+// Message framing, descriptor passing and connection lifetime stay in the
8+// transport, so a generated binding never opens a socket or writes a header.
9+package scanner
10+
11+import (
12+ "bytes"
13+ "encoding/xml"
14+ "fmt"
15+ "go/format"
16+ "io"
17+ "os"
18+ "path/filepath"
19+ "sort"
20+ "strings"
21+ "text/template"
22+)
23+
24+// TransportImport is the import path generated bindings use for the Wayland
25+// transport.
26+const TransportImport = "github.com/bnema/wlturbo/wl"
27+
28+// Protocol represents a Wayland protocol specification
29+type Protocol struct {
30+ XMLName xml.Name `xml:"protocol"`
31+ Name string `xml:"name,attr"`
32+ Copyright string `xml:"copyright"`
33+ Description *Description `xml:"description"`
34+ Interfaces []Interface `xml:"interface"`
35+}
36+
37+// Interface represents a Wayland interface in the protocol
38+type Interface struct {
39+ Name string `xml:"name,attr"`
40+ Version int `xml:"version,attr"`
41+ Description *Description `xml:"description"`
42+ Requests []Request `xml:"request"`
43+ Events []Event `xml:"event"`
44+ Enums []Enum `xml:"enum"`
45+}
46+
47+// Request represents a client-to-server message
48+type Request struct {
49+ Name string `xml:"name,attr"`
50+ Type string `xml:"type,attr"` // "destructor" or empty
51+ Since int `xml:"since,attr"`
52+ Description *Description `xml:"description"`
53+ Args []Arg `xml:"arg"`
54+}
55+
56+// Event represents a server-to-client message
57+type Event struct {
58+ Type string `xml:"type,attr"`
59+ Name string `xml:"name,attr"`
60+ Since int `xml:"since,attr"`
61+ Description *Description `xml:"description"`
62+ Args []Arg `xml:"arg"`
63+}
64+
65+// Arg represents a message argument
66+type Arg struct {
67+ Name string `xml:"name,attr"`
68+ Type string `xml:"type,attr"`
69+ Summary string `xml:"summary,attr"`
70+ Interface string `xml:"interface,attr"`
71+ AllowNull bool `xml:"allow-null,attr"`
72+ Enum string `xml:"enum,attr"`
73+}
74+
75+// Enum represents an enumeration type
76+type Enum struct {
77+ Name string `xml:"name,attr"`
78+ Since int `xml:"since,attr"`
79+ Bitfield bool `xml:"bitfield,attr"`
80+ Description *Description `xml:"description"`
81+ Entries []Entry `xml:"entry"`
82+}
83+
84+// Entry represents an enum value
85+type Entry struct {
86+ Name string `xml:"name,attr"`
87+ Value string `xml:"value,attr"`
88+ Summary string `xml:"summary,attr"`
89+ Since int `xml:"since,attr"`
90+}
91+
92+// Description represents documentation
93+type Description struct {
94+ Summary string `xml:"summary,attr"`
95+ Text string `xml:",chardata"`
96+}
97+
98+// Scanner generates Go bindings from Wayland protocol XML
99+type Scanner struct {
100+ protocol *Protocol
101+ source string
102+ // CrossPackage maps interface names to Go package import paths. Local wins.
103+ CrossPackage map[string]string
104+}
105+
106+// NewScanner creates a new protocol scanner
107+func NewScanner() *Scanner {
108+ return &Scanner{}
109+}
110+
111+// ParseXML parses a Wayland protocol XML file
112+func (s *Scanner) ParseXML(path string) error {
113+ file, err := os.Open(path)
114+ if err != nil {
115+ return fmt.Errorf("failed to open XML file: %w", err)
116+ }
117+ defer func() { _ = file.Close() }()
118+
119+ data, err := io.ReadAll(file)
120+ if err != nil {
121+ return fmt.Errorf("failed to read XML file: %w", err)
122+ }
123+
124+ var protocol Protocol
125+ if err := xml.Unmarshal(data, &protocol); err != nil {
126+ return fmt.Errorf("failed to parse XML: %w", err)
127+ }
128+
129+ s.protocol = &protocol
130+ // Only the base name is recorded: generated output must not depend on the
131+ // path the XML happened to be read from.
132+ s.source = filepath.Base(path)
133+ return nil
134+}
135+
136+// Generate creates Go source code for the parsed protocol
137+func (s *Scanner) Generate(packageName string) ([]byte, error) {
138+ if s.protocol == nil {
139+ return nil, fmt.Errorf("no protocol parsed")
140+ }
141+
142+ data, err := s.prepareTemplateData(packageName)
143+ if err != nil {
144+ return nil, err
145+ }
146+
147+ var buf bytes.Buffer
148+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
149+ if err := tmpl.Execute(&buf, data); err != nil {
150+ return nil, fmt.Errorf("failed to execute template: %w", err)
151+ }
152+
153+ formatted, err := format.Source(buf.Bytes())
154+ if err != nil {
155+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
156+ }
157+ return formatted, nil
158+}
159+
160+// templateData holds data for template generation
161+type templateData struct {
162+ Package string
163+ Protocol string
164+ Source string
165+ ImportBlock string
166+ Constants []constantData
167+ Interfaces []interfaceData
168+}
169+
170+type interfaceData struct {
171+ Name string
172+ GoName string
173+ Version int
174+ HasEvents bool
175+ SignatureCases []signatureCase
176+ Requests []requestData
177+ Events []eventData
178+}
179+
180+type signatureCase struct {
181+ Opcode int
182+ Signature string
183+}
184+type requestData struct {
185+ Name string
186+ GoName string
187+ Doc string
188+ Opcode int
189+ Destructor bool
190+ Params string
191+ Results string
192+ ArgPreparations []string
193+ ArgExprs []string
194+ CreatesChild bool
195+ ChildVar string
196+ ChildType string
197+ ChildFactory string
198+ ErrorReturn string
199+ FDArgs []string
200+ HasFDs bool
201+ SendCall string
202+}
203+
204+// argSlot is one positional argument of a request, in declaration order. A
205+// new_id slot is filled in once the child object has been created.
206+type argSlot struct {
207+ newID bool
208+ expr string
209+}
210+
211+type eventData struct {
212+ HasFD bool
213+ ServerDestroy bool
214+ FDNames []string
215+ Name string
216+ GoName string
217+ Opcode int
218+ HandlerField string
219+ HandlerAccessor string
220+ HandlerParams string
221+ HandlerArgs string
222+ DecodeLines []string
223+}
224+
225+type constantData struct {
226+ Name string
227+ Value string
228+ Type string
229+ Enum string
230+ Entry string
231+}
232+
233+// Only display and registry have handwritten bootstrap bindings.
234+var bootstrapTypes = map[string]bool{
235+ "Display": true,
236+ "Registry": true,
237+}
238+
239+// goKeywords are avoided as parameter names because generated identifiers
240+// must not collide with the language.
241+var goKeywords = map[string]bool{
242+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
243+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
244+ "func": true, "go": true, "goto": true, "if": true, "import": true,
245+ "interface": true, "map": true, "package": true, "range": true, "return": true,
246+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
247+}
248+
249+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
250+ data := templateData{
251+ Package: packageName,
252+ Protocol: s.protocol.Name,
253+ Source: s.source,
254+ }
255+
256+ imports := []string{TransportImport}
257+ usedCross := make(map[string]bool)
258+ for _, candidate := range s.protocol.Interfaces {
259+ for _, req := range candidate.Requests {
260+ for _, arg := range req.Args {
261+ if arg.Type == "object" || arg.Type == "new_id" {
262+ usedCross[arg.Interface] = true
263+ }
264+ }
265+ }
266+ for _, ev := range candidate.Events {
267+ for _, arg := range ev.Args {
268+ if arg.Type == "object" || arg.Type == "new_id" {
269+ usedCross[arg.Interface] = true
270+ }
271+ }
272+ }
273+ }
274+ for iface, path := range s.CrossPackage {
275+ local := false
276+ for _, candidate := range s.protocol.Interfaces {
277+ if candidate.Name == iface {
278+ local = true
279+ break
280+ }
281+ }
282+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
283+ continue
284+ }
285+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
286+ imports = append(imports, alias+" "+path)
287+ }
288+ sort.Strings(imports)
289+ stdImports := []string{}
290+ needsSync := false
291+
292+ // Enum constants are package-level declarations, so two interfaces that
293+ // declare the same enum entry (for example xdg_surface.error.invalid_size
294+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
295+ // occurs in more than one interface are qualified with their interface
296+ // name below; unique names keep their historical, unprefixed form.
297+ enumEntryCounts := map[string]int{}
298+ for _, iface := range s.protocol.Interfaces {
299+ for _, enum := range iface.Enums {
300+ for _, entry := range enum.Entries {
301+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
302+ }
303+ }
304+ }
305+
306+ for _, iface := range s.protocol.Interfaces {
307+ if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
308+ continue
309+ }
310+ ifaceData, err := s.processInterface(iface)
311+ if err != nil {
312+ return data, err
313+ }
314+ data.Interfaces = append(data.Interfaces, ifaceData)
315+ if len(iface.Events) > 0 {
316+ needsSync = true
317+ }
318+
319+ for _, enum := range iface.Enums {
320+ for _, entry := range enum.Entries {
321+ name := constantName(enum.Name, entry.Name)
322+ if enumEntryCounts[name] > 1 {
323+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
324+ }
325+ // Enum values are emitted untyped: the same enum travels as a uint
326+ // in one protocol and as an int in another, and an untyped constant
327+ // assigns to either without casts at the call site.
328+ data.Constants = append(data.Constants, constantData{
329+ Name: name,
330+ Value: entry.Value,
331+ Enum: enum.Name,
332+ Entry: entry.Name,
333+ })
334+ }
335+ }
336+ }
337+
338+ // Handlers are registered and dispatched from different goroutines, so an
339+ // interface with events needs a mutex.
340+ if needsSync {
341+ stdImports = append(stdImports, "sync")
342+ }
343+
344+ var block strings.Builder
345+ block.WriteString("import (\n")
346+ for _, imp := range stdImports {
347+ fmt.Fprintf(&block, "\t%q\n", imp)
348+ }
349+ if len(stdImports) > 0 && len(imports) > 0 {
350+ block.WriteString("\n")
351+ }
352+ for _, imp := range imports {
353+ if strings.Contains(imp, " ") {
354+ parts := strings.SplitN(imp, " ", 2)
355+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
356+ } else {
357+ fmt.Fprintf(&block, "\t%q\n", imp)
358+ }
359+ }
360+ block.WriteString(")")
361+ data.ImportBlock = block.String()
362+
363+ return data, nil
364+}
365+
366+func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
367+ data := interfaceData{
368+ Name: iface.Name,
369+ GoName: s.toGoName(iface.Name),
370+ Version: iface.Version,
371+ HasEvents: len(iface.Events) > 0,
372+ }
373+
374+ for i, req := range iface.Requests {
375+ request, err := s.processRequest(iface, req, i)
376+ if err != nil {
377+ return data, err
378+ }
379+ data.Requests = append(data.Requests, request)
380+ }
381+
382+ for i, event := range iface.Events {
383+ var sig strings.Builder
384+ for _, arg := range event.Args {
385+ sig.WriteString(arg.Type)
386+ sig.WriteByte(',')
387+ }
388+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
389+ eventData, err := s.processEvent(event, i)
390+ if iface.Name == "wl_callback" && event.Name == "done" {
391+ eventData.ServerDestroy = true
392+ }
393+ if err != nil {
394+ return data, err
395+ }
396+ data.Events = append(data.Events, eventData)
397+ }
398+
399+ return data, nil
400+}
401+
402+func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
403+ data := requestData{
404+ Name: req.Name,
405+ GoName: s.toGoName(req.Name),
406+ Doc: s.formatDescription(req.Description),
407+ Opcode: opcode,
408+ Destructor: req.Type == "destructor",
409+ ChildVar: "child",
410+ }
411+
412+ var (
413+ params []string
414+ argSlots []argSlot
415+ argExprs []string
416+ fdExprs []string
417+ newIDArg *Arg
418+ )
419+
420+ for i, arg := range req.Args {
421+ if arg.Type == "new_id" {
422+ if newIDArg != nil {
423+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
424+ }
425+ newIDArg = &arg
426+ // The child object is created later, but it keeps its declared
427+ // position: the wire order is the argument order.
428+ argSlots = append(argSlots, argSlot{newID: true})
429+ continue
430+ }
431+
432+ name := s.paramName(arg.Name, i)
433+ goType, err := s.goTypeForArg(arg)
434+ if err != nil {
435+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
436+ }
437+
438+ expr := name
439+ switch arg.Type {
440+ case "object":
441+ // Objects are pointer types, and travel as wl.Object so a nil
442+ // pointer is never sent as a non-nil interface holding a typed nil.
443+ params = append(params, name+" *"+goType)
444+ holder := fmt.Sprintf("arg%d", i)
445+ data.ArgPreparations = append(data.ArgPreparations,
446+ fmt.Sprintf("var %s wl.Object", holder),
447+ fmt.Sprintf("if %s != nil {", name),
448+ fmt.Sprintf("\t%s = %s", holder, name),
449+ "}")
450+ expr = holder
451+
452+ case "fd":
453+ // Descriptors travel out of band: the body carries no value for
454+ // them, and the descriptor list is attached to the request.
455+ params = append(params, name+" int")
456+ fdExprs = append(fdExprs, name)
457+ data.FDArgs = append(data.FDArgs, name)
458+ expr = "uintptr(" + name + ")"
459+
460+ default:
461+ params = append(params, name+" "+goType)
462+ }
463+
464+ argSlots = append(argSlots, argSlot{expr: expr})
465+ }
466+
467+ if newIDArg != nil {
468+ childType, err := s.goTypeForArg(*newIDArg)
469+ if err != nil {
470+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
471+ }
472+ data.CreatesChild = true
473+ data.ChildType = childType
474+ if strings.Contains(childType, ".") {
475+ data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
476+ }
477+ }
478+
479+ for _, slot := range argSlots {
480+ if slot.newID {
481+ argExprs = append(argExprs, data.ChildVar)
482+ continue
483+ }
484+ argExprs = append(argExprs, slot.expr)
485+ }
486+
487+ data.Params = strings.Join(params, ", ")
488+ data.ArgExprs = argExprs
489+ args := ""
490+ if len(argExprs) > 0 {
491+ args = ", " + strings.Join(argExprs, ", ")
492+ }
493+ // Destructors claim the proxy atomically before sending so a concurrent
494+ // second destroy cannot reach the wire.
495+ send := "SendRequest"
496+ if data.Destructor {
497+ send = "SendDestructor"
498+ }
499+ if len(fdExprs) > 0 {
500+ data.HasFDs = true
501+ data.SendCall = fmt.Sprintf("%sWithFDs(o, %d, []int{%s}%s)", send, opcode, strings.Join(fdExprs, ", "), args)
502+ } else {
503+ data.SendCall = fmt.Sprintf("%s(o, %d%s)", send, opcode, args)
504+ }
505+ if data.CreatesChild {
506+ data.Results = "(*" + data.ChildType + ", error)"
507+ data.ErrorReturn = "nil, err"
508+ } else {
509+ data.Results = "error"
510+ data.ErrorReturn = "err"
511+ }
512+
513+ return data, nil
514+}
515+
516+func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
517+ goName := s.toGoName(event.Name)
518+ data := eventData{
519+ ServerDestroy: event.Type == "destructor",
520+ Name: event.Name,
521+ GoName: goName,
522+ Opcode: opcode,
523+ HandlerField: "on" + goName,
524+ HandlerAccessor: "handlersFor" + goName,
525+ }
526+
527+ var params, args []string
528+
529+ for i, arg := range event.Args {
530+ name := s.paramName(arg.Name, i)
531+
532+ switch arg.Type {
533+ case "new_id":
534+ childType, err := s.goTypeForArg(arg)
535+ if err != nil {
536+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
537+ }
538+ child := name + "Object"
539+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
540+ if strings.Contains(childType, ".") {
541+ factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
542+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
543+ } else {
544+ data.DecodeLines = append(data.DecodeLines,
545+ fmt.Sprintf("%s := &%s{}", child, childType),
546+ fmt.Sprintf("%s.SetContext(o.Context())", child),
547+ )
548+ }
549+ data.DecodeLines = append(data.DecodeLines,
550+ fmt.Sprintf("%s.SetID(%sID)", child, name),
551+ fmt.Sprintf("o.Context().Register(%s)", child),
552+ )
553+ params = append(params, child+" *"+childType)
554+ args = append(args, child)
555+
556+ case "object":
557+ // Object references are reported as raw object IDs: the generated
558+ // layer does not own a registry of foreign proxies.
559+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
560+ params = append(params, name+"ID uint32")
561+ args = append(args, name+"ID")
562+
563+ default:
564+ if arg.Type == "fd" {
565+ data.HasFD = true
566+ data.FDNames = append(data.FDNames, name)
567+ }
568+ goType, decoder, err := s.scalarDecoder(arg)
569+ if err != nil {
570+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
571+ }
572+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
573+ params = append(params, name+" "+goType)
574+ args = append(args, name)
575+ }
576+ }
577+
578+ data.HandlerParams = strings.Join(params, ", ")
579+ data.HandlerArgs = strings.Join(args, ", ")
580+ return data, nil
581+}
582+
583+// scalarDecoder maps a simple argument to its Go type and event decoder.
584+func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
585+ switch arg.Type {
586+ case "int":
587+ return "int32", "Int32", nil
588+ case "uint":
589+ return "uint32", "Uint32", nil
590+ case "fixed":
591+ return "wl.Fixed", "Fixed", nil
592+ case "string":
593+ return "string", "String", nil
594+ case "array":
595+ return "[]byte", "Array", nil
596+ case "fd":
597+ return "*wl.OwnedFD", "FD", nil
598+ }
599+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
600+}
601+
602+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
603+// interfaces map to generated types, explicit external references to their
604+// mapped package, and bootstrap display/registry to transport wrappers.
605+func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
606+ switch arg.Type {
607+ case "int":
608+ return "int32", nil
609+ case "uint":
610+ return "uint32", nil
611+ case "fixed":
612+ return "wl.Fixed", nil
613+ case "string":
614+ return "string", nil
615+ case "array":
616+ return "[]byte", nil
617+ case "fd":
618+ return "int", nil
619+ case "object", "new_id":
620+ return s.goTypeForInterface(arg.Interface)
621+ }
622+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
623+}
624+
625+func (s *Scanner) goTypeForInterface(iface string) (string, error) {
626+ if iface == "" {
627+ return "wl.BaseProxy", nil
628+ }
629+ // The XML package owns its own definitions even when the name starts wl_.
630+ for _, local := range s.protocol.Interfaces {
631+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
632+ return s.toGoName(iface), nil
633+ }
634+ }
635+ if bootstrapTypes[s.toGoName(iface)] {
636+ return "wl." + s.toGoName(iface), nil
637+ }
638+ if _, ok := s.CrossPackage[iface]; ok {
639+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
640+ }
641+ return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
642+}
643+
644+// paramName turns a protocol argument name into a Go parameter name.
645+func (s *Scanner) paramName(name string, index int) string {
646+ if name == "" {
647+ return fmt.Sprintf("arg%d", index)
648+ }
649+
650+ parts := strings.Split(name, "_")
651+ for i, part := range parts {
652+ if part == "" {
653+ continue
654+ }
655+ if i == 0 {
656+ parts[i] = strings.ToLower(part[:1]) + part[1:]
657+ continue
658+ }
659+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
660+ }
661+ result := strings.Join(parts, "")
662+ if result == "" {
663+ return fmt.Sprintf("arg%d", index)
664+ }
665+ if goKeywords[result] {
666+ return result + "Arg"
667+ }
668+ return result
669+}
670+
671+func (s *Scanner) toGoName(name string) string {
672+ name = strings.TrimPrefix(name, "zwlr_")
673+ name = strings.TrimPrefix(name, "zwp_")
674+ name = strings.TrimPrefix(name, "wl_")
675+ name = strings.TrimSuffix(name, "_v1")
676+ name = strings.TrimSuffix(name, "_v2")
677+
678+ parts := strings.Split(name, "_")
679+ for i, part := range parts {
680+ if part != "" {
681+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
682+ }
683+ }
684+ return strings.Join(parts, "")
685+}
686+
687+// constantName renders an enum entry as the unprefixed constant name.
688+func constantName(enum, entry string) string {
689+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
690+}
691+
692+// qualifiedConstantName prefixes an enum entry constant with its interface
693+// name. It is used for entries that several interfaces declare, which would
694+// otherwise collide in the generated package.
695+func qualifiedConstantName(iface, enum, entry string) string {
696+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
697+}
698+
699+// joinName upper-cases the underscore-separated parts of a protocol name.
700+func joinName(value string) []string {
701+ parts := strings.Split(value, "_")
702+ out := make([]string, 0, len(parts))
703+ for _, part := range parts {
704+ if part != "" {
705+ out = append(out, strings.ToUpper(part))
706+ }
707+ }
708+ return out
709+}
710+
711+// formatDescription renders an argument or request description as a single
712+// documentation line.
713+func (s *Scanner) formatDescription(desc *Description) string {
714+ if desc == nil {
715+ return ""
716+ }
717+
718+ text := strings.TrimSpace(desc.Text)
719+ if text == "" {
720+ return desc.Summary
721+ }
722+
723+ lines := strings.Split(text, "\n")
724+ formatted := make([]string, 0, len(lines))
725+ for _, line := range lines {
726+ if line = strings.TrimSpace(line); line != "" {
727+ formatted = append(formatted, line)
728+ }
729+ }
730+ return strings.Join(formatted, " ")
731+}
@@ -0,0 +1,731 @@
1+// Package scanner generates WLTurbo-backed Go bindings from Wayland protocol
2+// XML files.
3+//
4+// The generated code owns protocol semantics only: it embeds wl.BaseProxy,
5+// allocates and registers child objects through the wl.Context, sends requests
6+// through that context, and dispatches typed events to registered handlers.
7+// Message framing, descriptor passing and connection lifetime stay in the
8+// transport, so a generated binding never opens a socket or writes a header.
9+package scanner
10+
11+import (
12+ "bytes"
13+ "encoding/xml"
14+ "fmt"
15+ "go/format"
16+ "io"
17+ "os"
18+ "path/filepath"
19+ "sort"
20+ "strings"
21+ "text/template"
22+)
23+
24+// TransportImport is the import path generated bindings use for the Wayland
25+// transport.
26+const TransportImport = "github.com/bnema/wlturbo/wl"
27+
28+// Protocol represents a Wayland protocol specification
29+type Protocol struct {
30+ XMLName xml.Name `xml:"protocol"`
31+ Name string `xml:"name,attr"`
32+ Copyright string `xml:"copyright"`
33+ Description *Description `xml:"description"`
34+ Interfaces []Interface `xml:"interface"`
35+}
36+
37+// Interface represents a Wayland interface in the protocol
38+type Interface struct {
39+ Name string `xml:"name,attr"`
40+ Version int `xml:"version,attr"`
41+ Description *Description `xml:"description"`
42+ Requests []Request `xml:"request"`
43+ Events []Event `xml:"event"`
44+ Enums []Enum `xml:"enum"`
45+}
46+
47+// Request represents a client-to-server message
48+type Request struct {
49+ Name string `xml:"name,attr"`
50+ Type string `xml:"type,attr"` // "destructor" or empty
51+ Since int `xml:"since,attr"`
52+ Description *Description `xml:"description"`
53+ Args []Arg `xml:"arg"`
54+}
55+
56+// Event represents a server-to-client message
57+type Event struct {
58+ Type string `xml:"type,attr"`
59+ Name string `xml:"name,attr"`
60+ Since int `xml:"since,attr"`
61+ Description *Description `xml:"description"`
62+ Args []Arg `xml:"arg"`
63+}
64+
65+// Arg represents a message argument
66+type Arg struct {
67+ Name string `xml:"name,attr"`
68+ Type string `xml:"type,attr"`
69+ Summary string `xml:"summary,attr"`
70+ Interface string `xml:"interface,attr"`
71+ AllowNull bool `xml:"allow-null,attr"`
72+ Enum string `xml:"enum,attr"`
73+}
74+
75+// Enum represents an enumeration type
76+type Enum struct {
77+ Name string `xml:"name,attr"`
78+ Since int `xml:"since,attr"`
79+ Bitfield bool `xml:"bitfield,attr"`
80+ Description *Description `xml:"description"`
81+ Entries []Entry `xml:"entry"`
82+}
83+
84+// Entry represents an enum value
85+type Entry struct {
86+ Name string `xml:"name,attr"`
87+ Value string `xml:"value,attr"`
88+ Summary string `xml:"summary,attr"`
89+ Since int `xml:"since,attr"`
90+}
91+
92+// Description represents documentation
93+type Description struct {
94+ Summary string `xml:"summary,attr"`
95+ Text string `xml:",chardata"`
96+}
97+
98+// Scanner generates Go bindings from Wayland protocol XML
99+type Scanner struct {
100+ protocol *Protocol
101+ source string
102+ // CrossPackage maps interface names to Go package import paths. Local wins.
103+ CrossPackage map[string]string
104+}
105+
106+// NewScanner creates a new protocol scanner
107+func NewScanner() *Scanner {
108+ return &Scanner{}
109+}
110+
111+// ParseXML parses a Wayland protocol XML file
112+func (s *Scanner) ParseXML(path string) error {
113+ file, err := os.Open(path)
114+ if err != nil {
115+ return fmt.Errorf("failed to open XML file: %w", err)
116+ }
117+ defer func() { _ = file.Close() }()
118+
119+ data, err := io.ReadAll(file)
120+ if err != nil {
121+ return fmt.Errorf("failed to read XML file: %w", err)
122+ }
123+
124+ var protocol Protocol
125+ if err := xml.Unmarshal(data, &protocol); err != nil {
126+ return fmt.Errorf("failed to parse XML: %w", err)
127+ }
128+
129+ s.protocol = &protocol
130+ // Only the base name is recorded: generated output must not depend on the
131+ // path the XML happened to be read from.
132+ s.source = filepath.Base(path)
133+ return nil
134+}
135+
136+// Generate creates Go source code for the parsed protocol
137+func (s *Scanner) Generate(packageName string) ([]byte, error) {
138+ if s.protocol == nil {
139+ return nil, fmt.Errorf("no protocol parsed")
140+ }
141+
142+ data, err := s.prepareTemplateData(packageName)
143+ if err != nil {
144+ return nil, err
145+ }
146+
147+ var buf bytes.Buffer
148+ tmpl := template.Must(template.New("protocol").Parse(protocolTemplate))
149+ if err := tmpl.Execute(&buf, data); err != nil {
150+ return nil, fmt.Errorf("failed to execute template: %w", err)
151+ }
152+
153+ formatted, err := format.Source(buf.Bytes())
154+ if err != nil {
155+ return buf.Bytes(), fmt.Errorf("failed to format generated code: %w", err)
156+ }
157+ return formatted, nil
158+}
159+
160+// templateData holds data for template generation
161+type templateData struct {
162+ Package string
163+ Protocol string
164+ Source string
165+ ImportBlock string
166+ Constants []constantData
167+ Interfaces []interfaceData
168+}
169+
170+type interfaceData struct {
171+ Name string
172+ GoName string
173+ Version int
174+ HasEvents bool
175+ SignatureCases []signatureCase
176+ Requests []requestData
177+ Events []eventData
178+}
179+
180+type signatureCase struct {
181+ Opcode int
182+ Signature string
183+}
184+type requestData struct {
185+ Name string
186+ GoName string
187+ Doc string
188+ Opcode int
189+ Destructor bool
190+ Params string
191+ Results string
192+ ArgPreparations []string
193+ ArgExprs []string
194+ CreatesChild bool
195+ ChildVar string
196+ ChildType string
197+ ChildFactory string
198+ ErrorReturn string
199+ FDArgs []string
200+ HasFDs bool
201+ SendCall string
202+}
203+
204+// argSlot is one positional argument of a request, in declaration order. A
205+// new_id slot is filled in once the child object has been created.
206+type argSlot struct {
207+ newID bool
208+ expr string
209+}
210+
211+type eventData struct {
212+ HasFD bool
213+ ServerDestroy bool
214+ FDNames []string
215+ Name string
216+ GoName string
217+ Opcode int
218+ HandlerField string
219+ HandlerAccessor string
220+ HandlerParams string
221+ HandlerArgs string
222+ DecodeLines []string
223+}
224+
225+type constantData struct {
226+ Name string
227+ Value string
228+ Type string
229+ Enum string
230+ Entry string
231+}
232+
233+// Only display and registry have handwritten bootstrap bindings.
234+var bootstrapTypes = map[string]bool{
235+ "Display": true,
236+ "Registry": true,
237+}
238+
239+// goKeywords are avoided as parameter names because generated identifiers
240+// must not collide with the language.
241+var goKeywords = map[string]bool{
242+ "break": true, "case": true, "chan": true, "const": true, "continue": true,
243+ "default": true, "defer": true, "else": true, "fallthrough": true, "for": true,
244+ "func": true, "go": true, "goto": true, "if": true, "import": true,
245+ "interface": true, "map": true, "package": true, "range": true, "return": true,
246+ "select": true, "struct": true, "switch": true, "type": true, "var": true,
247+}
248+
249+func (s *Scanner) prepareTemplateData(packageName string) (templateData, error) {
250+ data := templateData{
251+ Package: packageName,
252+ Protocol: s.protocol.Name,
253+ Source: s.source,
254+ }
255+
256+ imports := []string{TransportImport}
257+ usedCross := make(map[string]bool)
258+ for _, candidate := range s.protocol.Interfaces {
259+ for _, req := range candidate.Requests {
260+ for _, arg := range req.Args {
261+ if arg.Type == "object" || arg.Type == "new_id" {
262+ usedCross[arg.Interface] = true
263+ }
264+ }
265+ }
266+ for _, ev := range candidate.Events {
267+ for _, arg := range ev.Args {
268+ if arg.Type == "object" || arg.Type == "new_id" {
269+ usedCross[arg.Interface] = true
270+ }
271+ }
272+ }
273+ }
274+ for iface, path := range s.CrossPackage {
275+ local := false
276+ for _, candidate := range s.protocol.Interfaces {
277+ if candidate.Name == iface {
278+ local = true
279+ break
280+ }
281+ }
282+ if local || !usedCross[iface] || bootstrapTypes[s.toGoName(iface)] {
283+ continue
284+ }
285+ alias := "cross_" + strings.ReplaceAll(iface, "-", "_")
286+ imports = append(imports, alias+" "+path)
287+ }
288+ sort.Strings(imports)
289+ stdImports := []string{}
290+ needsSync := false
291+
292+ // Enum constants are package-level declarations, so two interfaces that
293+ // declare the same enum entry (for example xdg_surface.error.invalid_size
294+ // and xdg_toplevel.error.invalid_size) would collide. Entries whose name
295+ // occurs in more than one interface are qualified with their interface
296+ // name below; unique names keep their historical, unprefixed form.
297+ enumEntryCounts := map[string]int{}
298+ for _, iface := range s.protocol.Interfaces {
299+ for _, enum := range iface.Enums {
300+ for _, entry := range enum.Entries {
301+ enumEntryCounts[constantName(enum.Name, entry.Name)]++
302+ }
303+ }
304+ }
305+
306+ for _, iface := range s.protocol.Interfaces {
307+ if packageName == "core" && (iface.Name == "wl_display" || iface.Name == "wl_registry") {
308+ continue
309+ }
310+ ifaceData, err := s.processInterface(iface)
311+ if err != nil {
312+ return data, err
313+ }
314+ data.Interfaces = append(data.Interfaces, ifaceData)
315+ if len(iface.Events) > 0 {
316+ needsSync = true
317+ }
318+
319+ for _, enum := range iface.Enums {
320+ for _, entry := range enum.Entries {
321+ name := constantName(enum.Name, entry.Name)
322+ if enumEntryCounts[name] > 1 {
323+ name = qualifiedConstantName(iface.Name, enum.Name, entry.Name)
324+ }
325+ // Enum values are emitted untyped: the same enum travels as a uint
326+ // in one protocol and as an int in another, and an untyped constant
327+ // assigns to either without casts at the call site.
328+ data.Constants = append(data.Constants, constantData{
329+ Name: name,
330+ Value: entry.Value,
331+ Enum: enum.Name,
332+ Entry: entry.Name,
333+ })
334+ }
335+ }
336+ }
337+
338+ // Handlers are registered and dispatched from different goroutines, so an
339+ // interface with events needs a mutex.
340+ if needsSync {
341+ stdImports = append(stdImports, "sync")
342+ }
343+
344+ var block strings.Builder
345+ block.WriteString("import (\n")
346+ for _, imp := range stdImports {
347+ fmt.Fprintf(&block, "\t%q\n", imp)
348+ }
349+ if len(stdImports) > 0 && len(imports) > 0 {
350+ block.WriteString("\n")
351+ }
352+ for _, imp := range imports {
353+ if strings.Contains(imp, " ") {
354+ parts := strings.SplitN(imp, " ", 2)
355+ fmt.Fprintf(&block, "\t%s %q\n", parts[0], parts[1])
356+ } else {
357+ fmt.Fprintf(&block, "\t%q\n", imp)
358+ }
359+ }
360+ block.WriteString(")")
361+ data.ImportBlock = block.String()
362+
363+ return data, nil
364+}
365+
366+func (s *Scanner) processInterface(iface Interface) (interfaceData, error) {
367+ data := interfaceData{
368+ Name: iface.Name,
369+ GoName: s.toGoName(iface.Name),
370+ Version: iface.Version,
371+ HasEvents: len(iface.Events) > 0,
372+ }
373+
374+ for i, req := range iface.Requests {
375+ request, err := s.processRequest(iface, req, i)
376+ if err != nil {
377+ return data, err
378+ }
379+ data.Requests = append(data.Requests, request)
380+ }
381+
382+ for i, event := range iface.Events {
383+ var sig strings.Builder
384+ for _, arg := range event.Args {
385+ sig.WriteString(arg.Type)
386+ sig.WriteByte(',')
387+ }
388+ data.SignatureCases = append(data.SignatureCases, signatureCase{i, sig.String()})
389+ eventData, err := s.processEvent(event, i)
390+ if iface.Name == "wl_callback" && event.Name == "done" {
391+ eventData.ServerDestroy = true
392+ }
393+ if err != nil {
394+ return data, err
395+ }
396+ data.Events = append(data.Events, eventData)
397+ }
398+
399+ return data, nil
400+}
401+
402+func (s *Scanner) processRequest(iface Interface, req Request, opcode int) (requestData, error) {
403+ data := requestData{
404+ Name: req.Name,
405+ GoName: s.toGoName(req.Name),
406+ Doc: s.formatDescription(req.Description),
407+ Opcode: opcode,
408+ Destructor: req.Type == "destructor",
409+ ChildVar: "child",
410+ }
411+
412+ var (
413+ params []string
414+ argSlots []argSlot
415+ argExprs []string
416+ fdExprs []string
417+ newIDArg *Arg
418+ )
419+
420+ for i, arg := range req.Args {
421+ if arg.Type == "new_id" {
422+ if newIDArg != nil {
423+ return data, fmt.Errorf("%s.%s: more than one new_id argument is not supported", iface.Name, req.Name)
424+ }
425+ newIDArg = &arg
426+ // The child object is created later, but it keeps its declared
427+ // position: the wire order is the argument order.
428+ argSlots = append(argSlots, argSlot{newID: true})
429+ continue
430+ }
431+
432+ name := s.paramName(arg.Name, i)
433+ goType, err := s.goTypeForArg(arg)
434+ if err != nil {
435+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
436+ }
437+
438+ expr := name
439+ switch arg.Type {
440+ case "object":
441+ // Objects are pointer types, and travel as wl.Object so a nil
442+ // pointer is never sent as a non-nil interface holding a typed nil.
443+ params = append(params, name+" *"+goType)
444+ holder := fmt.Sprintf("arg%d", i)
445+ data.ArgPreparations = append(data.ArgPreparations,
446+ fmt.Sprintf("var %s wl.Object", holder),
447+ fmt.Sprintf("if %s != nil {", name),
448+ fmt.Sprintf("\t%s = %s", holder, name),
449+ "}")
450+ expr = holder
451+
452+ case "fd":
453+ // Descriptors travel out of band: the body carries no value for
454+ // them, and the descriptor list is attached to the request.
455+ params = append(params, name+" int")
456+ fdExprs = append(fdExprs, name)
457+ data.FDArgs = append(data.FDArgs, name)
458+ expr = "uintptr(" + name + ")"
459+
460+ default:
461+ params = append(params, name+" "+goType)
462+ }
463+
464+ argSlots = append(argSlots, argSlot{expr: expr})
465+ }
466+
467+ if newIDArg != nil {
468+ childType, err := s.goTypeForArg(*newIDArg)
469+ if err != nil {
470+ return data, fmt.Errorf("%s.%s: %w", iface.Name, req.Name, err)
471+ }
472+ data.CreatesChild = true
473+ data.ChildType = childType
474+ if strings.Contains(childType, ".") {
475+ data.ChildFactory = childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(newIDArg.Interface)
476+ }
477+ }
478+
479+ for _, slot := range argSlots {
480+ if slot.newID {
481+ argExprs = append(argExprs, data.ChildVar)
482+ continue
483+ }
484+ argExprs = append(argExprs, slot.expr)
485+ }
486+
487+ data.Params = strings.Join(params, ", ")
488+ data.ArgExprs = argExprs
489+ args := ""
490+ if len(argExprs) > 0 {
491+ args = ", " + strings.Join(argExprs, ", ")
492+ }
493+ // Destructors claim the proxy atomically before sending so a concurrent
494+ // second destroy cannot reach the wire.
495+ send := "SendRequest"
496+ if data.Destructor {
497+ send = "SendDestructor"
498+ }
499+ if len(fdExprs) > 0 {
500+ data.HasFDs = true
501+ data.SendCall = fmt.Sprintf("%sWithFDs(o, %d, []int{%s}%s)", send, opcode, strings.Join(fdExprs, ", "), args)
502+ } else {
503+ data.SendCall = fmt.Sprintf("%s(o, %d%s)", send, opcode, args)
504+ }
505+ if data.CreatesChild {
506+ data.Results = "(*" + data.ChildType + ", error)"
507+ data.ErrorReturn = "nil, err"
508+ } else {
509+ data.Results = "error"
510+ data.ErrorReturn = "err"
511+ }
512+
513+ return data, nil
514+}
515+
516+func (s *Scanner) processEvent(event Event, opcode int) (eventData, error) {
517+ goName := s.toGoName(event.Name)
518+ data := eventData{
519+ ServerDestroy: event.Type == "destructor",
520+ Name: event.Name,
521+ GoName: goName,
522+ Opcode: opcode,
523+ HandlerField: "on" + goName,
524+ HandlerAccessor: "handlersFor" + goName,
525+ }
526+
527+ var params, args []string
528+
529+ for i, arg := range event.Args {
530+ name := s.paramName(arg.Name, i)
531+
532+ switch arg.Type {
533+ case "new_id":
534+ childType, err := s.goTypeForArg(arg)
535+ if err != nil {
536+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
537+ }
538+ child := name + "Object"
539+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
540+ if strings.Contains(childType, ".") {
541+ factory := childType[:strings.LastIndex(childType, ".")+1] + "New" + s.toGoName(arg.Interface)
542+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := %s(o.Context())", child, factory))
543+ } else {
544+ data.DecodeLines = append(data.DecodeLines,
545+ fmt.Sprintf("%s := &%s{}", child, childType),
546+ fmt.Sprintf("%s.SetContext(o.Context())", child),
547+ )
548+ }
549+ data.DecodeLines = append(data.DecodeLines,
550+ fmt.Sprintf("%s.SetID(%sID)", child, name),
551+ fmt.Sprintf("o.Context().Register(%s)", child),
552+ )
553+ params = append(params, child+" *"+childType)
554+ args = append(args, child)
555+
556+ case "object":
557+ // Object references are reported as raw object IDs: the generated
558+ // layer does not own a registry of foreign proxies.
559+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%sID := event.Uint32()", name))
560+ params = append(params, name+"ID uint32")
561+ args = append(args, name+"ID")
562+
563+ default:
564+ if arg.Type == "fd" {
565+ data.HasFD = true
566+ data.FDNames = append(data.FDNames, name)
567+ }
568+ goType, decoder, err := s.scalarDecoder(arg)
569+ if err != nil {
570+ return data, fmt.Errorf("%s.%s: %w", event.Name, arg.Name, err)
571+ }
572+ data.DecodeLines = append(data.DecodeLines, fmt.Sprintf("%s := event.%s()", name, decoder))
573+ params = append(params, name+" "+goType)
574+ args = append(args, name)
575+ }
576+ }
577+
578+ data.HandlerParams = strings.Join(params, ", ")
579+ data.HandlerArgs = strings.Join(args, ", ")
580+ return data, nil
581+}
582+
583+// scalarDecoder maps a simple argument to its Go type and event decoder.
584+func (s *Scanner) scalarDecoder(arg Arg) (goType, decoder string, err error) {
585+ switch arg.Type {
586+ case "int":
587+ return "int32", "Int32", nil
588+ case "uint":
589+ return "uint32", "Uint32", nil
590+ case "fixed":
591+ return "wl.Fixed", "Fixed", nil
592+ case "string":
593+ return "string", "String", nil
594+ case "array":
595+ return "[]byte", "Array", nil
596+ case "fd":
597+ return "*wl.OwnedFD", "FD", nil
598+ }
599+ return "", "", fmt.Errorf("unsupported event argument type %q", arg.Type)
600+}
601+
602+// goTypeForArg returns the Go type of an object-like argument. Protocol-local
603+// interfaces map to generated types, explicit external references to their
604+// mapped package, and bootstrap display/registry to transport wrappers.
605+func (s *Scanner) goTypeForArg(arg Arg) (string, error) {
606+ switch arg.Type {
607+ case "int":
608+ return "int32", nil
609+ case "uint":
610+ return "uint32", nil
611+ case "fixed":
612+ return "wl.Fixed", nil
613+ case "string":
614+ return "string", nil
615+ case "array":
616+ return "[]byte", nil
617+ case "fd":
618+ return "int", nil
619+ case "object", "new_id":
620+ return s.goTypeForInterface(arg.Interface)
621+ }
622+ return "", fmt.Errorf("unsupported argument type %q", arg.Type)
623+}
624+
625+func (s *Scanner) goTypeForInterface(iface string) (string, error) {
626+ if iface == "" {
627+ return "wl.BaseProxy", nil
628+ }
629+ // The XML package owns its own definitions even when the name starts wl_.
630+ for _, local := range s.protocol.Interfaces {
631+ if local.Name == iface && iface != "wl_display" && iface != "wl_registry" {
632+ return s.toGoName(iface), nil
633+ }
634+ }
635+ if bootstrapTypes[s.toGoName(iface)] {
636+ return "wl." + s.toGoName(iface), nil
637+ }
638+ if _, ok := s.CrossPackage[iface]; ok {
639+ return "cross_" + strings.ReplaceAll(iface, "-", "_") + "." + s.toGoName(iface), nil
640+ }
641+ return "", fmt.Errorf("unresolved interface %q: add CrossPackage mapping", iface)
642+}
643+
644+// paramName turns a protocol argument name into a Go parameter name.
645+func (s *Scanner) paramName(name string, index int) string {
646+ if name == "" {
647+ return fmt.Sprintf("arg%d", index)
648+ }
649+
650+ parts := strings.Split(name, "_")
651+ for i, part := range parts {
652+ if part == "" {
653+ continue
654+ }
655+ if i == 0 {
656+ parts[i] = strings.ToLower(part[:1]) + part[1:]
657+ continue
658+ }
659+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
660+ }
661+ result := strings.Join(parts, "")
662+ if result == "" {
663+ return fmt.Sprintf("arg%d", index)
664+ }
665+ if goKeywords[result] {
666+ return result + "Arg"
667+ }
668+ return result
669+}
670+
671+func (s *Scanner) toGoName(name string) string {
672+ name = strings.TrimPrefix(name, "zwlr_")
673+ name = strings.TrimPrefix(name, "zwp_")
674+ name = strings.TrimPrefix(name, "wl_")
675+ name = strings.TrimSuffix(name, "_v1")
676+ name = strings.TrimSuffix(name, "_v2")
677+
678+ parts := strings.Split(name, "_")
679+ for i, part := range parts {
680+ if part != "" {
681+ parts[i] = strings.ToUpper(part[:1]) + part[1:]
682+ }
683+ }
684+ return strings.Join(parts, "")
685+}
686+
687+// constantName renders an enum entry as the unprefixed constant name.
688+func constantName(enum, entry string) string {
689+ return strings.Join(append(joinName(enum), joinName(entry)...), "_")
690+}
691+
692+// qualifiedConstantName prefixes an enum entry constant with its interface
693+// name. It is used for entries that several interfaces declare, which would
694+// otherwise collide in the generated package.
695+func qualifiedConstantName(iface, enum, entry string) string {
696+ return strings.Join(append(joinName(iface), constantName(enum, entry)), "_")
697+}
698+
699+// joinName upper-cases the underscore-separated parts of a protocol name.
700+func joinName(value string) []string {
701+ parts := strings.Split(value, "_")
702+ out := make([]string, 0, len(parts))
703+ for _, part := range parts {
704+ if part != "" {
705+ out = append(out, strings.ToUpper(part))
706+ }
707+ }
708+ return out
709+}
710+
711+// formatDescription renders an argument or request description as a single
712+// documentation line.
713+func (s *Scanner) formatDescription(desc *Description) string {
714+ if desc == nil {
715+ return ""
716+ }
717+
718+ text := strings.TrimSpace(desc.Text)
719+ if text == "" {
720+ return desc.Summary
721+ }
722+
723+ lines := strings.Split(text, "\n")
724+ formatted := make([]string, 0, len(lines))
725+ for _, line := range lines {
726+ if line = strings.TrimSpace(line); line != "" {
727+ formatted = append(formatted, line)
728+ }
729+ }
730+ return strings.Join(formatted, " ")
731+}
A internal/scanner/scanner_test.go +163 -0
@@ -0,0 +1,163 @@
1+package scanner
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestResolution(t *testing.T) {
12+ p := filepath.Join(t.TempDir(), "fixture.xml")
13+ xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>`
14+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
15+ t.Fatal(err)
16+ }
17+ s := NewScanner()
18+ if err := s.ParseXML(p); err != nil {
19+ t.Fatal(err)
20+ }
21+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"}
22+ b, err := s.Generate("fixture")
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ text := string(b)
27+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
28+ t.Fatalf("wrong resolution: %s", text)
29+ }
30+}
31+func TestCoreGeneration(t *testing.T) {
32+ s := NewScanner()
33+ if err := s.ParseXML("../../protocol/wayland.xml"); err != nil {
34+ t.Fatal(err)
35+ }
36+ b, err := s.Generate("core")
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
41+ if !strings.Contains(string(b), want) {
42+ t.Errorf("missing %s", want)
43+ }
44+ }
45+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
46+ t.Fatal("bootstrap generated twice")
47+ }
48+}
49+
50+// P3 uses wl_buffer as both an event-created child and a request-created child.
51+// Verify the generated code uses the public constructor instead of embedding
52+// an inaccessible BaseProxy context field across package boundaries.
53+func TestExternalChildFactory(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "external.xml")
55+ xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
56+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
57+ t.Fatal(err)
58+ }
59+ s := NewScanner()
60+ if err := s.ParseXML(path); err != nil {
61+ t.Fatal(err)
62+ }
63+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
64+ generated, err := s.Generate("ext")
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} {
69+ if !strings.Contains(string(generated), part) {
70+ t.Errorf("generated code missing %q", part)
71+ }
72+ }
73+ if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") {
74+ t.Fatal("foreign child was constructed without its package factory")
75+ }
76+}
77+
78+func TestUnresolvedExternalInterface(t *testing.T) {
79+ path := filepath.Join(t.TempDir(), "extension.xml")
80+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>`
81+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
82+ t.Fatal(err)
83+ }
84+ s := NewScanner()
85+ if err := s.ParseXML(path); err != nil {
86+ t.Fatal(err)
87+ }
88+ if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") {
89+ t.Fatalf("unmapped external reference: %v", err)
90+ }
91+}
92+
93+func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) {
94+ goBin, err := exec.LookPath("go")
95+ if err != nil {
96+ t.Skip("go unavailable: generated extension compilation requires go")
97+ }
98+ root, err := filepath.Abs("../..")
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ dir := t.TempDir()
103+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
104+ path := filepath.Join(dir, "ext.xml")
105+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
106+ t.Fatal(err)
107+ }
108+ s := NewScanner()
109+ if err := s.ParseXML(path); err != nil {
110+ t.Fatal(err)
111+ }
112+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
113+ generated, err := s.Generate("ext")
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil {
118+ t.Fatal(err)
119+ }
120+ mod := "module example.com/ext\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n"
121+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0600); err != nil {
122+ t.Fatal(err)
123+ }
124+ test := `package ext
125+import (
126+ "encoding/binary"
127+ "net"
128+ "os"
129+ "testing"
130+ "github.com/bnema/wlturbo"
131+ "github.com/bnema/wlturbo/protocol/core"
132+ "golang.org/x/sys/unix"
133+)
134+func TestTypedChild(t *testing.T) {
135+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) }
136+ a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b")
137+ c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close()
138+ p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close()
139+ d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close()
140+ parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent)
141+ var child *core.Buffer
142+ parent.OnChild(func(b *core.Buffer) { child = b })
143+ frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001)
144+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
145+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
146+ if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) }
147+ called := false; child.OnRelease(func(){ called = true })
148+ frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16)
149+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
150+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
151+ if !called { t.Fatal("typed child did not dispatch release") }
152+}
153+`
154+ if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil {
155+ t.Fatal(err)
156+ }
157+ cmd := exec.Command(goBin, "test", "./...")
158+ cmd.Dir = dir
159+ cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
160+ if out, err := cmd.CombinedOutput(); err != nil {
161+ t.Fatalf("generated extension test: %v\n%s", err, out)
162+ }
163+}
@@ -0,0 +1,163 @@
1+package scanner
2+
3+import (
4+ "os"
5+ "os/exec"
6+ "path/filepath"
7+ "strings"
8+ "testing"
9+)
10+
11+func TestResolution(t *testing.T) {
12+ p := filepath.Join(t.TempDir(), "fixture.xml")
13+ xml := `<protocol name="sample"><interface name="wl_surface" version="1"><event name="done"><arg name="value" type="uint"/></event></interface><interface name="custom_thing" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/><arg name="output" type="object" interface="wl_output"/></request></interface></protocol>`
14+ if err := os.WriteFile(p, []byte(xml), 0600); err != nil {
15+ t.Fatal(err)
16+ }
17+ s := NewScanner()
18+ if err := s.ParseXML(p); err != nil {
19+ t.Fatal(err)
20+ }
21+ s.CrossPackage = map[string]string{"wl_surface": "github.com/bnema/wlturbo/protocol/core", "wl_output": "github.com/bnema/wlturbo/protocol/core"}
22+ b, err := s.Generate("fixture")
23+ if err != nil {
24+ t.Fatal(err)
25+ }
26+ text := string(b)
27+ if !strings.Contains(text, "(*Surface, error)") || !strings.Contains(text, "output *cross_wl_output.Output") || strings.Contains(text, "cross_wl_surface") {
28+ t.Fatalf("wrong resolution: %s", text)
29+ }
30+}
31+func TestCoreGeneration(t *testing.T) {
32+ s := NewScanner()
33+ if err := s.ParseXML("../../protocol/wayland.xml"); err != nil {
34+ t.Fatal(err)
35+ }
36+ b, err := s.Generate("core")
37+ if err != nil {
38+ t.Fatal(err)
39+ }
40+ for _, want := range []string{"type Keyboard struct", "type Subsurface struct", "type Buffer struct", "type Callback struct", "return \"uint,fd,uint,\", true", "child := &Keyboard{}"} {
41+ if !strings.Contains(string(b), want) {
42+ t.Errorf("missing %s", want)
43+ }
44+ }
45+ if strings.Contains(string(b), "type Display struct") || strings.Contains(string(b), "type Registry struct") {
46+ t.Fatal("bootstrap generated twice")
47+ }
48+}
49+
50+// P3 uses wl_buffer as both an event-created child and a request-created child.
51+// Verify the generated code uses the public constructor instead of embedding
52+// an inaccessible BaseProxy context field across package boundaries.
53+func TestExternalChildFactory(t *testing.T) {
54+ path := filepath.Join(t.TempDir(), "external.xml")
55+ xml := `<protocol name="fixture"><interface name="ext_params" version="1"><request name="create"><arg name="id" type="new_id" interface="wl_buffer"/></request><event name="created"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
56+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
57+ t.Fatal(err)
58+ }
59+ s := NewScanner()
60+ if err := s.ParseXML(path); err != nil {
61+ t.Fatal(err)
62+ }
63+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
64+ generated, err := s.Generate("ext")
65+ if err != nil {
66+ t.Fatal(err)
67+ }
68+ for _, part := range []string{"cross_wl_buffer.NewBuffer(o.Context())", "(*cross_wl_buffer.Buffer, error)", "idObject.SetID(idID)"} {
69+ if !strings.Contains(string(generated), part) {
70+ t.Errorf("generated code missing %q", part)
71+ }
72+ }
73+ if strings.Contains(string(generated), "&cross_wl_buffer.Buffer{}") {
74+ t.Fatal("foreign child was constructed without its package factory")
75+ }
76+}
77+
78+func TestUnresolvedExternalInterface(t *testing.T) {
79+ path := filepath.Join(t.TempDir(), "extension.xml")
80+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><request name="make"><arg name="id" type="new_id" interface="wl_surface"/></request></interface></protocol>`
81+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
82+ t.Fatal(err)
83+ }
84+ s := NewScanner()
85+ if err := s.ParseXML(path); err != nil {
86+ t.Fatal(err)
87+ }
88+ if _, err := s.Generate("ext"); err == nil || !strings.Contains(err.Error(), "wl_surface") {
89+ t.Fatalf("unmapped external reference: %v", err)
90+ }
91+}
92+
93+func TestMappedExtensionCompilesAndDispatchesChild(t *testing.T) {
94+ goBin, err := exec.LookPath("go")
95+ if err != nil {
96+ t.Skip("go unavailable: generated extension compilation requires go")
97+ }
98+ root, err := filepath.Abs("../..")
99+ if err != nil {
100+ t.Fatal(err)
101+ }
102+ dir := t.TempDir()
103+ xml := `<protocol name="ext"><interface name="ext_parent" version="1"><event name="child"><arg name="id" type="new_id" interface="wl_buffer"/></event></interface></protocol>`
104+ path := filepath.Join(dir, "ext.xml")
105+ if err := os.WriteFile(path, []byte(xml), 0600); err != nil {
106+ t.Fatal(err)
107+ }
108+ s := NewScanner()
109+ if err := s.ParseXML(path); err != nil {
110+ t.Fatal(err)
111+ }
112+ s.CrossPackage = map[string]string{"wl_buffer": "github.com/bnema/wlturbo/protocol/core"}
113+ generated, err := s.Generate("ext")
114+ if err != nil {
115+ t.Fatal(err)
116+ }
117+ if err := os.WriteFile(filepath.Join(dir, "ext.go"), generated, 0600); err != nil {
118+ t.Fatal(err)
119+ }
120+ mod := "module example.com/ext\n\ngo 1.27\n\nrequire github.com/bnema/wlturbo v0.0.0\nreplace github.com/bnema/wlturbo => " + root + "\n"
121+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0600); err != nil {
122+ t.Fatal(err)
123+ }
124+ test := `package ext
125+import (
126+ "encoding/binary"
127+ "net"
128+ "os"
129+ "testing"
130+ "github.com/bnema/wlturbo"
131+ "github.com/bnema/wlturbo/protocol/core"
132+ "golang.org/x/sys/unix"
133+)
134+func TestTypedChild(t *testing.T) {
135+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0); if err != nil { t.Fatal(err) }
136+ a, b := os.NewFile(uintptr(f[0]), "a"), os.NewFile(uintptr(f[1]), "b")
137+ c, err := net.FileConn(a); a.Close(); if err != nil { t.Fatal(err) }; defer c.Close()
138+ p, err := net.FileConn(b); b.Close(); if err != nil { t.Fatal(err) }; defer p.Close()
139+ d, err := wlturbo.ConnectFromConn(c); if err != nil { t.Fatal(err) }; defer d.Close()
140+ parent := NewExtParent(d.Context()); parent.SetID(d.AllocateID()); d.Context().Register(parent)
141+ var child *core.Buffer
142+ parent.OnChild(func(b *core.Buffer) { child = b })
143+ frame := make([]byte, 12); binary.LittleEndian.PutUint32(frame, parent.ID()); binary.LittleEndian.PutUint32(frame[4:], 12<<16); binary.LittleEndian.PutUint32(frame[8:], 0xff000001)
144+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
145+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
146+ if child == nil || child.ID() != 0xff000001 { t.Fatalf("child: %v", child) }
147+ called := false; child.OnRelease(func(){ called = true })
148+ frame = frame[:8]; binary.LittleEndian.PutUint32(frame, child.ID()); binary.LittleEndian.PutUint32(frame[4:], 8<<16)
149+ if _, err := p.Write(frame); err != nil { t.Fatal(err) }
150+ if err := d.Dispatch(); err != nil { t.Fatal(err) }
151+ if !called { t.Fatal("typed child did not dispatch release") }
152+}
153+`
154+ if err := os.WriteFile(filepath.Join(dir, "ext_test.go"), []byte(test), 0600); err != nil {
155+ t.Fatal(err)
156+ }
157+ cmd := exec.Command(goBin, "test", "./...")
158+ cmd.Dir = dir
159+ cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod")
160+ if out, err := cmd.CombinedOutput(); err != nil {
161+ t.Fatalf("generated extension test: %v\n%s", err, out)
162+ }
163+}
A internal/scanner/template.go +142 -0
@@ -0,0 +1,142 @@
1+package scanner
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, allocate and register child objects
6+// through the wl.Context, send requests through that context, and dispatch
7+// typed events to registered handlers. Nothing in the generated code opens a
8+// socket or frames messages itself: that is the transport's job.
9+const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT.
10+// Protocol: {{.Protocol}}
11+// Source: {{.Source}}
12+
13+package {{.Package}}
14+
15+{{.ImportBlock}}
16+
17+{{range .Constants}}
18+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
19+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
20+{{- end}}
21+{{range .Interfaces}}
22+{{- $iface := .}}
23+
24+// {{$iface.GoName}}Interface is the protocol interface name.
25+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
26+
27+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
28+type {{$iface.GoName}} struct {
29+ wl.BaseProxy
30+{{- if $iface.HasEvents}}
31+ // handlersMu guards the handler slices: a client may register or replace
32+ // handlers while another goroutine dispatches events.
33+ handlersMu sync.Mutex
34+{{- end}}
35+{{- range $iface.Events}}
36+ {{.HandlerField}} []func({{.HandlerParams}})
37+{{- end}}
38+}
39+
40+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
41+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
42+ obj := &{{$iface.GoName}}{}
43+ obj.SetContext(ctx)
44+ ctx.Register(obj)
45+ return obj
46+}
47+{{- range $iface.Requests}}
48+
49+{{- if .Doc}}
50+// {{.Doc}}
51+{{- end}}
52+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
53+if err := o.Context().CheckProxy(o); err != nil { return {{.ErrorReturn}} }
54+{{- range .ArgPreparations}}
55+ {{.}}
56+{{- end}}
57+{{- if .CreatesChild}}
58+{{- if .ChildFactory}}
59+ {{.ChildVar}} := {{.ChildFactory}}(o.Context())
60+{{- else}}
61+ {{.ChildVar}} := &{{.ChildType}}{}
62+ {{.ChildVar}}.SetContext(o.Context())
63+{{- end}}
64+ {{.ChildVar}}.SetID(o.Context().AllocateID())
65+ o.Context().Register({{.ChildVar}})
66+{{- end}}
67+ if err := o.Context().{{.SendCall}}; err != nil {
68+{{- if .CreatesChild}}
69+ o.Context().Unregister({{.ChildVar}})
70+{{- end}}
71+ return {{.ErrorReturn}}
72+ }
73+{{- range .FDArgs}}
74+ _ = wl.CloseSentFD({{.}})
75+{{- end}}
76+{{- if .CreatesChild}}
77+ return {{.ChildVar}}, nil
78+{{- else}}
79+ return nil
80+{{- end}}
81+}
82+{{- end}}
83+{{- range $iface.Events}}
84+
85+// On{{.GoName}} registers a handler for the {{.Name}} event.
86+//
87+// Handlers are appended, so a second call adds another handler rather than
88+// replacing the first. Registration is safe while another goroutine dispatches.
89+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
90+ if handler == nil { return }
91+ o.handlersMu.Lock()
92+ defer o.handlersMu.Unlock()
93+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
94+}
95+
96+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
97+// under the lock so a handler may register another handler while events are
98+// being dispatched.
99+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
100+ o.handlersMu.Lock()
101+ defer o.handlersMu.Unlock()
102+ return o.{{.HandlerField}}
103+}
104+{{- end}}
105+
106+// EventSignature returns the declared event argument types in wire order.
107+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
108+ switch opcode {
109+ {{- range $iface.SignatureCases}}
110+ case {{.Opcode}}: return "{{.Signature}}", true
111+ {{- end}}
112+ default: return "", false
113+ }
114+}
115+
116+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
117+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
118+ switch event.Opcode {
119+{{- range $iface.Events}}
120+ case {{.Opcode}}: // {{.Name}}
121+{{- range .DecodeLines}}
122+ {{.}}
123+{{- end}}
124+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
125+ {{end}}{{end}}
126+ for i, handler := range o.{{.HandlerAccessor}}() {
127+ {{- if .HasFD}}
128+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
129+ {{- else}}
130+ _ = i
131+ {{- end}}
132+ handler({{.HandlerArgs}})
133+ }
134+{{- if .ServerDestroy}}
135+ o.Context().Unregister(o)
136+{{- end}}
137+{{- end}}
138+ default:
139+ }
140+}
141+{{end}}
142+`
@@ -0,0 +1,142 @@
1+package scanner
2+
3+// protocolTemplate is the template for generating WLTurbo-backed Go bindings.
4+//
5+// Generated objects embed wl.BaseProxy, allocate and register child objects
6+// through the wl.Context, send requests through that context, and dispatch
7+// typed events to registered handlers. Nothing in the generated code opens a
8+// socket or frames messages itself: that is the transport's job.
9+const protocolTemplate = `// Code generated by wlturbo-scanner. DO NOT EDIT.
10+// Protocol: {{.Protocol}}
11+// Source: {{.Source}}
12+
13+package {{.Package}}
14+
15+{{.ImportBlock}}
16+
17+{{range .Constants}}
18+// {{.Name}} is the {{.Enum}} {{.Entry}} value.
19+const {{.Name}}{{if .Type}} {{.Type}}{{end}} = {{.Value}}
20+{{- end}}
21+{{range .Interfaces}}
22+{{- $iface := .}}
23+
24+// {{$iface.GoName}}Interface is the protocol interface name.
25+const {{$iface.GoName}}Interface = "{{$iface.Name}}"
26+
27+// {{$iface.GoName}} is the generated binding for {{$iface.Name}} version {{$iface.Version}}.
28+type {{$iface.GoName}} struct {
29+ wl.BaseProxy
30+{{- if $iface.HasEvents}}
31+ // handlersMu guards the handler slices: a client may register or replace
32+ // handlers while another goroutine dispatches events.
33+ handlersMu sync.Mutex
34+{{- end}}
35+{{- range $iface.Events}}
36+ {{.HandlerField}} []func({{.HandlerParams}})
37+{{- end}}
38+}
39+
40+// New{{$iface.GoName}} returns an unbound {{$iface.GoName}}.
41+func New{{$iface.GoName}}(ctx *wl.Context) *{{$iface.GoName}} {
42+ obj := &{{$iface.GoName}}{}
43+ obj.SetContext(ctx)
44+ ctx.Register(obj)
45+ return obj
46+}
47+{{- range $iface.Requests}}
48+
49+{{- if .Doc}}
50+// {{.Doc}}
51+{{- end}}
52+func (o *{{$iface.GoName}}) {{.GoName}}({{.Params}}) {{.Results}} {
53+if err := o.Context().CheckProxy(o); err != nil { return {{.ErrorReturn}} }
54+{{- range .ArgPreparations}}
55+ {{.}}
56+{{- end}}
57+{{- if .CreatesChild}}
58+{{- if .ChildFactory}}
59+ {{.ChildVar}} := {{.ChildFactory}}(o.Context())
60+{{- else}}
61+ {{.ChildVar}} := &{{.ChildType}}{}
62+ {{.ChildVar}}.SetContext(o.Context())
63+{{- end}}
64+ {{.ChildVar}}.SetID(o.Context().AllocateID())
65+ o.Context().Register({{.ChildVar}})
66+{{- end}}
67+ if err := o.Context().{{.SendCall}}; err != nil {
68+{{- if .CreatesChild}}
69+ o.Context().Unregister({{.ChildVar}})
70+{{- end}}
71+ return {{.ErrorReturn}}
72+ }
73+{{- range .FDArgs}}
74+ _ = wl.CloseSentFD({{.}})
75+{{- end}}
76+{{- if .CreatesChild}}
77+ return {{.ChildVar}}, nil
78+{{- else}}
79+ return nil
80+{{- end}}
81+}
82+{{- end}}
83+{{- range $iface.Events}}
84+
85+// On{{.GoName}} registers a handler for the {{.Name}} event.
86+//
87+// Handlers are appended, so a second call adds another handler rather than
88+// replacing the first. Registration is safe while another goroutine dispatches.
89+func (o *{{$iface.GoName}}) On{{.GoName}}(handler func({{.HandlerParams}})) {
90+ if handler == nil { return }
91+ o.handlersMu.Lock()
92+ defer o.handlersMu.Unlock()
93+ o.{{.HandlerField}} = append(o.{{.HandlerField}}, handler)
94+}
95+
96+// {{.HandlerAccessor}} returns the handlers registered for {{.Name}}, taken
97+// under the lock so a handler may register another handler while events are
98+// being dispatched.
99+func (o *{{$iface.GoName}}) {{.HandlerAccessor}}() []func({{.HandlerParams}}) {
100+ o.handlersMu.Lock()
101+ defer o.handlersMu.Unlock()
102+ return o.{{.HandlerField}}
103+}
104+{{- end}}
105+
106+// EventSignature returns the declared event argument types in wire order.
107+func (o *{{$iface.GoName}}) EventSignature(opcode uint16) (string, bool) {
108+ switch opcode {
109+ {{- range $iface.SignatureCases}}
110+ case {{.Opcode}}: return "{{.Signature}}", true
111+ {{- end}}
112+ default: return "", false
113+ }
114+}
115+
116+// Dispatch decodes one event on {{$iface.Name}} and calls the registered handlers.
117+func (o *{{$iface.GoName}}) Dispatch(event *wl.Event) {
118+ switch event.Opcode {
119+{{- range $iface.Events}}
120+ case {{.Opcode}}: // {{.Name}}
121+{{- range .DecodeLines}}
122+ {{.}}
123+{{- end}}
124+ {{if .HasFD}}{{range .FDNames}}owned{{.}} := {{.}}; defer owned{{.}}.Close()
125+ {{end}}{{end}}
126+ for i, handler := range o.{{.HandlerAccessor}}() {
127+ {{- if .HasFD}}
128+ if i > 0 { {{range .FDNames}}{{.}} = nil; {{end}} }
129+ {{- else}}
130+ _ = i
131+ {{- end}}
132+ handler({{.HandlerArgs}})
133+ }
134+{{- if .ServerDestroy}}
135+ o.Context().Unregister(o)
136+{{- end}}
137+{{- end}}
138+ default:
139+ }
140+}
141+{{end}}
142+`
A internal/scanner/testdata/protocol_fixture.xml +112 -0
@@ -0,0 +1,112 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlvision_fixture">
3+ <copyright>
4+ Synthetic protocol used to test the wlvision protocol generator. It exists
5+ only in this repository's tests.
6+ </copyright>
7+
8+ <interface name="zwlr_fixture_manager_v1" version="2">
9+ <description summary="fixture manager">
10+ Exercises new_id requests, object references, fixed point numbers,
11+ strings, arrays, file descriptors and destructors.
12+ </description>
13+
14+ <request name="create_thing">
15+ <description summary="create a fixture object"/>
16+ <arg name="seat" type="object" interface="wl_seat"/>
17+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
18+ </request>
19+
20+ <request name="set_ratio">
21+ <arg name="ratio" type="fixed"/>
22+ </request>
23+
24+ <request name="set_label">
25+ <arg name="label" type="string"/>
26+ </request>
27+
28+ <request name="set_region">
29+ <arg name="region" type="object" interface="wl_region"/>
30+ </request>
31+
32+ <request name="set_buffer">
33+ <description summary="reference an interface without a typed wrapper"/>
34+ <arg name="buffer" type="object" interface="wl_buffer"/>
35+ </request>
36+
37+ <request name="set_optional_output">
38+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
39+ </request>
40+
41+ <request name="attach_raw">
42+ <arg name="data" type="array"/>
43+ </request>
44+
45+ <request name="pass_fd">
46+ <arg name="fd" type="fd"/>
47+ </request>
48+
49+ <request name="destroy" type="destructor">
50+ <description summary="destroy the manager"/>
51+ </request>
52+
53+ <request name="create_labeled_thing">
54+ <description summary="new_id argument that is not the last one"/>
55+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
56+ <arg name="label" type="string"/>
57+ <arg name="ratio" type="fixed"/>
58+ </request>
59+ </interface>
60+
61+ <interface name="zwlr_fixture_thing_v1" version="1">
62+ <description summary="fixture object"/>
63+
64+ <enum name="fixture_state">
65+ <entry name="idle" value="0"/>
66+ <entry name="busy" value="1"/>
67+ </enum>
68+
69+ <request name="set_scale">
70+ <arg name="scale" type="fixed"/>
71+ </request>
72+
73+ <request name="destroy" type="destructor"/>
74+
75+ <event name="ready">
76+ <arg name="serial" type="uint"/>
77+ </event>
78+
79+ <event name="label_changed">
80+ <arg name="label" type="string"/>
81+ </event>
82+
83+ <event name="geometry">
84+ <arg name="x" type="int"/>
85+ <arg name="y" type="int"/>
86+ <arg name="width" type="uint"/>
87+ <arg name="height" type="uint"/>
88+ </event>
89+
90+ <event name="mode">
91+ <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/>
92+ </event>
93+
94+ <event name="peer">
95+ <arg name="surface" type="object" interface="wl_surface"/>
96+ </event>
97+
98+ <event name="scaled">
99+ <arg name="scale" type="fixed"/>
100+ </event>
101+
102+ <event name="blob">
103+ <arg name="data" type="array"/>
104+ </event>
105+ </interface>
106+
107+ <interface name="zwlr_fixture_mode_v1" version="1">
108+ <description summary="fixture mode object"/>
109+
110+ <request name="destroy" type="destructor"/>
111+ </interface>
112+</protocol>
@@ -0,0 +1,112 @@
1+<?xml version="1.0" encoding="UTF-8"?>
2+<protocol name="wlvision_fixture">
3+ <copyright>
4+ Synthetic protocol used to test the wlvision protocol generator. It exists
5+ only in this repository's tests.
6+ </copyright>
7+
8+ <interface name="zwlr_fixture_manager_v1" version="2">
9+ <description summary="fixture manager">
10+ Exercises new_id requests, object references, fixed point numbers,
11+ strings, arrays, file descriptors and destructors.
12+ </description>
13+
14+ <request name="create_thing">
15+ <description summary="create a fixture object"/>
16+ <arg name="seat" type="object" interface="wl_seat"/>
17+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
18+ </request>
19+
20+ <request name="set_ratio">
21+ <arg name="ratio" type="fixed"/>
22+ </request>
23+
24+ <request name="set_label">
25+ <arg name="label" type="string"/>
26+ </request>
27+
28+ <request name="set_region">
29+ <arg name="region" type="object" interface="wl_region"/>
30+ </request>
31+
32+ <request name="set_buffer">
33+ <description summary="reference an interface without a typed wrapper"/>
34+ <arg name="buffer" type="object" interface="wl_buffer"/>
35+ </request>
36+
37+ <request name="set_optional_output">
38+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
39+ </request>
40+
41+ <request name="attach_raw">
42+ <arg name="data" type="array"/>
43+ </request>
44+
45+ <request name="pass_fd">
46+ <arg name="fd" type="fd"/>
47+ </request>
48+
49+ <request name="destroy" type="destructor">
50+ <description summary="destroy the manager"/>
51+ </request>
52+
53+ <request name="create_labeled_thing">
54+ <description summary="new_id argument that is not the last one"/>
55+ <arg name="id" type="new_id" interface="zwlr_fixture_thing_v1"/>
56+ <arg name="label" type="string"/>
57+ <arg name="ratio" type="fixed"/>
58+ </request>
59+ </interface>
60+
61+ <interface name="zwlr_fixture_thing_v1" version="1">
62+ <description summary="fixture object"/>
63+
64+ <enum name="fixture_state">
65+ <entry name="idle" value="0"/>
66+ <entry name="busy" value="1"/>
67+ </enum>
68+
69+ <request name="set_scale">
70+ <arg name="scale" type="fixed"/>
71+ </request>
72+
73+ <request name="destroy" type="destructor"/>
74+
75+ <event name="ready">
76+ <arg name="serial" type="uint"/>
77+ </event>
78+
79+ <event name="label_changed">
80+ <arg name="label" type="string"/>
81+ </event>
82+
83+ <event name="geometry">
84+ <arg name="x" type="int"/>
85+ <arg name="y" type="int"/>
86+ <arg name="width" type="uint"/>
87+ <arg name="height" type="uint"/>
88+ </event>
89+
90+ <event name="mode">
91+ <arg name="mode" type="new_id" interface="zwlr_fixture_mode_v1"/>
92+ </event>
93+
94+ <event name="peer">
95+ <arg name="surface" type="object" interface="wl_surface"/>
96+ </event>
97+
98+ <event name="scaled">
99+ <arg name="scale" type="fixed"/>
100+ </event>
101+
102+ <event name="blob">
103+ <arg name="data" type="array"/>
104+ </event>
105+ </interface>
106+
107+ <interface name="zwlr_fixture_mode_v1" version="1">
108+ <description summary="fixture mode object"/>
109+
110+ <request name="destroy" type="destructor"/>
111+ </interface>
112+</protocol>
A protocol/SOURCE.md +7 -0
@@ -0,0 +1,7 @@
1+# Core protocol source
2+
3+`wayland.xml` is from Wayland 1.26.0, upstream GitLab
4+`https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit
5+`87cc8a8728a923fc57938faa81ba0e74f34ecdc7`.
6+SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`.
7+License: MIT (full copyright and grant in `wayland.xml` `<copyright>`).
@@ -0,0 +1,7 @@
1+# Core protocol source
2+
3+`wayland.xml` is from Wayland 1.26.0, upstream GitLab
4+`https://gitlab.freedesktop.org/wayland/wayland`, tag `1.26.0`, peeled commit
5+`87cc8a8728a923fc57938faa81ba0e74f34ecdc7`.
6+SHA-256: `cc860987e54f8d85c940e97fa1270c69b6e4ad31fbcf5a7f00107ce1157f5e07`.
7+License: MIT (full copyright and grant in `wayland.xml` `<copyright>`).
A protocol/core/core_test.go +532 -0
@@ -0,0 +1,532 @@
1+//go:build linux
2+
3+package core_test
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io"
9+ "net"
10+ "os"
11+ "testing"
12+ "time"
13+
14+ "github.com/bnema/wlturbo"
15+ "github.com/bnema/wlturbo/protocol/core"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
20+ t.Helper()
21+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ a := os.NewFile(uintptr(f[0]), "a")
26+ b := os.NewFile(uintptr(f[1]), "b")
27+ ca, e := net.FileConn(a)
28+ a.Close()
29+ if e != nil {
30+ t.Fatal(e)
31+ }
32+ cb, e := net.FileConn(b)
33+ b.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ t.Cleanup(func() { ca.Close(); cb.Close() })
38+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
39+}
40+func frame(id uint32, op uint16, body ...uint32) []byte {
41+ p := make([]byte, 8+4*len(body))
42+ binary.LittleEndian.PutUint32(p, id)
43+ binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
44+ for i, v := range body {
45+ binary.LittleEndian.PutUint32(p[8+i*4:], v)
46+ }
47+ return p
48+}
49+func receiveRequest(t *testing.T, p *net.UnixConn) {
50+ t.Helper()
51+ p.SetReadDeadline(testDeadline())
52+ b := make([]byte, 256)
53+ if _, e := p.Read(b); e != nil {
54+ t.Fatal(e)
55+ }
56+}
57+func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) }
58+func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
59+ t.Helper()
60+ var oob []byte
61+ if len(fd) > 0 {
62+ oob = unix.UnixRights(fd...)
63+ }
64+ if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil {
65+ t.Fatal(e)
66+ }
67+}
68+func TestGeneratedCoreOverSocketpair(t *testing.T) {
69+ c, p := pair(t)
70+ d, err := wlturbo.ConnectFromConn(c)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ defer d.Close()
75+ receiveRequest(t, p) // get_registry
76+ seat := core.NewSeat(d.Context())
77+ if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil {
78+ t.Fatal(err)
79+ }
80+ receiveRequest(t, p)
81+ kb, err := seat.GetKeyboard()
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ receiveRequest(t, p)
86+ var key uint32
87+ kb.OnKey(func(_, _, k, _ uint32) { key = k })
88+ send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1))
89+ if err := d.Dispatch(); err != nil {
90+ t.Fatal(err)
91+ }
92+ if key != 42 {
93+ t.Fatalf("key=%d", key)
94+ }
95+ compositor := core.NewCompositor(d.Context())
96+ if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil {
97+ t.Fatal(err)
98+ }
99+ receiveRequest(t, p)
100+ surface, err := compositor.CreateSurface()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ receiveRequest(t, p)
105+ callback, err := surface.Frame()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ receiveRequest(t, p)
110+ var done uint32
111+ callback.OnDone(func(ms uint32) { done = ms })
112+ send(t, p, frame(callback.ID(), 0, 123))
113+ if err := d.Dispatch(); err != nil {
114+ t.Fatal(err)
115+ }
116+ if done != 123 {
117+ t.Fatalf("done=%d", done)
118+ }
119+ send(t, p, frame(callback.ID(), 0, 124))
120+ if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
121+ t.Fatalf("destroyed callback: %v", err)
122+ }
123+}
124+func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) {
125+ c, p := pair(t)
126+ d, e := wlturbo.ConnectFromConn(c)
127+ if e != nil {
128+ t.Fatal(e)
129+ }
130+ defer d.Close()
131+ receiveRequest(t, p)
132+ kb := core.NewKeyboard(d.Context())
133+ kb.SetID(d.AllocateID())
134+ d.Context().Register(kb)
135+ r, w, e := os.Pipe()
136+ if e != nil {
137+ t.Fatal(e)
138+ }
139+ defer r.Close()
140+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
141+ w.Close()
142+ if e := d.Dispatch(); e != nil {
143+ t.Fatal(e)
144+ }
145+ buf := make([]byte, 1)
146+ if n, e := r.Read(buf); n != 0 || e != io.EOF {
147+ t.Fatalf("unclaimed FD n=%d err=%v", n, e)
148+ }
149+ r2, w2, e := os.Pipe()
150+ if e != nil {
151+ t.Fatal(e)
152+ }
153+ defer r2.Close()
154+ received := 0
155+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
156+ if fd == nil {
157+ t.Fatal("first FD missing")
158+ }
159+ n, e := fd.Take()
160+ if e != nil {
161+ t.Fatal(e)
162+ }
163+ received++
164+ unix.Close(n)
165+ })
166+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
167+ if fd != nil {
168+ t.Fatal("duplicated owner")
169+ }
170+ received++
171+ })
172+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd()))
173+ w2.Close()
174+ if e := d.Dispatch(); e != nil {
175+ t.Fatal(e)
176+ }
177+ if received != 2 {
178+ t.Fatalf("handlers=%d", received)
179+ }
180+}
181+func TestBufferReleaseAndOfferChild(t *testing.T) {
182+ c, p := pair(t)
183+ d, e := wlturbo.ConnectFromConn(c)
184+ if e != nil {
185+ t.Fatal(e)
186+ }
187+ defer d.Close()
188+ receiveRequest(t, p)
189+ b := core.NewBuffer(d.Context())
190+ b.SetID(d.AllocateID())
191+ d.Context().Register(b)
192+ releases := 0
193+ b.OnRelease(func() { releases++ })
194+ send(t, p, frame(b.ID(), 0))
195+ if e := d.Dispatch(); e != nil {
196+ t.Fatal(e)
197+ }
198+ if releases != 1 {
199+ t.Fatal("release not dispatched")
200+ }
201+ device := core.NewDataDevice(d.Context())
202+ device.SetID(d.AllocateID())
203+ d.Context().Register(device)
204+ var offer *core.DataOffer
205+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
206+ send(t, p, frame(device.ID(), 0, 0xff000001))
207+ if e := d.Dispatch(); e != nil {
208+ t.Fatal(e)
209+ }
210+ if offer == nil || offer.ID() != 0xff000001 {
211+ t.Fatalf("offer=%v", offer)
212+ }
213+}
214+
215+func TestMissingKeymapDescriptor(t *testing.T) {
216+ c, p := pair(t)
217+ d, e := wlturbo.ConnectFromConn(c)
218+ if e != nil {
219+ t.Fatal(e)
220+ }
221+ defer d.Close()
222+ receiveRequest(t, p)
223+ kb := core.NewKeyboard(d.Context())
224+ kb.SetID(d.AllocateID())
225+ d.Context().Register(kb)
226+ send(t, p, frame(kb.ID(), 0, 1, 8))
227+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
228+ t.Fatalf("missing FD: %v", e)
229+ }
230+}
231+
232+func TestInterleavedFDAndPlainMessages(t *testing.T) {
233+ c, p := pair(t)
234+ d, e := wlturbo.ConnectFromConn(c)
235+ if e != nil {
236+ t.Fatal(e)
237+ }
238+ defer d.Close()
239+ receiveRequest(t, p)
240+ kb := core.NewKeyboard(d.Context())
241+ kb.SetID(d.AllocateID())
242+ d.Context().Register(kb)
243+ calls := 0
244+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
245+ if fd == nil {
246+ t.Fatal("FD not assigned")
247+ }
248+ _ = fd.Close()
249+ calls++
250+ })
251+ kb.OnKey(func(_, _, key, _ uint32) {
252+ if key != 33 {
253+ t.Errorf("key=%d", key)
254+ }
255+ calls++
256+ })
257+ r, w, e := os.Pipe()
258+ if e != nil {
259+ t.Fatal(e)
260+ }
261+ defer r.Close()
262+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
263+ w.Close()
264+ send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1))
265+ if e := d.Dispatch(); e != nil {
266+ t.Fatal(e)
267+ }
268+ if e := d.Dispatch(); e != nil {
269+ t.Fatal(e)
270+ }
271+ if calls != 2 {
272+ t.Fatalf("calls=%d", calls)
273+ }
274+}
275+func TestMalformedGeneratedPayload(t *testing.T) {
276+ c, p := pair(t)
277+ d, e := wlturbo.ConnectFromConn(c)
278+ if e != nil {
279+ t.Fatal(e)
280+ }
281+ defer d.Close()
282+ receiveRequest(t, p)
283+ kb := core.NewKeyboard(d.Context())
284+ kb.SetID(d.AllocateID())
285+ d.Context().Register(kb)
286+ send(t, p, frame(kb.ID(), 3, 1, 2))
287+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
288+ t.Fatalf("truncated key: %v", e)
289+ }
290+}
291+
292+func TestServerChildEventsAfterOffer(t *testing.T) {
293+ c, p := pair(t)
294+ d, e := wlturbo.ConnectFromConn(c)
295+ if e != nil {
296+ t.Fatal(e)
297+ }
298+ defer d.Close()
299+ receiveRequest(t, p)
300+ device := core.NewDataDevice(d.Context())
301+ device.SetID(d.AllocateID())
302+ d.Context().Register(device)
303+ const offerID = 0xff000011
304+ var offer *core.DataOffer
305+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
306+ send(t, p, frame(device.ID(), 0, offerID))
307+ if e := d.Dispatch(); e != nil {
308+ t.Fatal(e)
309+ }
310+ if offer == nil {
311+ t.Fatal("missing concrete child")
312+ }
313+ got := ""
314+ offer.OnOffer(func(mime string) { got = mime })
315+ payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0)
316+ binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16)
317+ send(t, p, payload)
318+ if e := d.Dispatch(); e != nil {
319+ t.Fatal(e)
320+ }
321+ if got != "abc" {
322+ t.Fatalf("typed child event: %q", got)
323+ }
324+}
325+
326+func TestRegistryBootstrapAnnouncements(t *testing.T) {
327+ c, p := pair(t)
328+ d, e := wlturbo.ConnectFromConn(c)
329+ if e != nil {
330+ t.Fatal(e)
331+ }
332+ defer d.Close()
333+ receiveRequest(t, p)
334+ var got wlturbo.Global
335+ d.Registry().AddHandler("wl_compositor", func(r *wlturbo.Registry, n, v uint32) { got, _ = r.FindGlobalByName(n) })
336+ name := "wl_compositor"
337+ body := make([]byte, 4+4+((len(name)+1+3)&^3)+4)
338+ binary.LittleEndian.PutUint32(body, 17)
339+ binary.LittleEndian.PutUint32(body[4:], uint32(len(name)+1))
340+ copy(body[8:], name)
341+ binary.LittleEndian.PutUint32(body[len(body)-4:], 7)
342+ msg := append(frame(d.Registry().ID(), 0), body...)
343+ binary.LittleEndian.PutUint32(msg[4:], uint32(len(msg))<<16)
344+ send(t, p, msg)
345+ if e := d.Dispatch(); e != nil {
346+ t.Fatal(e)
347+ }
348+ if got.Name != 17 || got.Interface != name || got.Version != 7 {
349+ t.Fatalf("global: %+v", got)
350+ }
351+ send(t, p, frame(d.Registry().ID(), 1, 17))
352+ if e := d.Dispatch(); e != nil {
353+ t.Fatal(e)
354+ }
355+ if _, ok := d.Registry().FindGlobalByName(17); ok {
356+ t.Fatal("global not removed")
357+ }
358+ send(t, p, frame(d.Registry().ID(), 2))
359+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrUnknownOpcode) {
360+ t.Fatalf("unknown registry opcode: %v", e)
361+ }
362+}
363+func TestUnclaimedFirstHandlerFDClosed(t *testing.T) {
364+ c, p := pair(t)
365+ d, e := wlturbo.ConnectFromConn(c)
366+ if e != nil {
367+ t.Fatal(e)
368+ }
369+ defer d.Close()
370+ receiveRequest(t, p)
371+ kb := core.NewKeyboard(d.Context())
372+ kb.SetID(d.AllocateID())
373+ d.Context().Register(kb)
374+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
375+ if fd == nil {
376+ t.Fatal("missing first owner")
377+ }
378+ })
379+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
380+ if fd != nil {
381+ t.Fatal("duplicate owner")
382+ }
383+ })
384+ r, w, e := os.Pipe()
385+ if e != nil {
386+ t.Fatal(e)
387+ }
388+ defer r.Close()
389+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
390+ w.Close()
391+ if e := d.Dispatch(); e != nil {
392+ t.Fatal(e)
393+ }
394+ if e := r.SetReadDeadline(testDeadline()); e != nil {
395+ t.Fatal(e)
396+ }
397+ var b [1]byte
398+ if n, e := r.Read(b[:]); n != 0 || e != io.EOF {
399+ t.Fatalf("unclaimed FD not closed: %d %v", n, e)
400+ }
401+}
402+
403+func TestDestroyedProxyRejectsRequests(t *testing.T) {
404+ c, p := pair(t)
405+ d, err := wlturbo.ConnectFromConn(c)
406+ if err != nil {
407+ t.Fatal(err)
408+ }
409+ defer d.Close()
410+ receiveRequest(t, p)
411+ surface := core.NewSurface(d.Context())
412+ surface.SetID(d.AllocateID())
413+ d.Context().Register(surface)
414+ if err := surface.Destroy(); err != nil {
415+ t.Fatal(err)
416+ }
417+ receiveRequest(t, p)
418+ if err := surface.Destroy(); err == nil {
419+ t.Fatal("second destroy succeeded")
420+ }
421+ if err := surface.Commit(); err == nil {
422+ t.Fatal("request on destroyed surface succeeded")
423+ }
424+ if _, err := surface.Frame(); err == nil {
425+ t.Fatal("child request on destroyed surface succeeded")
426+ }
427+ p.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
428+ _, err = p.Read(make([]byte, 32))
429+ ne, ok := err.(net.Error)
430+ if !ok || !ne.Timeout() {
431+ t.Fatalf("unexpected bytes on wire: %v", err)
432+ }
433+}
434+
435+func TestConcurrentDestroySendsOnce(t *testing.T) {
436+ c, p := pair(t)
437+ d, err := wlturbo.ConnectFromConn(c)
438+ if err != nil {
439+ t.Fatal(err)
440+ }
441+ defer d.Close()
442+ receiveRequest(t, p)
443+ const rounds, destroyers, committers = 100, 4, 4
444+ totalCommits := 0
445+ for round := 0; round < rounds; round++ {
446+ surface := core.NewSurface(d.Context())
447+ surface.SetID(d.AllocateID())
448+ d.Context().Register(surface)
449+ start := make(chan struct{})
450+ destroyed := make(chan error, destroyers)
451+ committed := make(chan error, committers)
452+ for i := 0; i < destroyers; i++ {
453+ go func() { <-start; destroyed <- surface.Destroy() }()
454+ }
455+ for i := 0; i < committers; i++ {
456+ go func() { <-start; committed <- surface.Commit() }()
457+ }
458+ close(start)
459+ okDestroy, okCommit := 0, 0
460+ for i := 0; i < destroyers; i++ {
461+ if <-destroyed == nil {
462+ okDestroy++
463+ }
464+ }
465+ for i := 0; i < committers; i++ {
466+ if <-committed == nil {
467+ okCommit++
468+ }
469+ }
470+ if okDestroy != 1 {
471+ t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy)
472+ }
473+ totalCommits += okCommit
474+ // Decode every frame: exactly okCommit commits (opcode 6), then one
475+ // destroy (opcode 0) for this surface, and nothing after it.
476+ frames := readFrames(t, p, okCommit+1)
477+ for i, f := range frames {
478+ if f.id != surface.ID() {
479+ t.Fatalf("round %d: frame %d targets object %d", round, i, f.id)
480+ }
481+ last := i == len(frames)-1
482+ if last && f.opcode != 0 || !last && f.opcode != 6 {
483+ t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last)
484+ }
485+ }
486+ p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))
487+ if n, err := p.Read(make([]byte, 8)); n != 0 {
488+ t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err)
489+ }
490+ p.SetReadDeadline(time.Time{})
491+ }
492+ // Ordering is only exercised when some commits won the race.
493+ if totalCommits == 0 {
494+ t.Fatal("no commit succeeded in any round; ordering was not exercised")
495+ }
496+}
497+
498+type wireFrame struct {
499+ id uint32
500+ opcode uint16
501+}
502+
503+func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame {
504+ t.Helper()
505+ p.SetReadDeadline(testDeadline())
506+ defer p.SetReadDeadline(time.Time{})
507+ var out []wireFrame
508+ var buf []byte
509+ tmp := make([]byte, 4096)
510+ for len(out) < n {
511+ for len(buf) >= 8 {
512+ size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16)
513+ if size < 8 || len(buf) < size {
514+ break
515+ }
516+ out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))})
517+ buf = buf[size:]
518+ }
519+ if len(out) >= n {
520+ break
521+ }
522+ m, err := p.Read(tmp)
523+ if err != nil {
524+ t.Fatalf("read frames: got %d of %d: %v", len(out), n, err)
525+ }
526+ buf = append(buf, tmp[:m]...)
527+ }
528+ if len(out) != n || len(buf) != 0 {
529+ t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n)
530+ }
531+ return out
532+}
@@ -0,0 +1,532 @@
1+//go:build linux
2+
3+package core_test
4+
5+import (
6+ "encoding/binary"
7+ "errors"
8+ "io"
9+ "net"
10+ "os"
11+ "testing"
12+ "time"
13+
14+ "github.com/bnema/wlturbo"
15+ "github.com/bnema/wlturbo/protocol/core"
16+ "golang.org/x/sys/unix"
17+)
18+
19+func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
20+ t.Helper()
21+ f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
22+ if err != nil {
23+ t.Fatal(err)
24+ }
25+ a := os.NewFile(uintptr(f[0]), "a")
26+ b := os.NewFile(uintptr(f[1]), "b")
27+ ca, e := net.FileConn(a)
28+ a.Close()
29+ if e != nil {
30+ t.Fatal(e)
31+ }
32+ cb, e := net.FileConn(b)
33+ b.Close()
34+ if e != nil {
35+ t.Fatal(e)
36+ }
37+ t.Cleanup(func() { ca.Close(); cb.Close() })
38+ return ca.(*net.UnixConn), cb.(*net.UnixConn)
39+}
40+func frame(id uint32, op uint16, body ...uint32) []byte {
41+ p := make([]byte, 8+4*len(body))
42+ binary.LittleEndian.PutUint32(p, id)
43+ binary.LittleEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op))
44+ for i, v := range body {
45+ binary.LittleEndian.PutUint32(p[8+i*4:], v)
46+ }
47+ return p
48+}
49+func receiveRequest(t *testing.T, p *net.UnixConn) {
50+ t.Helper()
51+ p.SetReadDeadline(testDeadline())
52+ b := make([]byte, 256)
53+ if _, e := p.Read(b); e != nil {
54+ t.Fatal(e)
55+ }
56+}
57+func testDeadline() (d time.Time) { return time.Now().Add(2 * time.Second) }
58+func send(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
59+ t.Helper()
60+ var oob []byte
61+ if len(fd) > 0 {
62+ oob = unix.UnixRights(fd...)
63+ }
64+ if _, _, e := p.WriteMsgUnix(msg, oob, nil); e != nil {
65+ t.Fatal(e)
66+ }
67+}
68+func TestGeneratedCoreOverSocketpair(t *testing.T) {
69+ c, p := pair(t)
70+ d, err := wlturbo.ConnectFromConn(c)
71+ if err != nil {
72+ t.Fatal(err)
73+ }
74+ defer d.Close()
75+ receiveRequest(t, p) // get_registry
76+ seat := core.NewSeat(d.Context())
77+ if err := d.Registry().Bind(1, core.SeatInterface, 1, seat); err != nil {
78+ t.Fatal(err)
79+ }
80+ receiveRequest(t, p)
81+ kb, err := seat.GetKeyboard()
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ receiveRequest(t, p)
86+ var key uint32
87+ kb.OnKey(func(_, _, k, _ uint32) { key = k })
88+ send(t, p, frame(kb.ID(), 3, 1, 2, 42, 1))
89+ if err := d.Dispatch(); err != nil {
90+ t.Fatal(err)
91+ }
92+ if key != 42 {
93+ t.Fatalf("key=%d", key)
94+ }
95+ compositor := core.NewCompositor(d.Context())
96+ if err := d.Registry().Bind(2, core.CompositorInterface, 1, compositor); err != nil {
97+ t.Fatal(err)
98+ }
99+ receiveRequest(t, p)
100+ surface, err := compositor.CreateSurface()
101+ if err != nil {
102+ t.Fatal(err)
103+ }
104+ receiveRequest(t, p)
105+ callback, err := surface.Frame()
106+ if err != nil {
107+ t.Fatal(err)
108+ }
109+ receiveRequest(t, p)
110+ var done uint32
111+ callback.OnDone(func(ms uint32) { done = ms })
112+ send(t, p, frame(callback.ID(), 0, 123))
113+ if err := d.Dispatch(); err != nil {
114+ t.Fatal(err)
115+ }
116+ if done != 123 {
117+ t.Fatalf("done=%d", done)
118+ }
119+ send(t, p, frame(callback.ID(), 0, 124))
120+ if err := d.Dispatch(); !errors.Is(err, wlturbo.ErrUnknownObject) {
121+ t.Fatalf("destroyed callback: %v", err)
122+ }
123+}
124+func TestKeymapFDNoHandlerAndMultipleHandlers(t *testing.T) {
125+ c, p := pair(t)
126+ d, e := wlturbo.ConnectFromConn(c)
127+ if e != nil {
128+ t.Fatal(e)
129+ }
130+ defer d.Close()
131+ receiveRequest(t, p)
132+ kb := core.NewKeyboard(d.Context())
133+ kb.SetID(d.AllocateID())
134+ d.Context().Register(kb)
135+ r, w, e := os.Pipe()
136+ if e != nil {
137+ t.Fatal(e)
138+ }
139+ defer r.Close()
140+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
141+ w.Close()
142+ if e := d.Dispatch(); e != nil {
143+ t.Fatal(e)
144+ }
145+ buf := make([]byte, 1)
146+ if n, e := r.Read(buf); n != 0 || e != io.EOF {
147+ t.Fatalf("unclaimed FD n=%d err=%v", n, e)
148+ }
149+ r2, w2, e := os.Pipe()
150+ if e != nil {
151+ t.Fatal(e)
152+ }
153+ defer r2.Close()
154+ received := 0
155+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
156+ if fd == nil {
157+ t.Fatal("first FD missing")
158+ }
159+ n, e := fd.Take()
160+ if e != nil {
161+ t.Fatal(e)
162+ }
163+ received++
164+ unix.Close(n)
165+ })
166+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
167+ if fd != nil {
168+ t.Fatal("duplicated owner")
169+ }
170+ received++
171+ })
172+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w2.Fd()))
173+ w2.Close()
174+ if e := d.Dispatch(); e != nil {
175+ t.Fatal(e)
176+ }
177+ if received != 2 {
178+ t.Fatalf("handlers=%d", received)
179+ }
180+}
181+func TestBufferReleaseAndOfferChild(t *testing.T) {
182+ c, p := pair(t)
183+ d, e := wlturbo.ConnectFromConn(c)
184+ if e != nil {
185+ t.Fatal(e)
186+ }
187+ defer d.Close()
188+ receiveRequest(t, p)
189+ b := core.NewBuffer(d.Context())
190+ b.SetID(d.AllocateID())
191+ d.Context().Register(b)
192+ releases := 0
193+ b.OnRelease(func() { releases++ })
194+ send(t, p, frame(b.ID(), 0))
195+ if e := d.Dispatch(); e != nil {
196+ t.Fatal(e)
197+ }
198+ if releases != 1 {
199+ t.Fatal("release not dispatched")
200+ }
201+ device := core.NewDataDevice(d.Context())
202+ device.SetID(d.AllocateID())
203+ d.Context().Register(device)
204+ var offer *core.DataOffer
205+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
206+ send(t, p, frame(device.ID(), 0, 0xff000001))
207+ if e := d.Dispatch(); e != nil {
208+ t.Fatal(e)
209+ }
210+ if offer == nil || offer.ID() != 0xff000001 {
211+ t.Fatalf("offer=%v", offer)
212+ }
213+}
214+
215+func TestMissingKeymapDescriptor(t *testing.T) {
216+ c, p := pair(t)
217+ d, e := wlturbo.ConnectFromConn(c)
218+ if e != nil {
219+ t.Fatal(e)
220+ }
221+ defer d.Close()
222+ receiveRequest(t, p)
223+ kb := core.NewKeyboard(d.Context())
224+ kb.SetID(d.AllocateID())
225+ d.Context().Register(kb)
226+ send(t, p, frame(kb.ID(), 0, 1, 8))
227+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
228+ t.Fatalf("missing FD: %v", e)
229+ }
230+}
231+
232+func TestInterleavedFDAndPlainMessages(t *testing.T) {
233+ c, p := pair(t)
234+ d, e := wlturbo.ConnectFromConn(c)
235+ if e != nil {
236+ t.Fatal(e)
237+ }
238+ defer d.Close()
239+ receiveRequest(t, p)
240+ kb := core.NewKeyboard(d.Context())
241+ kb.SetID(d.AllocateID())
242+ d.Context().Register(kb)
243+ calls := 0
244+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
245+ if fd == nil {
246+ t.Fatal("FD not assigned")
247+ }
248+ _ = fd.Close()
249+ calls++
250+ })
251+ kb.OnKey(func(_, _, key, _ uint32) {
252+ if key != 33 {
253+ t.Errorf("key=%d", key)
254+ }
255+ calls++
256+ })
257+ r, w, e := os.Pipe()
258+ if e != nil {
259+ t.Fatal(e)
260+ }
261+ defer r.Close()
262+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
263+ w.Close()
264+ send(t, p, frame(kb.ID(), 3, 1, 2, 33, 1))
265+ if e := d.Dispatch(); e != nil {
266+ t.Fatal(e)
267+ }
268+ if e := d.Dispatch(); e != nil {
269+ t.Fatal(e)
270+ }
271+ if calls != 2 {
272+ t.Fatalf("calls=%d", calls)
273+ }
274+}
275+func TestMalformedGeneratedPayload(t *testing.T) {
276+ c, p := pair(t)
277+ d, e := wlturbo.ConnectFromConn(c)
278+ if e != nil {
279+ t.Fatal(e)
280+ }
281+ defer d.Close()
282+ receiveRequest(t, p)
283+ kb := core.NewKeyboard(d.Context())
284+ kb.SetID(d.AllocateID())
285+ d.Context().Register(kb)
286+ send(t, p, frame(kb.ID(), 3, 1, 2))
287+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrMalformedFrame) {
288+ t.Fatalf("truncated key: %v", e)
289+ }
290+}
291+
292+func TestServerChildEventsAfterOffer(t *testing.T) {
293+ c, p := pair(t)
294+ d, e := wlturbo.ConnectFromConn(c)
295+ if e != nil {
296+ t.Fatal(e)
297+ }
298+ defer d.Close()
299+ receiveRequest(t, p)
300+ device := core.NewDataDevice(d.Context())
301+ device.SetID(d.AllocateID())
302+ d.Context().Register(device)
303+ const offerID = 0xff000011
304+ var offer *core.DataOffer
305+ device.OnDataOffer(func(o *core.DataOffer) { offer = o })
306+ send(t, p, frame(device.ID(), 0, offerID))
307+ if e := d.Dispatch(); e != nil {
308+ t.Fatal(e)
309+ }
310+ if offer == nil {
311+ t.Fatal("missing concrete child")
312+ }
313+ got := ""
314+ offer.OnOffer(func(mime string) { got = mime })
315+ payload := append(frame(offerID, 0), 4, 0, 0, 0, 'a', 'b', 'c', 0)
316+ binary.LittleEndian.PutUint32(payload[4:], uint32(len(payload))<<16)
317+ send(t, p, payload)
318+ if e := d.Dispatch(); e != nil {
319+ t.Fatal(e)
320+ }
321+ if got != "abc" {
322+ t.Fatalf("typed child event: %q", got)
323+ }
324+}
325+
326+func TestRegistryBootstrapAnnouncements(t *testing.T) {
327+ c, p := pair(t)
328+ d, e := wlturbo.ConnectFromConn(c)
329+ if e != nil {
330+ t.Fatal(e)
331+ }
332+ defer d.Close()
333+ receiveRequest(t, p)
334+ var got wlturbo.Global
335+ d.Registry().AddHandler("wl_compositor", func(r *wlturbo.Registry, n, v uint32) { got, _ = r.FindGlobalByName(n) })
336+ name := "wl_compositor"
337+ body := make([]byte, 4+4+((len(name)+1+3)&^3)+4)
338+ binary.LittleEndian.PutUint32(body, 17)
339+ binary.LittleEndian.PutUint32(body[4:], uint32(len(name)+1))
340+ copy(body[8:], name)
341+ binary.LittleEndian.PutUint32(body[len(body)-4:], 7)
342+ msg := append(frame(d.Registry().ID(), 0), body...)
343+ binary.LittleEndian.PutUint32(msg[4:], uint32(len(msg))<<16)
344+ send(t, p, msg)
345+ if e := d.Dispatch(); e != nil {
346+ t.Fatal(e)
347+ }
348+ if got.Name != 17 || got.Interface != name || got.Version != 7 {
349+ t.Fatalf("global: %+v", got)
350+ }
351+ send(t, p, frame(d.Registry().ID(), 1, 17))
352+ if e := d.Dispatch(); e != nil {
353+ t.Fatal(e)
354+ }
355+ if _, ok := d.Registry().FindGlobalByName(17); ok {
356+ t.Fatal("global not removed")
357+ }
358+ send(t, p, frame(d.Registry().ID(), 2))
359+ if e := d.Dispatch(); !errors.Is(e, wlturbo.ErrUnknownOpcode) {
360+ t.Fatalf("unknown registry opcode: %v", e)
361+ }
362+}
363+func TestUnclaimedFirstHandlerFDClosed(t *testing.T) {
364+ c, p := pair(t)
365+ d, e := wlturbo.ConnectFromConn(c)
366+ if e != nil {
367+ t.Fatal(e)
368+ }
369+ defer d.Close()
370+ receiveRequest(t, p)
371+ kb := core.NewKeyboard(d.Context())
372+ kb.SetID(d.AllocateID())
373+ d.Context().Register(kb)
374+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
375+ if fd == nil {
376+ t.Fatal("missing first owner")
377+ }
378+ })
379+ kb.OnKeymap(func(_ uint32, fd *wlturbo.OwnedFD, _ uint32) {
380+ if fd != nil {
381+ t.Fatal("duplicate owner")
382+ }
383+ })
384+ r, w, e := os.Pipe()
385+ if e != nil {
386+ t.Fatal(e)
387+ }
388+ defer r.Close()
389+ send(t, p, frame(kb.ID(), 0, 1, 8), int(w.Fd()))
390+ w.Close()
391+ if e := d.Dispatch(); e != nil {
392+ t.Fatal(e)
393+ }
394+ if e := r.SetReadDeadline(testDeadline()); e != nil {
395+ t.Fatal(e)
396+ }
397+ var b [1]byte
398+ if n, e := r.Read(b[:]); n != 0 || e != io.EOF {
399+ t.Fatalf("unclaimed FD not closed: %d %v", n, e)
400+ }
401+}
402+
403+func TestDestroyedProxyRejectsRequests(t *testing.T) {
404+ c, p := pair(t)
405+ d, err := wlturbo.ConnectFromConn(c)
406+ if err != nil {
407+ t.Fatal(err)
408+ }
409+ defer d.Close()
410+ receiveRequest(t, p)
411+ surface := core.NewSurface(d.Context())
412+ surface.SetID(d.AllocateID())
413+ d.Context().Register(surface)
414+ if err := surface.Destroy(); err != nil {
415+ t.Fatal(err)
416+ }
417+ receiveRequest(t, p)
418+ if err := surface.Destroy(); err == nil {
419+ t.Fatal("second destroy succeeded")
420+ }
421+ if err := surface.Commit(); err == nil {
422+ t.Fatal("request on destroyed surface succeeded")
423+ }
424+ if _, err := surface.Frame(); err == nil {
425+ t.Fatal("child request on destroyed surface succeeded")
426+ }
427+ p.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
428+ _, err = p.Read(make([]byte, 32))
429+ ne, ok := err.(net.Error)
430+ if !ok || !ne.Timeout() {
431+ t.Fatalf("unexpected bytes on wire: %v", err)
432+ }
433+}
434+
435+func TestConcurrentDestroySendsOnce(t *testing.T) {
436+ c, p := pair(t)
437+ d, err := wlturbo.ConnectFromConn(c)
438+ if err != nil {
439+ t.Fatal(err)
440+ }
441+ defer d.Close()
442+ receiveRequest(t, p)
443+ const rounds, destroyers, committers = 100, 4, 4
444+ totalCommits := 0
445+ for round := 0; round < rounds; round++ {
446+ surface := core.NewSurface(d.Context())
447+ surface.SetID(d.AllocateID())
448+ d.Context().Register(surface)
449+ start := make(chan struct{})
450+ destroyed := make(chan error, destroyers)
451+ committed := make(chan error, committers)
452+ for i := 0; i < destroyers; i++ {
453+ go func() { <-start; destroyed <- surface.Destroy() }()
454+ }
455+ for i := 0; i < committers; i++ {
456+ go func() { <-start; committed <- surface.Commit() }()
457+ }
458+ close(start)
459+ okDestroy, okCommit := 0, 0
460+ for i := 0; i < destroyers; i++ {
461+ if <-destroyed == nil {
462+ okDestroy++
463+ }
464+ }
465+ for i := 0; i < committers; i++ {
466+ if <-committed == nil {
467+ okCommit++
468+ }
469+ }
470+ if okDestroy != 1 {
471+ t.Fatalf("round %d: %d destroys succeeded, want 1", round, okDestroy)
472+ }
473+ totalCommits += okCommit
474+ // Decode every frame: exactly okCommit commits (opcode 6), then one
475+ // destroy (opcode 0) for this surface, and nothing after it.
476+ frames := readFrames(t, p, okCommit+1)
477+ for i, f := range frames {
478+ if f.id != surface.ID() {
479+ t.Fatalf("round %d: frame %d targets object %d", round, i, f.id)
480+ }
481+ last := i == len(frames)-1
482+ if last && f.opcode != 0 || !last && f.opcode != 6 {
483+ t.Fatalf("round %d: frame %d opcode %d (last=%v)", round, i, f.opcode, last)
484+ }
485+ }
486+ p.SetReadDeadline(time.Now().Add(2 * time.Millisecond))
487+ if n, err := p.Read(make([]byte, 8)); n != 0 {
488+ t.Fatalf("round %d: request after destroy (%d bytes, %v)", round, n, err)
489+ }
490+ p.SetReadDeadline(time.Time{})
491+ }
492+ // Ordering is only exercised when some commits won the race.
493+ if totalCommits == 0 {
494+ t.Fatal("no commit succeeded in any round; ordering was not exercised")
495+ }
496+}
497+
498+type wireFrame struct {
499+ id uint32
500+ opcode uint16
501+}
502+
503+func readFrames(t *testing.T, p *net.UnixConn, n int) []wireFrame {
504+ t.Helper()
505+ p.SetReadDeadline(testDeadline())
506+ defer p.SetReadDeadline(time.Time{})
507+ var out []wireFrame
508+ var buf []byte
509+ tmp := make([]byte, 4096)
510+ for len(out) < n {
511+ for len(buf) >= 8 {
512+ size := int(binary.LittleEndian.Uint32(buf[4:8]) >> 16)
513+ if size < 8 || len(buf) < size {
514+ break
515+ }
516+ out = append(out, wireFrame{binary.LittleEndian.Uint32(buf[0:4]), uint16(binary.LittleEndian.Uint32(buf[4:8]))})
517+ buf = buf[size:]
518+ }
519+ if len(out) >= n {
520+ break
521+ }
522+ m, err := p.Read(tmp)
523+ if err != nil {
524+ t.Fatalf("read frames: got %d of %d: %v", len(out), n, err)
525+ }
526+ buf = append(buf, tmp[:m]...)
527+ }
528+ if len(out) != n || len(buf) != 0 {
529+ t.Fatalf("got %d frames (+%d bytes), want %d", len(out), len(buf), n)
530+ }
531+ return out
532+}
A protocol/core/doc.go +8 -0
@@ -0,0 +1,8 @@
1+// Package core contains canonical Wayland core protocol bindings generated from
2+// pinned wayland.xml. Bind globals with display.Registry().Bind and the
3+// corresponding New* constructor. Client-created and server-created children
4+// are registered with their concrete generated types; never replace them with
5+// transport wrappers. On* handlers receive typed values. Received OwnedFDs
6+// are closed after the event unless a handler calls Take; request FDs are
7+// transferred (and closed locally) only after a complete successful send.
8+package core
@@ -0,0 +1,8 @@
1+// Package core contains canonical Wayland core protocol bindings generated from
2+// pinned wayland.xml. Bind globals with display.Registry().Bind and the
3+// corresponding New* constructor. Client-created and server-created children
4+// are registered with their concrete generated types; never replace them with
5+// transport wrappers. On* handlers receive typed values. Received OwnedFDs
6+// are closed after the event unless a handler calls Take; request FDs are
7+// transferred (and closed locally) only after a complete successful send.
8+package core
A protocol/core/wayland_generated.go +4216 -0 large diff (4216 lines changed): collapsed
A protocol/generate.go +3 -0
@@ -0,0 +1,3 @@
1+package protocol
2+
3+//go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml
@@ -0,0 +1,3 @@
1+package protocol
2+
3+//go:generate go run ../cmd/wlturbo-scanner -p core -o core/wayland_generated.go wayland.xml
A protocol/wayland.xml +3442 -0 large diff (3442 lines changed): collapsed
M scm_linux.go +77 -49
@@ -6,9 +6,13 @@ package wlturbo
66 import (
77 "errors"
88 "fmt"
9- "golang.org/x/sys/unix"
9+ "io"
10+ "net"
1011 "sync"
1112 "syscall"
13+ "unsafe"
14+
15+ "golang.org/x/sys/unix"
1216 )
1317
1418 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
@@ -16,10 +20,12 @@ import (
1620 // instead of silently dropping descriptors.
1721 const maxRecvFDs = 64
1822
23+type controlBuffer struct{ data []byte }
24+
1925 // controlBufferPool provides control-message buffers sized for maxRecvFDs.
2026 var controlBufferPool = sync.Pool{
2127 New: func() interface{} {
22- return make([]byte, unix.CmsgSpace(maxRecvFDs*4))
28+ return &controlBuffer{data: make([]byte, unix.CmsgSpace(maxRecvFDs*4))}
2329 },
2430 }
2531
@@ -40,8 +46,9 @@ func (d *Display) readChunk() (int, error) {
4046 return n, err
4147 }
4248
43- oob := controlBufferPool.Get().([]byte)
44- defer controlBufferPool.Put(oob)
49+ buffer := controlBufferPool.Get().(*controlBuffer)
50+ defer controlBufferPool.Put(buffer)
51+ oob := buffer.data
4552
4653 n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
4754 if n < 0 {
@@ -51,12 +58,12 @@ func (d *Display) readChunk() (int, error) {
5158 if oobn > 0 {
5259 fds, perr := parseFDs(oob[:oobn])
5360 d.queueFDs(fds)
54- if perr != nil && err == nil {
55- err = perr
61+ if perr != nil {
62+ err = &ProtocolError{Kind: "malformed_control", Err: perr}
5663 }
5764 }
5865
59- if flags&unix.MSG_CTRUNC != 0 && err == nil {
66+ if flags&unix.MSG_CTRUNC != 0 {
6067 // Descriptors were dropped by the kernel, so the connection can no
6168 // longer be demarshalled correctly: treat it as a protocol failure so
6269 // the caller closes the descriptors that did arrive.
@@ -68,55 +75,54 @@ func (d *Display) readChunk() (int, error) {
6875 // parseFDs extracts the file descriptors carried by socket control messages.
6976 // Descriptors parsed before an error are returned so the caller can close them.
7077 func parseFDs(oob []byte) ([]int, error) {
71- scms, err := syscall.ParseSocketControlMessage(oob)
72- if err != nil {
73- return nil, fmt.Errorf("parse control message: %w", err)
74- }
75-
7678 var fds []int
77- for i := range scms {
78- scm := &scms[i]
79- if scm.Header.Type != syscall.SCM_RIGHTS {
80- continue
79+ for len(oob) > 0 {
80+ if len(oob) < unix.SizeofCmsghdr {
81+ return fds, fmt.Errorf("short control header")
8182 }
82- parsed, err := syscall.ParseUnixRights(scm)
83- if err != nil {
84- return fds, fmt.Errorf("parse unix rights: %w", err)
83+ var hdr unix.Cmsghdr
84+ // Parse each header independently: a malformed later header must not
85+ // discard rights already decoded from earlier messages.
86+ if err := readControlHeader(oob, &hdr); err != nil {
87+ return fds, err
88+ }
89+ length := int(hdr.Len)
90+ if length < unix.SizeofCmsghdr || length > len(oob) {
91+ return fds, fmt.Errorf("invalid control length %d", length)
92+ }
93+ if hdr.Level == unix.SOL_SOCKET && hdr.Type == unix.SCM_RIGHTS {
94+ message := syscall.SocketControlMessage{Header: syscall.Cmsghdr{Len: uint64(length), Level: int32(hdr.Level), Type: int32(hdr.Type)}, Data: oob[unix.SizeofCmsghdr:length]}
95+ parsed, err := syscall.ParseUnixRights(&message)
96+ if err != nil {
97+ return fds, fmt.Errorf("parse unix rights: %w", err)
98+ }
99+ fds = append(fds, parsed...)
85100 }
86- fds = append(fds, parsed...)
101+ step := unix.CmsgSpace(length - unix.SizeofCmsghdr)
102+ if step > len(oob) {
103+ break
104+ } // final control message may omit trailing padding
105+ oob = oob[step:]
87106 }
88107 return fds, nil
89108 }
90109
91-// queueFDs appends descriptors to this connection's pending list.
92-func (d *Display) queueFDs(fds []int) {
93- if len(fds) == 0 {
94- return
110+func readControlHeader(data []byte, hdr *unix.Cmsghdr) error {
111+ // Linux cmsghdr has a native-endian size_t followed by two int32s.
112+ if len(data) < unix.SizeofCmsghdr {
113+ return fmt.Errorf("short control header")
95114 }
96- d.fdMu.Lock()
97- d.pendingFDs = append(d.pendingFDs, fds...)
98- d.fdMu.Unlock()
115+ *hdr = *(*unix.Cmsghdr)(unsafe.Pointer(&data[0]))
116+ return nil
99117 }
100118
101-// nextFD removes and returns the oldest pending descriptor.
102-func (d *Display) nextFD() (int, bool) {
103- d.fdMu.Lock()
104- defer d.fdMu.Unlock()
105- if len(d.pendingFDs) == 0 {
106- return -1, false
107- }
108- fd := d.pendingFDs[0]
109- d.pendingFDs = d.pendingFDs[1:]
110- return fd, true
111-}
119+// queueFDs is called only by the active Dispatch reader.
120+func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) }
112121
113-// closePendingFDs closes every descriptor this connection still holds.
122+// closePendingFDs is called under recvMu (including during shutdown).
114123 func (d *Display) closePendingFDs() {
115- d.fdMu.Lock()
116124 fds := d.pendingFDs
117125 d.pendingFDs = nil
118- d.fdMu.Unlock()
119-
120126 for _, fd := range fds {
121127 if fd >= 0 {
122128 _ = unix.Close(fd)
@@ -124,20 +130,42 @@ func (d *Display) closePendingFDs() {
124130 }
125131 }
126132
133+// CloseSentFD releases a descriptor after a complete successful Wayland send.
134+func CloseSentFD(fd int) error { return unix.Close(fd) }
135+
127136 // sendmsgWithFDs sends a message, attaching file descriptors when present.
128-func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {
137+// A non-nil guard runs under the send lock, so its checks and the write are
138+// ordered atomically with every other request on the connection.
139+func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error {
129140 d.sendMu.Lock()
130141 defer d.sendMu.Unlock()
131142
132- if len(fds) == 0 {
133- _, err := d.conn.Write(buf)
134- return err
143+ if d.closed.Load() {
144+ return net.ErrClosed
135145 }
136- if d.unix == nil {
146+ if len(fds) > 0 && d.unix == nil {
137147 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
138148 }
139-
140- _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
149+ if guard != nil {
150+ if err := guard(); err != nil {
151+ return err
152+ }
153+ }
154+ var n int
155+ var err error
156+ if len(fds) == 0 {
157+ n, err = d.conn.Write(buf)
158+ } else {
159+ n, _, err = d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
160+ }
161+ if err == nil && n != len(buf) {
162+ err = io.ErrShortWrite
163+ }
164+ if err != nil {
165+ // A failed or partial write desynchronizes the stream: shut the socket
166+ // down so every later send and receive fails instead of corrupting it.
167+ _ = d.conn.Close()
168+ }
141169 return err
142170 }
143171
@@ -6,9 +6,13 @@ package wlturbo
6 import (6 import (
7 "errors"7 "errors"
8 "fmt"8 "fmt"
9- "golang.org/x/sys/unix"9+ "io"
10+ "net"
10 "sync"11 "sync"
11 "syscall"12 "syscall"
13+ "unsafe"
14+
15+ "golang.org/x/sys/unix"
12 )16 )
13 17
14 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.18 // maxRecvFDs bounds how many ancillary file descriptors a single read accepts.
@@ -16,10 +20,12 @@ import (
16 // instead of silently dropping descriptors.20 // instead of silently dropping descriptors.
17 const maxRecvFDs = 6421 const maxRecvFDs = 64
18 22
23+type controlBuffer struct{ data []byte }
24+
19 // controlBufferPool provides control-message buffers sized for maxRecvFDs.25 // controlBufferPool provides control-message buffers sized for maxRecvFDs.
20 var controlBufferPool = sync.Pool{26 var controlBufferPool = sync.Pool{
21 New: func() interface{} {27 New: func() interface{} {
22- return make([]byte, unix.CmsgSpace(maxRecvFDs*4))28+ return &controlBuffer{data: make([]byte, unix.CmsgSpace(maxRecvFDs*4))}
23 },29 },
24 }30 }
25 31
@@ -40,8 +46,9 @@ func (d *Display) readChunk() (int, error) {
40 return n, err46 return n, err
41 }47 }
42 48
43- oob := controlBufferPool.Get().([]byte)49+ buffer := controlBufferPool.Get().(*controlBuffer)
44- defer controlBufferPool.Put(oob)50+ defer controlBufferPool.Put(buffer)
51+ oob := buffer.data
45 52
46 n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)53 n, oobn, flags, _, err := d.unix.ReadMsgUnix(d.recvBuf, oob)
47 if n < 0 {54 if n < 0 {
@@ -51,12 +58,12 @@ func (d *Display) readChunk() (int, error) {
51 if oobn > 0 {58 if oobn > 0 {
52 fds, perr := parseFDs(oob[:oobn])59 fds, perr := parseFDs(oob[:oobn])
53 d.queueFDs(fds)60 d.queueFDs(fds)
54- if perr != nil && err == nil {61+ if perr != nil {
55- err = perr62+ err = &ProtocolError{Kind: "malformed_control", Err: perr}
56 }63 }
57 }64 }
58 65
59- if flags&unix.MSG_CTRUNC != 0 && err == nil {66+ if flags&unix.MSG_CTRUNC != 0 {
60 // Descriptors were dropped by the kernel, so the connection can no67 // Descriptors were dropped by the kernel, so the connection can no
61 // longer be demarshalled correctly: treat it as a protocol failure so68 // longer be demarshalled correctly: treat it as a protocol failure so
62 // the caller closes the descriptors that did arrive.69 // the caller closes the descriptors that did arrive.
@@ -68,55 +75,54 @@ func (d *Display) readChunk() (int, error) {
68 // parseFDs extracts the file descriptors carried by socket control messages.75 // parseFDs extracts the file descriptors carried by socket control messages.
69 // Descriptors parsed before an error are returned so the caller can close them.76 // Descriptors parsed before an error are returned so the caller can close them.
70 func parseFDs(oob []byte) ([]int, error) {77 func parseFDs(oob []byte) ([]int, error) {
71- scms, err := syscall.ParseSocketControlMessage(oob)
72- if err != nil {
73- return nil, fmt.Errorf("parse control message: %w", err)
74- }
75-
76 var fds []int78 var fds []int
77- for i := range scms {79+ for len(oob) > 0 {
78- scm := &scms[i]80+ if len(oob) < unix.SizeofCmsghdr {
79- if scm.Header.Type != syscall.SCM_RIGHTS {81+ return fds, fmt.Errorf("short control header")
80- continue
81 }82 }
82- parsed, err := syscall.ParseUnixRights(scm)83+ var hdr unix.Cmsghdr
83- if err != nil {84+ // Parse each header independently: a malformed later header must not
84- return fds, fmt.Errorf("parse unix rights: %w", err)85+ // discard rights already decoded from earlier messages.
86+ if err := readControlHeader(oob, &hdr); err != nil {
87+ return fds, err
88+ }
89+ length := int(hdr.Len)
90+ if length < unix.SizeofCmsghdr || length > len(oob) {
91+ return fds, fmt.Errorf("invalid control length %d", length)
92+ }
93+ if hdr.Level == unix.SOL_SOCKET && hdr.Type == unix.SCM_RIGHTS {
94+ message := syscall.SocketControlMessage{Header: syscall.Cmsghdr{Len: uint64(length), Level: int32(hdr.Level), Type: int32(hdr.Type)}, Data: oob[unix.SizeofCmsghdr:length]}
95+ parsed, err := syscall.ParseUnixRights(&message)
96+ if err != nil {
97+ return fds, fmt.Errorf("parse unix rights: %w", err)
98+ }
99+ fds = append(fds, parsed...)
85 }100 }
86- fds = append(fds, parsed...)101+ step := unix.CmsgSpace(length - unix.SizeofCmsghdr)
102+ if step > len(oob) {
103+ break
104+ } // final control message may omit trailing padding
105+ oob = oob[step:]
87 }106 }
88 return fds, nil107 return fds, nil
89 }108 }
90 109
91-// queueFDs appends descriptors to this connection's pending list.110+func readControlHeader(data []byte, hdr *unix.Cmsghdr) error {
92-func (d *Display) queueFDs(fds []int) {111+ // Linux cmsghdr has a native-endian size_t followed by two int32s.
93- if len(fds) == 0 {112+ if len(data) < unix.SizeofCmsghdr {
94- return113+ return fmt.Errorf("short control header")
95 }114 }
96- d.fdMu.Lock()115+ *hdr = *(*unix.Cmsghdr)(unsafe.Pointer(&data[0]))
97- d.pendingFDs = append(d.pendingFDs, fds...)116+ return nil
98- d.fdMu.Unlock()
99 }117 }
100 118
101-// nextFD removes and returns the oldest pending descriptor.119+// queueFDs is called only by the active Dispatch reader.
102-func (d *Display) nextFD() (int, bool) {120+func (d *Display) queueFDs(fds []int) { d.pendingFDs = append(d.pendingFDs, fds...) }
103- d.fdMu.Lock()
104- defer d.fdMu.Unlock()
105- if len(d.pendingFDs) == 0 {
106- return -1, false
107- }
108- fd := d.pendingFDs[0]
109- d.pendingFDs = d.pendingFDs[1:]
110- return fd, true
111-}
112 121
113-// closePendingFDs closes every descriptor this connection still holds.122+// closePendingFDs is called under recvMu (including during shutdown).
114 func (d *Display) closePendingFDs() {123 func (d *Display) closePendingFDs() {
115- d.fdMu.Lock()
116 fds := d.pendingFDs124 fds := d.pendingFDs
117 d.pendingFDs = nil125 d.pendingFDs = nil
118- d.fdMu.Unlock()
119-
120 for _, fd := range fds {126 for _, fd := range fds {
121 if fd >= 0 {127 if fd >= 0 {
122 _ = unix.Close(fd)128 _ = unix.Close(fd)
@@ -124,20 +130,42 @@ func (d *Display) closePendingFDs() {
124 }130 }
125 }131 }
126 132
133+// CloseSentFD releases a descriptor after a complete successful Wayland send.
134+func CloseSentFD(fd int) error { return unix.Close(fd) }
135+
127 // sendmsgWithFDs sends a message, attaching file descriptors when present.136 // sendmsgWithFDs sends a message, attaching file descriptors when present.
128-func (d *Display) sendmsgWithFDs(buf []byte, fds []int) error {137+// A non-nil guard runs under the send lock, so its checks and the write are
138+// ordered atomically with every other request on the connection.
139+func (d *Display) sendmsgWithFDs(buf []byte, fds []int, guard func() error) error {
129 d.sendMu.Lock()140 d.sendMu.Lock()
130 defer d.sendMu.Unlock()141 defer d.sendMu.Unlock()
131 142
132- if len(fds) == 0 {143+ if d.closed.Load() {
133- _, err := d.conn.Write(buf)144+ return net.ErrClosed
134- return err
135 }145 }
136- if d.unix == nil {146+ if len(fds) > 0 && d.unix == nil {
137 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")147 return errors.New("wlturbo: cannot send file descriptors over a non-Unix connection")
138 }148 }
139-149+ if guard != nil {
140- _, _, err := d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)150+ if err := guard(); err != nil {
151+ return err
152+ }
153+ }
154+ var n int
155+ var err error
156+ if len(fds) == 0 {
157+ n, err = d.conn.Write(buf)
158+ } else {
159+ n, _, err = d.unix.WriteMsgUnix(buf, unix.UnixRights(fds...), nil)
160+ }
161+ if err == nil && n != len(buf) {
162+ err = io.ErrShortWrite
163+ }
164+ if err != nil {
165+ // A failed or partial write desynchronizes the stream: shut the socket
166+ // down so every later send and receive fails instead of corrupting it.
167+ _ = d.conn.Close()
168+ }
141 return err169 return err
142 }170 }
143 171
M shm.go +0 -1
@@ -7,7 +7,6 @@ import (
77
88 // ShmPool represents a shared memory pool
99 type ShmPool struct {
10- id uint32
1110 fd int
1211 size int
1312 data []byte
@@ -7,7 +7,6 @@ import (
7 7
8 // ShmPool represents a shared memory pool8 // ShmPool represents a shared memory pool
9 type ShmPool struct {9 type ShmPool struct {
10- id uint32
11 fd int10 fd int
12 size int11 size int
13 data []byte12 data []byte
A signature.go +50 -0
@@ -0,0 +1,50 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+)
7+
8+// validateEventBody rejects truncated, unterminated and trailing wire values
9+// before generated code decodes or registers any children.
10+func validateEventBody(sig string, body []byte) error {
11+ pos := 0
12+ for len(sig) > 0 {
13+ end := 0
14+ for end < len(sig) && sig[end] != ',' {
15+ end++
16+ }
17+ kind := sig[:end]
18+ if end == len(sig) {
19+ return fmt.Errorf("invalid signature %q", sig)
20+ }
21+ sig = sig[end+1:]
22+ if kind == "fd" {
23+ continue
24+ }
25+ if len(body)-pos < 4 {
26+ return ErrMalformedFrame
27+ }
28+ switch kind {
29+ case "int", "uint", "fixed", "object", "new_id":
30+ pos += 4
31+ case "string", "array":
32+ n := uint64(binary.LittleEndian.Uint32(body[pos:]))
33+ pos += 4
34+ padded := (n + 3) &^ uint64(3)
35+ if padded > uint64(len(body)-pos) {
36+ return ErrMalformedFrame
37+ }
38+ if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 {
39+ return ErrMalformedFrame
40+ }
41+ pos += int(padded)
42+ default:
43+ return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame)
44+ }
45+ }
46+ if pos != len(body) {
47+ return ErrMalformedFrame
48+ }
49+ return nil
50+}
@@ -0,0 +1,50 @@
1+package wlturbo
2+
3+import (
4+ "encoding/binary"
5+ "fmt"
6+)
7+
8+// validateEventBody rejects truncated, unterminated and trailing wire values
9+// before generated code decodes or registers any children.
10+func validateEventBody(sig string, body []byte) error {
11+ pos := 0
12+ for len(sig) > 0 {
13+ end := 0
14+ for end < len(sig) && sig[end] != ',' {
15+ end++
16+ }
17+ kind := sig[:end]
18+ if end == len(sig) {
19+ return fmt.Errorf("invalid signature %q", sig)
20+ }
21+ sig = sig[end+1:]
22+ if kind == "fd" {
23+ continue
24+ }
25+ if len(body)-pos < 4 {
26+ return ErrMalformedFrame
27+ }
28+ switch kind {
29+ case "int", "uint", "fixed", "object", "new_id":
30+ pos += 4
31+ case "string", "array":
32+ n := uint64(binary.LittleEndian.Uint32(body[pos:]))
33+ pos += 4
34+ padded := (n + 3) &^ uint64(3)
35+ if padded > uint64(len(body)-pos) {
36+ return ErrMalformedFrame
37+ }
38+ if kind == "string" && n > 0 && body[pos+int(n)-1] != 0 {
39+ return ErrMalformedFrame
40+ }
41+ pos += int(padded)
42+ default:
43+ return fmt.Errorf("invalid argument kind %q: %w", kind, ErrMalformedFrame)
44+ }
45+ }
46+ if pos != len(body) {
47+ return ErrMalformedFrame
48+ }
49+ return nil
50+}
M transport_test.go +46 -5
@@ -92,6 +92,13 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
9292 BaseProxy: BaseProxy{id: 7, context: d.context},
9393 records: &records,
9494 })
95+ for _, opcode := range []uint16{3, 4, 5} {
96+ signature := "uint,"
97+ if opcode == 3 {
98+ signature = "uint,uint,"
99+ }
100+ d.RegisterEventSignature(7, opcode, signature)
101+ }
95102 return d, &records
96103 }
97104
@@ -132,7 +139,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) {
132139 }
133140
134141 func TestDisplayDispatch_Coalesced(t *testing.T) {
135- first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
142+ first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
136143 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137144
138145 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
@@ -146,7 +153,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
146153 }
147154
148155 want := []EventRecord{
149- {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
156+ {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150157 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151158 }
152159 if !reflect.DeepEqual(*records, want) {
@@ -157,7 +164,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
157164 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158165 // A read boundary that ends between the header and the body of the second
159166 // message must not be mistaken for a frame boundary.
160- whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
167+ whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
161168 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162169
163170 split := HeaderSize + 4 + 4 // mid-body of the second message
@@ -171,7 +178,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
171178 }
172179
173180 want := []EventRecord{
174- {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
181+ {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175182 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176183 }
177184 if !reflect.DeepEqual(*records, want) {
@@ -274,6 +281,10 @@ func TestDisplayDispatch_DisplayError(t *testing.T) {
274281
275282 func TestDisplayDispatch_DeleteID(t *testing.T) {
276283 d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
284+ for d.nextID <= 7 {
285+ d.allocateID()
286+ }
287+ d.objects.Delete(uint32(7))
277288
278289 if err := d.Dispatch(); err != nil {
279290 t.Fatalf("Dispatch: %v", err)
@@ -299,7 +310,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) {
299310 },
300311 {
301312 name: "closed after a complete message",
302- chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
313+ chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
303314 },
304315 }
305316
@@ -350,3 +361,33 @@ func TestReadFrame(t *testing.T) {
350361 t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351362 }
352363 }
364+
365+func TestDeleteIDReuseAndInvalid(t *testing.T) {
366+ d := newDisplay(&chunkConn{})
367+ id := d.AllocateID()
368+ body := make([]byte, 4)
369+ binary.LittleEndian.PutUint32(body, id)
370+ if err := d.handleDisplayEvent(1, body); err != nil {
371+ t.Fatal(err)
372+ }
373+ if err := d.handleDisplayEvent(1, body); err == nil {
374+ t.Fatal("duplicate delete_id accepted")
375+ }
376+ d.nextID = 0xff000000 // simulate exhaustion
377+ if got := d.AllocateID(); got != id {
378+ t.Fatalf("reused %d, want %d", got, id)
379+ }
380+ if got := d.AllocateID(); got != 0 {
381+ t.Fatalf("allocated server-range ID %x", got)
382+ }
383+ d.objects.Store(id, d)
384+ if err := d.handleDisplayEvent(1, body); err == nil {
385+ t.Fatal("live ID deleted")
386+ }
387+ for _, bad := range []uint32{0, 1, 0xff000001} {
388+ binary.LittleEndian.PutUint32(body, bad)
389+ if err := d.handleDisplayEvent(1, body); err == nil {
390+ t.Fatalf("accepted reserved ID %x", bad)
391+ }
392+ }
393+}
@@ -92,6 +92,13 @@ func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
92 BaseProxy: BaseProxy{id: 7, context: d.context},92 BaseProxy: BaseProxy{id: 7, context: d.context},
93 records: &records,93 records: &records,
94 })94 })
95+ for _, opcode := range []uint16{3, 4, 5} {
96+ signature := "uint,"
97+ if opcode == 3 {
98+ signature = "uint,uint,"
99+ }
100+ d.RegisterEventSignature(7, opcode, signature)
101+ }
95 return d, &records102 return d, &records
96 }103 }
97 104
@@ -132,7 +139,7 @@ func TestDisplayDispatch_Fragmented(t *testing.T) {
132 }139 }
133 140
134 func TestDisplayDispatch_Coalesced(t *testing.T) {141 func TestDisplayDispatch_Coalesced(t *testing.T) {
135- first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})142+ first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
136 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})143 second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137 144
138 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})145 d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
@@ -146,7 +153,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
146 }153 }
147 154
148 want := []EventRecord{155 want := []EventRecord{
149- {Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},156+ {Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},157 {Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151 }158 }
152 if !reflect.DeepEqual(*records, want) {159 if !reflect.DeepEqual(*records, want) {
@@ -157,7 +164,7 @@ func TestDisplayDispatch_Coalesced(t *testing.T) {
157 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {164 func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158 // A read boundary that ends between the header and the body of the second165 // A read boundary that ends between the header and the body of the second
159 // message must not be mistaken for a frame boundary.166 // message must not be mistaken for a frame boundary.
160- whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),167+ whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
161 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)168 message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162 169
163 split := HeaderSize + 4 + 4 // mid-body of the second message170 split := HeaderSize + 4 + 4 // mid-body of the second message
@@ -171,7 +178,7 @@ func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
171 }178 }
172 179
173 want := []EventRecord{180 want := []EventRecord{
174- {Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},181+ {Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},182 {Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176 }183 }
177 if !reflect.DeepEqual(*records, want) {184 if !reflect.DeepEqual(*records, want) {
@@ -274,6 +281,10 @@ func TestDisplayDispatch_DisplayError(t *testing.T) {
274 281
275 func TestDisplayDispatch_DeleteID(t *testing.T) {282 func TestDisplayDispatch_DeleteID(t *testing.T) {
276 d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})283 d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
284+ for d.nextID <= 7 {
285+ d.allocateID()
286+ }
287+ d.objects.Delete(uint32(7))
277 288
278 if err := d.Dispatch(); err != nil {289 if err := d.Dispatch(); err != nil {
279 t.Fatalf("Dispatch: %v", err)290 t.Fatalf("Dispatch: %v", err)
@@ -299,7 +310,7 @@ func TestDisplayDispatch_PeerClose(t *testing.T) {
299 },310 },
300 {311 {
301 name: "closed after a complete message",312 name: "closed after a complete message",
302- chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},313+ chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
303 },314 },
304 }315 }
305 316
@@ -350,3 +361,33 @@ func TestReadFrame(t *testing.T) {
350 t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)361 t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351 }362 }
352 }363 }
364+
365+func TestDeleteIDReuseAndInvalid(t *testing.T) {
366+ d := newDisplay(&chunkConn{})
367+ id := d.AllocateID()
368+ body := make([]byte, 4)
369+ binary.LittleEndian.PutUint32(body, id)
370+ if err := d.handleDisplayEvent(1, body); err != nil {
371+ t.Fatal(err)
372+ }
373+ if err := d.handleDisplayEvent(1, body); err == nil {
374+ t.Fatal("duplicate delete_id accepted")
375+ }
376+ d.nextID = 0xff000000 // simulate exhaustion
377+ if got := d.AllocateID(); got != id {
378+ t.Fatalf("reused %d, want %d", got, id)
379+ }
380+ if got := d.AllocateID(); got != 0 {
381+ t.Fatalf("allocated server-range ID %x", got)
382+ }
383+ d.objects.Store(id, d)
384+ if err := d.handleDisplayEvent(1, body); err == nil {
385+ t.Fatal("live ID deleted")
386+ }
387+ for _, bad := range []uint32{0, 1, 0xff000001} {
388+ binary.LittleEndian.PutUint32(body, bad)
389+ if err := d.handleDisplayEvent(1, body); err == nil {
390+ t.Fatalf("accepted reserved ID %x", bad)
391+ }
392+ }
393+}
M wl/wl.go +2 -18
@@ -21,14 +21,7 @@ type (
2121 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler
2222 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent
2323 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent
24- Seat = wlturbo.Seat
25- Surface = wlturbo.Surface
26- Pointer = wlturbo.Pointer
27- Keyboard = wlturbo.Keyboard
28- Touch = wlturbo.Touch
29- Output = wlturbo.Output
30- Region = wlturbo.Region
31- Compositor = wlturbo.Compositor
24+ OwnedFD = wlturbo.OwnedFD
3225
3326 // Transport failures. Without these, a caller that imports this shim has to
3427 // import the root package as well just to classify an error.
@@ -40,10 +33,8 @@ type (
4033 var (
4134 Connect = wlturbo.Connect
4235 NewFixed = wlturbo.NewFixed
43- NewSeat = wlturbo.NewSeat
4436 NewContext = wlturbo.NewContext
45- NewSurface = wlturbo.NewSurface
46- NewCompositor = wlturbo.NewCompositor
37+ CloseSentFD = wlturbo.CloseSentFD
4738 CreateAnonymousFile = wlturbo.CreateAnonymousFile
4839 MapMemory = wlturbo.MapMemory
4940 UnmapMemory = wlturbo.UnmapMemory
@@ -57,10 +48,3 @@ var (
5748 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode
5849 ErrDisplayError = wlturbo.ErrDisplayError
5950 )
60-
61-// Seat capability constants
62-const (
63- SeatCapabilityPointer = wlturbo.SeatCapabilityPointer
64- SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard
65- SeatCapabilityTouch = wlturbo.SeatCapabilityTouch
66-)
@@ -21,14 +21,7 @@ type (
21 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler21 RegistryGlobalRemoveHandler = wlturbo.RegistryGlobalRemoveHandler
22 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent22 RegistryGlobalEvent = wlturbo.RegistryGlobalEvent
23 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent23 RegistryGlobalRemoveEvent = wlturbo.RegistryGlobalRemoveEvent
24- Seat = wlturbo.Seat24+ OwnedFD = wlturbo.OwnedFD
25- Surface = wlturbo.Surface
26- Pointer = wlturbo.Pointer
27- Keyboard = wlturbo.Keyboard
28- Touch = wlturbo.Touch
29- Output = wlturbo.Output
30- Region = wlturbo.Region
31- Compositor = wlturbo.Compositor
32 25
33 // Transport failures. Without these, a caller that imports this shim has to26 // Transport failures. Without these, a caller that imports this shim has to
34 // import the root package as well just to classify an error.27 // import the root package as well just to classify an error.
@@ -40,10 +33,8 @@ type (
40 var (33 var (
41 Connect = wlturbo.Connect34 Connect = wlturbo.Connect
42 NewFixed = wlturbo.NewFixed35 NewFixed = wlturbo.NewFixed
43- NewSeat = wlturbo.NewSeat
44 NewContext = wlturbo.NewContext36 NewContext = wlturbo.NewContext
45- NewSurface = wlturbo.NewSurface37+ CloseSentFD = wlturbo.CloseSentFD
46- NewCompositor = wlturbo.NewCompositor
47 CreateAnonymousFile = wlturbo.CreateAnonymousFile38 CreateAnonymousFile = wlturbo.CreateAnonymousFile
48 MapMemory = wlturbo.MapMemory39 MapMemory = wlturbo.MapMemory
49 UnmapMemory = wlturbo.UnmapMemory40 UnmapMemory = wlturbo.UnmapMemory
@@ -57,10 +48,3 @@ var (
57 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode48 ErrUnknownOpcode = wlturbo.ErrUnknownOpcode
58 ErrDisplayError = wlturbo.ErrDisplayError49 ErrDisplayError = wlturbo.ErrDisplayError
59 )50 )
60-
61-// Seat capability constants
62-const (
63- SeatCapabilityPointer = wlturbo.SeatCapabilityPointer
64- SeatCapabilityKeyboard = wlturbo.SeatCapabilityKeyboard
65- SeatCapabilityTouch = wlturbo.SeatCapabilityTouch
66-)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close