7 Requests and Events
dxtr edited this page 2026-10-18 16:19:56 +02:00

Requests and Events

Requests

Each request is a function named interface-request. It takes the proxy first and then the request's arguments in protocol order:

(wl-surface-attach surface buffer 0 0)
(wl-surface-damage-buffer surface 0 0 width height)
(xdg-toplevel-set-title toplevel "Hello")
(wl-surface-commit surface)

Before anything is sent, each request:

  1. checks the type of each argument against the function's declaimed ftype, so a wrong argument gives a type-error, and SBCL warns at compile time where it can tell;

  2. signals destroyed-proxy-error if the proxy has been destroyed;

  3. signals version-error if the request is newer than the proxy's version:

    WL-SURFACE-OFFSET needs version 5 of wl_surface, but #<WL-SURFACE id 6 v3> has version 3.
    

What a request returns:

  • A request that creates an object returns the new proxy, which has the parent's version: (wl-compositor-create-surface compositor) returns a wl-surface.
  • wl-registry-bind, whose new object's interface is chosen by the caller, takes the proxy class and version as extra arguments.
  • Every other request returns no values.

A destructor request, such as wl-surface-destroy or wl-pointer-release, destroys the proxy after sending it. You rarely need to pick one by hand: destroy-proxy sends the interface's destructor request if the proxy's version has it, and otherwise destroys the proxy on the client side only. wl-pointer-release, for instance, exists only from version 3. See Lifetimes and Threads.

Requests are buffered in libwayland until flush, dispatch, read-events or roundtrip sends them. Call flush when you need the compositor to see a request now and are not about to dispatch, for example after a commit from a render thread.

The documentation of each request function lists its arguments and repeats the protocol's description:

(documentation 'wl-registry-bind 'function)
;; "bind an object to the display
;;
;;  Binds a new, client-created object to the server using the
;;  specified name as the identifier.
;;
;;  Arguments:
;;    NAME -- unique numeric name of the object
;;    INTERFACE -- the proxy class of the interface to create
;;    VERSION -- the interface version to create
;;
;;  Returns the new proxy."

Events

Each event of a proxy can have its own handler. A handler is called with the proxy and the event's arguments:

(wl:set-event-handlers pointer
  :enter (lambda (pointer serial surface x y)
           (declare (ignore pointer serial))
           (format t "entered ~A at ~,1F,~,1F~%" surface x y))
  :motion (lambda (pointer time x y)
            (declare (ignore pointer time))
            (setf *cursor* (cons x y)))
  :button (lambda (pointer serial time button state)
            (declare (ignore pointer serial time))
            (format t "button ~D ~S~%" button state)))   ; state is :pressed or :released
  • set-event-handlers sets several handlers from alternating keywords and functions and returns the proxy.

  • (setf (wl:event-handler proxy :motion) #'on-motion) sets one handler. (wl:event-handler proxy :motion) reads it back.

  • A handler may be a function or a symbol naming one, and NIL removes it.

  • Events without a handler are dropped.

  • An event the interface does not have signals wl-error and lists the events it does have:

    wl_seat has no :CAPABILITY event.  Its events are :CAPABILITIES, :NAME.
    

The documentation of each proxy class lists its events, the order of their arguments, and the Lisp type of each argument:

(documentation 'wl-pointer 'type)
;; …
;; Events, each passed to its EVENT-HANDLER with the proxy and
;; the arguments shown:
;;   (:enter SERIAL SURFACE SURFACE-X SURFACE-Y) -- enter event
;;     SERIAL -- serial number of the enter event
;;     SURFACE -- surface entered by the pointer (wl-surface)
;;     SURFACE-X -- surface-local x coordinate (single-float)
;;     SURFACE-Y -- surface-local y coordinate (single-float)
;;   …
;;   (:frame) -- end of a pointer event sequence (since version 5)

When handlers run

libwayland decodes events inside a C callback. cl-wl's callback only turns the arguments into Lisp values and queues the event. The handlers run later, from dispatch, dispatch-pending or roundtrip, after libwayland has returned. As a result:

  • Handlers may signal and unwind freely. If a handler unwinds, the event that was being handled has already left the queue. The events after it stay queued and are delivered by the next dispatch.
  • Handlers may send requests, create and destroy proxies, and set other handlers. They may even dispatch again.
  • Handlers run in the thread that dispatches.
  • If a handler destroys a proxy, events still queued for that proxy are dropped.

Destructor events

Some events end their object: once the compositor has sent one, the object is gone on its side. The protocol marks them as destructors. In the protocols cl-wl ships, that is wl_callback's :done. cl-wl destroys the proxy after its handler has run, whether the handler returns or unwinds, and also when the event has no handler. The handler can still use the proxy, and you never destroy it yourself:

(wl:set-event-handlers (wl-surface-frame surface)
  :done (lambda (callback time)
          (declare (ignore callback))
          (draw-next-frame time)))     ; the callback is destroyed afterwards

Calling destroy-proxy in the handler anyway, as code ported from C does, is harmless. A class's documentation marks these events with "destroys the proxy after its handler".

Events that create objects

Some events create objects. wl_data_device.data_offer, for instance, announces a new wl_data_offer. The handler receives the new proxy. Set its handlers in the handler itself, before you return, so that none of its events are missed.

Values

Wire type In events In requests
int (signed-byte 32) (signed-byte 32)
uint (unsigned-byte 32) (unsigned-byte 32)
int/uint with an enum keyword; for a bitfield, a list of keywords; an unknown value as an integer keyword, list of keywords for a bitfield, or integer
fixed single-float any real in [-8388608, 8388608), rounded to 1/256
string string; NIL if the protocol allows null string; NIL if allowed
object proxy; NIL if allowed proxy; NIL if allowed
new_id the new proxy not an argument: the request returns the proxy
array see below see below
fd integer; you own it and must close it integer; libwayland duplicates it, so you keep ownership

Strings are UTF-8 both ways. An event string that is not valid UTF-8 causes an event-error; see Errors.

Fixed-point numbers

Wayland's fixed is 24.8 fixed point. Events deliver it as a single-float, which allocates nothing. It is exact for magnitudes below 65536, which covers surface coordinates, and is rounded to single-float precision above. Requests take any real in the type cl-wl:fixed and round it to the nearest 1/256.

Enums

An argument that the protocol ties to an enum arrives as a keyword:

:button (lambda (pointer serial time button state) …)   ; STATE is :PRESSED or :RELEASED

A bitfield enum arrives as a list of keywords:

:capabilities (lambda (seat capabilities) …)   ; e.g. (:POINTER :KEYBOARD)

A value the bindings do not know, from a compositor newer than them, arrives as an integer rather than being lost. In a bitfield, the unknown bits are appended to the list as one integer:

(wl:enum-keywords 'wl-seat-capability 11)   ; => (:POINTER :KEYBOARD 8)

Requests accept a keyword, a list of keywords for a bitfield, or a raw integer:

(wl-shm-pool-create-buffer pool 0 w h stride :xrgb8888)
(xdg-toplevel-resize toplevel seat serial :bottom-right)

Each enum is a Lisp type, so you can write (typep x 'wl-shm-format). For a bitfield the type is list. enum-value and enum-keywords convert by hand:

(wl:enum-value 'wl-seat-capability '(:pointer :keyboard))  ; => 3
(wl:enum-value 'xdg-toplevel-resize-edge :bottom-right)    ; => 10
(wl:enum-keywords 'wl-output-transform 3)                  ; => :|270|

Entries whose names are numbers read as numbers when written plainly, so they are written with bars: :|90|, :|180|, :|270|.

The keyword lists of bitfields in events are cached and shared per display. Do not modify them destructively; copy one first if you need to.

Arrays

The protocol format does not say what an array holds. The prose says it, so cl-wl lists the arrays whose contents are known and decodes them:

Array Delivered as
xdg_toplevel.configure states list of xdg-toplevel-state keywords, such as (:maximized :activated)
xdg_toplevel.wm_capabilities capabilities list of xdg-toplevel-wm-capabilities keywords
wl_keyboard.enter keys (vector (unsigned-byte 32)) of keycodes
any other array (vector (unsigned-byte 8))

In requests, an array of integers or enum values takes any sequence of them, and any other array takes an octet vector. (cl-wl/scanner:shipped-array-elements) returns the list. See The Scanner to extend it.

File descriptors

An fd you receive, such as wl_keyboard.keymap's, is yours: close it with close-fd when you are done. read-keymap reads a keymap fd and closes it for you, so call one or the other:

(wl:set-event-handlers
 keyboard
 :keymap (lambda (keyboard format fd size)
           (declare (ignore keyboard))
           (if (eq format :xkb-v1)
               (use-keymap (wl:read-keymap fd size))
               (wl:close-fd fd))))

An fd you send, such as wl_shm.create_pool's, is duplicated by libwayland, so it stays yours too. A shared-memory's fd belongs to the shared memory, and destroy-shared-memory closes it.

Pipes

Some requests and events pass the end of a pipe, through which data moves outside the protocol: the clipboard's text, for one. make-pipe makes a pipe, receive-text reads text until the writer closes its end, and send-text writes text. Each closes its fd and gives up after :timeout seconds, so a client that never writes, or never reads, cannot hold up yours:

(multiple-value-bind (in out) (wl:make-pipe)
  (wl-data-offer-receive offer "text/plain;charset=utf-8" out)
  (wl:flush display)
  (wl:close-fd out)                      ; the other client holds its own copy
  (wl:receive-text in :timeout 1))       ; => the text, or NIL after a second

Do not wait in receive-text for text your own client sends: it is sent only while your client dispatches events. See Recipes for the whole clipboard.