6 Lifetimes and Threads
dxtr edited this page 2026-10-17 04:50:30 +02:00

Lifetimes and Threads

Lifetimes

Proxies

A proxy lives until one of these happens:

How What it does
destroy-proxy sends the interface's destructor request if the proxy's version has one, and otherwise destroys the proxy on the client side only
a destructor request, such as wl-surface-destroy or wl-pointer-release tells the compositor, then destroys the proxy
a destructor event, such as wl_callback's :done destroys the proxy after its handler has run, or unwound
disconnect destroys every remaining proxy of the display

destroy-proxy is the one to reach for. It sends wl_output.release for a version-3 output and destroys a version-2 output on the client side, which has no release request. It also destroys wl_registry on the client side, since that interface has no destructor at all. It does not send a destructor whose request takes arguments; in the protocols cl-wl ships, none does.

After that the proxy is dead. proxy-live-p returns NIL, its printed form says destroyed, and any request on it signals destroyed-proxy-error. cl-wl clears the proxy's pointer when it destroys it, so a dead proxy never reaches freed memory, even if it is kept in a variable. Destroying a dead proxy does nothing.

Events already queued for a proxy that is destroyed before they are delivered are dropped.

Garbage collection does not destroy proxies. A display keeps every live proxy, so an object you drop without destroying stays alive on both sides until disconnect. Destroy objects explicitly. Frame callbacks are the exception that takes care of itself: their :done destroys them.

Destruction order

The protocol defines the order for some objects. For example, an xdg_toplevel must be destroyed before its xdg_surface, and that before its wl_surface. Breaking the order is a protocol error, and destroy-proxy does not reorder anything. As a rule of thumb, destroy children first:

(dolist (object (list toplevel xdg-surface surface))
  (wl:destroy-proxy object))

Displays

disconnect, or leaving with-display, destroys every proxy, closes the connection and frees the foreign interface tables the display built. Disconnecting twice is harmless. Afterwards the display object is dead like a proxy.

A display made by connect-to-fd owns its fd and closes it on disconnect.

Shared memory

A shared-memory lives until destroy-shared-memory, or the end of with-shared-memory. Garbage collection does not free it. The compositor maps the memory itself when a pool is created from it, so pools and buffers outlive the client's shared-memory. Only writing needs it. Do not write to the part of the memory a buffer uses between committing the buffer and its :release event.

File descriptors

An fd received in an event is yours, so close it with close-fd; read-keymap closes a keymap's fd for you, and receive-text and send-text the pipe ends they are given. An fd passed in a request is duplicated by libwayland, so it is still yours to close as well: close the write end of a pipe once you have passed it. A shared-memory's fd belongs to it, and destroy-shared-memory closes it.

Borrowed proxies and cursor themes

A proxy from borrow-proxy wraps a wl_proxy that other C code owns. The owner destroys it: destroy-proxy and disconnect only forget the proxy. Forget it before the owner destroys the object.

A cursor theme of cl-wl/cursor owns its images' buffers, which are such borrowed proxies. Destroy the theme with destroy-cursor-theme before disconnecting its display; never destroy its buffers yourself. See Cursors.

Raw pointers

The pointer proxy-pointer returns is valid until its proxy is destroyed or its display disconnected. Anything built on it, such as a Vulkan surface, must be destroyed first. shared-memory-pointer is valid until the memory is resized or destroyed, and interface-pointer until its tables are freed.

Images

cl-wl keeps no foreign memory globally. The interface tables that libwayland needs are built per display on first use, and freed on disconnect. Loading cl-wl into an image and saving it with save-lisp-and-die therefore needs no special care. Do not save an image with a display open or shared memory mapped.

Threads

A display, and every proxy created on it, may be used by one thread at a time. That covers requests, handlers, dispatching and destruction. The one exception is read-events, which may wait on the display while another thread uses it. display-error and flush may run while another thread waits in read-events.

Different displays are independent and may be used on different threads.

Object May be shared between threads?
display Yes, if every use except read-events holds a lock you provide.
proxy Follows its display.
global-follower Follows its display.
shared-memory One thread at a time. It belongs to no display.
interface-tables One thread at a time.
Event handlers Run in the thread that dispatches.
Interface and enum descriptions (message, argument, enum-value, …) Yes. They are read-only once bindings load.
Bitfield keyword lists delivered in events Read-only and shared; do not modify them.

The pattern for an event thread is in Event Loops.

Handlers never run inside libwayland's callback. They run in the thread that called dispatch, dispatch-pending or roundtrip, after libwayland has returned. They may therefore take locks, signal and unwind.

Allocation

Events, requests and pixels happen every frame, so in steady state cl-wl allocates nothing to:

  • decode and queue an event, since the queue's records are reused once it has grown to its working size;
  • call a handler with up to ten arguments;
  • send a request that creates no object;
  • decode a fixed, delivered as an immediate single-float;
  • copy pixels with write-shared-memory;
  • wait for events, in dispatch or read-events, even with a timeout of 0 each frame;
  • find a cursor of a theme, its frame, or its image's buffer again.

The exceptions are values that must be new Lisp objects:

  • strings and arrays in events;
  • new proxies, from requests that create objects and events that announce them, such as the wl_callback of each frame;
  • the first occurrence of each bitfield value per display. Its keyword list is cached and reused afterwards, which is why you must not modify it.

The test suite checks this by counting the bytes allocated over many events, requests and writes.