Wayland objects, protocol, and wire format

Explain Wayland object identities, wire messages and protocol lifetime.

15 min readChapter 124 of 168Intermediate → advanced
In this chapter

Before you start#

Read Vulkan, OpenGL, EGL, and GBM and Unix sockets and descriptor passing. This chapter uses the independently pinned Wayland 1.23.0 source and protocol XML. It explains a local IPC protocol; it does not connect to your compositor, capture a screen or inspect input.

Learning goals#

Decode a small source-validated message, distinguish object IDs and protocol interfaces, explain file-descriptor side channels and event queues, and identify cleanup and error paths around disconnection and callbacks.

Source walkthrough#

Wayland message and descriptor channels
Wayland message and descriptor channelsMessage header: Object ID, size and opcode → Object map: ID selects an object and interface (select object). Arguments: Interface-defined, padded wire values → Dispatcher: Signature validates and decodes arguments (decode by signature). Ancillary descriptors: SCM_RIGHTS carries file references separately → Receiver: Owns received descriptors under protocol rules (transfer fd)select objectdecode by signaturetransfer fdMessage headerObject ID, size and opcodeArgumentsInterface-defined, paddedwire valuesAncillary descriptorsSCM_RIGHTS carries filereferences separatelyObject mapID selects an object andinterfaceDispatcherSignature validates anddecodes argumentsReceiverOwns received descriptorsunder protocol rules
Message headerObject ID, size and opcode
Object mapID selects an object and interface
ArgumentsInterface-defined, padded wire values
DispatcherSignature validates and decodes arguments
Ancillary descriptorsSCM_RIGHTS carries file references separately
ReceiverOwns received descriptors under protocol rules
Read the text version
  • Message header: Object ID, size and opcode → Object map: ID selects an object and interface (select object)
  • Arguments: Interface-defined, padded wire values → Dispatcher: Signature validates and decodes arguments (decode by signature)
  • Ancillary descriptors: SCM_RIGHTS carries file references separately → Receiver: Owns received descriptors under protocol rules (transfer fd)
Conceptual model. Wire bytes and ancillary file descriptors travel together on a Unix socket but are distinct data; values depend on the declared interface.

Wayland is a protocol between a client and compositor/server, normally transported over a Unix-domain socket. Each connection has an object map. An object has an integer ID and an interface defining its requests, events and version. A request is sent by the client to a server object; an event travels in the other direction. The opcode is interpreted in the context of that object's interface, not as a global operation number. Client-side proxies represent remote objects; server-side resources represent client-visible objects under server lifetime rules. See the pinned wayland.xml, wayland-client.c, and wayland-server.c.

The XML describes interfaces, request/event order, argument types, nullable arguments, destructor requests and since versions. wayland-scanner generates language bindings and interface metadata from XML; the generated functions marshal messages according to those signatures. A client must not use a request introduced after its bound object's negotiated version. A server can report a protocol error and disconnect a client that sends invalid object IDs, arguments or state. The XML is the contract for the specific version, not evidence that a running compositor advertises every extension.

The first wire word is the sender object ID; the second packs message byte length in the upper 16 bits and opcode in the lower 16 bits. Numeric words use the connection's native byte order, and variable-length data are padded to four-byte boundaries. Strings include a length and terminating NUL when non-null; arrays have their own length and padding. File descriptors are sent as Unix socket ancillary SCM_RIGHTS data, not as integer descriptor values embedded in the ordinary message bytes. A receiver must associate the side-channel descriptors with the message signature and own/close them under the protocol's rules.

In Wayland 1.23.0 connection.c, serialize_closure, the file carries an MIT-style permissive license. The header is formed by these exact statements:

c
	buffer[0] = closure->sender_id;
	buffer[1] = size << 16 | (closure->opcode & 0x0000ffff);

The first stores the object ID; the second combines the computed message size and masked opcode. Earlier in that same function, FD arguments are skipped in the ordinary word buffer, while object and new_id arguments are encoded according to the signature. wl_closure_send separately copies descriptors to the connection and serializes/writes the byte buffer. This separation prevents mistaking a process-local fd number for a wire-level object ID.

A concrete 12-byte message#

The core protocol's first wl_display request is sync, with a new_id argument for a wl_callback. Assume a little-endian client, wl_display object ID 1, and a newly allocated callback ID 2 that is valid and unused in this connection. The request opcode is 0 and the message contains an eight-byte header plus one four-byte new_id, so its size is 12 bytes. The authored example is:

text
01 00 00 00   00 00 0c 00   02 00 00 00
object 1      size 12/op 0    new callback ID 2

The second word is numeric 0x000c0000, serialized little-endian as 00 00 0c 00. On a big-endian peer the native-order byte representation differs; the example is not a universal literal byte sequence. There is no fd in this particular request. The server processes earlier requests before emitting the callback's done event according to the protocol's sync semantics; that event is a synchronization point for protocol processing, not a guarantee that pixels were displayed or GPU work completed. The object ID is scoped to this connection, not a global kernel handle.

Sync request, field by field

Select a field to connect its position, bytes, and meaning.

12 bytes in file order. Offsets start at zero.

Selected: sender object ID. wl_display is object 1 in this connection.

Read the byte table
OffsetFieldHex bytesInterpretation
0sender object ID01 00 00 00wl_display is object 1 in this connection
4size and opcode00 00 0c 00size 12 in the upper 16 bits; opcode 0 (sync) in the lower 16
8new_id argument02 00 00 00wl_callback object 2 created by the request
Conceptual model. Authored byte layout for the 12-byte wl_display.sync example with a little-endian peer, computed from the Wayland 1.23.0 header rule and the core XML. Not a captured connection.

In wayland.xml, wl_display.sync, Wayland 1.23.0 core protocol, MIT-style permission notice; the request is quoted unchanged:

text
    <request name="sync">
      <description summary="asynchronous roundtrip">
	The sync request asks the server to emit the 'done' event
	on the returned wl_callback object.  Since requests are
	handled in-order and events are delivered in-order, this can
	be used as a barrier to ensure all previous requests and the
	resulting events have been handled.

	The object returned by this request will be destroyed by the
	compositor after the callback is fired and as such the client must not
	attempt to use it after that point.

	The callback_data passed in the callback is undefined and should be ignored.
      </description>
      <arg name="callback" type="new_id" interface="wl_callback"
	   summary="callback object for the sync request"/>
    </request>

The new_id is the only argument, so the body is four bytes after the eight-byte header. The description calls sync a barrier for processing order and says callback_data is undefined. Nothing in this XML shows when the done event is sent on a real connection or any display result.

For an FD-bearing comparison, wl_shm.create_pool has new_id, fd and size arguments in the pinned XML. The new object ID and size are represented in the ordinary protocol message; the actual file reference is carried via SCM_RIGHTS. The integer the sender happened to call fd 5 is not a meaningful receiver fd number. The receiving process gets its own descriptor and must follow ownership and error cleanup rules. Buffer contents and access rights require additional validation: a shared-memory pool is not automatically trusted simply because it came from a local client.

Pool creation with a side-channel fd
Pool creation with a side-channel fd1. Client process to Unix socket: create_pool bytes: new_id, size. 2. Client process to Unix socket: fd via SCM_RIGHTS, not in the bytes. 3. Unix socket to Compositor: receiver installs its own fd number. 4. Compositor to Client process (return): mmap size bytes as pool storage.Client processUnix socketCompositorcreate_pool bytes:new_id, sizefd via SCM_RIGHTS, notin the bytesreceiver installs itsown fd numbermmap size bytes as pool storage
  1. Client processUnix socket

    create_pool bytes: new_id, size

  2. Client processUnix socket

    fd via SCM_RIGHTS, not in the bytes

  3. Unix socketCompositor

    receiver installs its own fd number

  4. CompositorClient process

    Return. mmap size bytes as pool storage

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Client process → Unix socket: create_pool bytes: new_id, size
  2. Client process → Unix socket: fd via SCM_RIGHTS, not in the bytes
  3. Unix socket → Compositor: receiver installs its own fd number
  4. Compositor → Client process (return): mmap size bytes as pool storage
Conceptual model. Teaching model of wl_shm.create_pool. The fd travels as SCM_RIGHTS ancillary data, not as a byte in the message; the descriptor number shown is local to each process. Not a captured session.

Marshalling, IDs, and errors#

In wl_closure_marshal, Wayland 1.23.0, MIT-style, the FD argument branch duplicates the descriptor with close-on-exec and handles failure. This excerpt is from that branch:

c
			dup_fd = wl_os_dupfd_cloexec(fd, 0);
			if (dup_fd < 0) {
				wl_closure_destroy(closure);

The source duplicates the descriptor for the closure's use; failure destroys the partially constructed closure before returning an error in the following statements. The original application's descriptor and the duplicate have separate descriptor lifetimes while referring to the underlying file according to Unix rules. The full marshalling function validates non-nullable arguments and handles object/new-ID forms. A missing fd, allocation failure or invalid object can fail before any complete request is queued.

Client-created and server-created IDs occupy different allocation ranges in this implementation; WL_SERVER_ID_START is defined as 0xff000000 in wayland-private.h. Allocation, delete_id and object-map rules prevent reuse while an ID is still in use. An integer that once named a destroyed proxy must not be assumed to name the same object after reuse. A new_id creates a new protocol object according to the message definition; it is not a transferable fd or a C pointer on the wire.

wl_proxy_marshal_array_flags in the client library takes the display mutex, can create an outgoing proxy, constructs a closure and queues a request. If construction or send fails, it follows error cleanup and records a fatal display error where applicable. A disconnect or protocol error is not repaired by continuing to send arbitrary requests on the same failed connection. The client's proxy lifetime and listener data lifetime must be coordinated: destroying a proxy while a callback or another thread can still use associated application memory requires a correct ownership protocol. The display mutex does not automatically protect every field in application-owned objects.

Event queues and the socket#

Events read from the socket are decoded and assigned to event queues. Dispatch invokes callbacks for the chosen queue; a callback can create or destroy application state, so reentrancy and thread ownership matter. A typical event-loop integration dispatches pending events, prepares to read, flushes outgoing requests, waits for socket readiness, then either reads events or cancels the prepared read. In Wayland 1.23.0, a successful wl_display_prepare_read_queue must be paired with wl_display_read_events or wl_display_cancel_read; forgetting the pairing can block other readers. EAGAIN on a nonblocking flush is different from a fatal connection error: the application waits for writability and handles the exact API result.

A wl_display.sync callback establishes ordering of requests processed by the server before that callback; it is not a frame-presentation timestamp. A surface commit, buffer release and frame callback have distinct meanings and are developed in the next chapter. A client must not free or overwrite shared buffer storage until the relevant protocol and synchronization contract permits it. Object destruction is also not equivalent to immediate freeing of every buffer or callback that another thread still references. On server disconnect, pending work and received fds need cleanup under library and application ownership rules.

Bounds, versions, and a failure case#

Suppose the 12-byte sync request above were instead advertised with a size smaller than the eight-byte header, or referred to an unknown object ID. The decoder cannot safely interpret it as the valid request. The pinned connection.c checks a minimum header and argument bounds during closure decoding; the server's protocol machinery can report an error and terminate the connection. A malicious client must not be trusted to supply a correct length, object type or number of ancillary fds. The protocol's native endianness is suitable for local peers on the same system, but a byte-dump tool must know the emitting machine's endianness and the message signature.

Version gating prevents a client from sending a request that the bound object version does not support. A registry global advertises a version, and the client binds a supported version no higher than it can use and the server advertises. Generated bindings supply request metadata, but application state and callback order still need correct handling. Optional protocol extensions have their own XML and versions; core Wayland 1.23.0 does not imply a particular compositor implements every extension. A local source tag is not the running compositor's negotiated version.

Scanner output and protocol metadata#

The pinned scanner.c builds message tables from the parsed XML. In emit_messages, it emits a since prefix where applicable, a nullable marker for eligible arguments and signature characters such as u, n and h for unsigned integers, new IDs and file descriptors. A generic new-ID argument can also carry interface and version information. emit_code emits separate request and event tables and associates them with interface metadata. These generated tables explain how the serializer and dispatcher know what each argument means; they do not remove the need for the client and server to negotiate object versions and manage callback lifetimes.

A message is validated against an interface#

The server cannot interpret opcode 0 in isolation. It first finds object 1 in that connection's object map, verifies that object is a wl_display for the worked example, then interprets opcode 0 using the request table for that interface. The XML declares sync as the first request and its argument as a new callback object. If object 1 instead had another interface, the same opcode number could mean something else. An unknown object, invalid opcode, wrong argument shape or unsupported version can lead to protocol failure; a robust server must not trust a client's object IDs as raw pointers.

The client allocates a valid new ID and creates proxy bookkeeping for the callback. The server creates the corresponding resource under its own implementation and sends an event defined by that interface. Destruction and delete_id coordination determine when client-allocated IDs can be reused. In the 1.23.0 implementation, server-created IDs use a separate high range beginning at the constant cited above. These are protocol object IDs within one connection; Unix fds have separate per-process descriptor tables and lifetime rules.

A new_id argument can have a statically specified interface or be accompanied by interface/version information in a generic binding request. The receiver must apply the declared signature and version, not assume that every new object is a callback. Nullable arguments have explicit protocol meaning: zero can represent null only where the signature permits it. wl_closure_marshal rejects certain null arguments and duplicates FD arguments for the closure, with error cleanup when duplication fails. Generated bindings help encode the signature, but a caller can still misuse object state or its own listener data.

Stream framing and ancillary data#

A Unix stream socket can return only part of a Wayland message in one read or several messages together. The connection buffer accumulates bytes until the declared message length is available; it must validate the header and bounds before decoding arguments. Four-byte alignment includes padding after strings and arrays, while the encoded length has the meaning defined by the argument type. A debugging tool that splits on individual read calls will not necessarily split at protocol message boundaries. The worked 12-byte sync message is one complete logical message, not a guarantee of one socket read.

Ancillary descriptor delivery has its own queue and limits. wl_closure_send first copies the closure's descriptors into the connection's outgoing fd handling, then serializes the message bytes; wl_connection_flush later handles transmission. The receiver needs both the message signature and the received fd sequence to associate descriptors correctly. If descriptor duplication, queuing, sending or decoding fails, the owning layer must close its references on the relevant cleanup path. Passing an fd transfers access to an underlying file object; it is an authority boundary and does not make a malicious buffer trustworthy.

Event-loop ownership and failure#

A common mistake is to prepare a read and then return from a callback without either reading events or canceling the prepared read. The library's documented pairing matters when other threads also participate in reading. A separate mistake is to dispatch a callback on a queue whose application-owned listener data have already been freed. The display lock and queue mechanics handle library state, while the application must coordinate its own object lifetime and thread ownership. A proxy can be logically destroyed before every unrelated application task has stopped using its pointer.

If the compositor disconnects, the socket can become readable for EOF and subsequent library calls report an error according to the API. A client should stop using the failed display, release local proxies and associated resources according to its design, and reconnect only through a new connection if appropriate. It should not reuse the old connection's object IDs as if the new server inherited them. A sync callback confirms ordering of earlier protocol requests, while a buffer-release event and presentation feedback answer different questions. The local Python program checks only header arithmetic; it does not exercise these runtime transitions.

Padding and message boundaries in the worked model#

The sync request has one fixed-width new_id argument, so its payload is exactly four bytes and no extra padding is needed. A string argument would instead have its own encoded length including its terminating NUL and occupy a whole number of four-byte words after padding. The padded bytes are not a second protocol argument. The receiver checks the advertised message size against the signature and available buffered bytes before accepting the closure; an application should not treat a short socket read as a complete message merely because it contains the first header word.

The sender and receiver are local peers using the protocol's native-endian representation. The example deliberately specifies little-endian to make each byte visible. A diagnostic tool reading a saved byte stream must retain architecture and signature context, while the two endpoints on a normal connection share the protocol's local transport assumptions. The example's ID 2 is valid only under its explicitly stated unused-ID condition; it is not a reserved global callback number.

Practice#

Source-only route: inspect serialize_closure and wl_closure_marshal in Wayland 1.23.0 src/connection.c, the wl_display.sync and wl_shm.create_pool declarations in protocol/wayland.xml, and wl_proxy_marshal_array_flags in src/wayland-client.c. Decode the supplied three 32-bit words: sender 1, size 12/opcode 0, new callback 2. Explain why a file descriptor for a different signature is not a fourth ordinary word. The byte example is authored from the inspected source and XML, not a captured client session.

Run this local arithmetic check without connecting to a Wayland display:

sh
python3 - <<'PY'
import struct
sender, opcode, callback = 1, 0, 2
packet = struct.pack('<III', sender, (12 << 16) | opcode, callback)
print('bytes:', packet.hex(' '))
words = struct.unpack('<III', packet)
print('sender:', words[0], 'size:', words[1] >> 16, 'opcode:', words[1] & 0xffff, 'new_id:', words[2])
PY

Expected 01 00 00 00 00 00 0c 00 02 00 00 00 and sender: 1 size: 12 opcode: 0 new_id: 2. The program deliberately chooses little-endian encoding for the example; it does not assert that the current host is little-endian or send a protocol request. Python unavailable or a failed assertion/command is a valid limitation. Deliver the decoded words, FD-side-channel explanation and prepare-read/cancel-read pairing.

Knowledge check#

  1. Why does opcode 0 need an object interface as well as a number?
  2. How are size and opcode represented in the worked header?
  3. Why is an SCM_RIGHTS fd not encoded as the sender's integer in ordinary Wayland bytes?
  4. What must follow a successful prepare-read call?
  5. Does wl_display.sync prove that a frame appeared on a monitor?

Answers#

  1. Opcodes are defined per interface and message direction; the object ID determines which interface's opcode table applies.
  2. The second 32-bit word is 0x000c0000: size 12 in its upper half and opcode 0 in its lower half, shown in little-endian bytes for this example.
  3. Unix descriptor passing transfers a file reference as ancillary data and installs a receiver-local descriptor; the sender's integer is process-local.
  4. The thread must call read-events or cancel-read according to the library's pairing contract.
  5. No. It orders protocol processing; rendering, buffer release and presentation have separate contracts.

Checkpoint#

Save the byte decoding and object/fd lifetime sketch. Pass when you can validate the 12-byte example, distinguish the FD side channel, and explain version and event-queue cleanup. Re-read the XML and serializer if any word or ownership transition is unclear. This is practice, not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.