Documentation Index
Fetch the complete documentation index at: https://mintlify.com/smithay/wayland-rs/llms.txt
Use this file to discover all available pages before exploring further.
wayland-server is the Rust crate that powers the server side of the Wayland protocol. It provides the building blocks every compositor needs: a typed Display<State> that owns all protocol state, a ListeningSocket that accepts incoming client connections, GlobalDispatch for advertising capabilities, and Dispatch for routing client requests to your application logic. This guide walks through each step and finishes with a complete, runnable compositor skeleton.
Add wayland-server to your project
Open your
Cargo.toml and add wayland-server. Choose the features that match your deployment target.[package]
name = "my-compositor"
version = "0.1.0"
edition = "2024"
[dependencies]
wayland-server = { version = "0.31", features = ["system"] }
[features]
# Use the system libwayland-server.so instead of the built-in Rust backend
system = ["wayland-server/system"]
# dlopen: load libwayland-server.so at runtime instead of link time
dlopen = ["wayland-server/dlopen"]
# Opt-in API additions available in libwayland >= 1.22
libwayland_1_22 = ["wayland-server/libwayland_1_22"]
# Opt-in API additions available in libwayland >= 1.23 (implies libwayland_1_22)
libwayland_1_23 = ["wayland-server/libwayland_1_23"]
Without any features
wayland-server uses an internal pure-Rust backend and does not require libwayland-server.so to be installed. Enable system (or dlopen) only if you need FFI interop, e.g. passing raw wl_display pointers to Mesa.Display<State> owns the entire protocol state of your compositor. The type parameter State is your application-level struct — every callback receives a &mut State.use wayland_server::Display;
struct AppState {
// your compositor fields go here
running: bool,
}
fn main() -> std::io::Result<()> {
let mut display: Display<AppState> = Display::new()
.expect("Failed to create Wayland display");
let mut state = AppState { running: true };
// ...
Ok(())
}
ListeningSocket manages the Unix-domain socket that clients connect to. It creates the socket file inside $XDG_RUNTIME_DIR and holds an exclusive lockfile for the lifetime of the process.use wayland_server::ListeningSocket;
let socket = ListeningSocket::bind("wayland-0")
.expect("Failed to bind wayland-0");
// Or auto-select the first free slot: wayland-1, wayland-2, …
// let socket = ListeningSocket::bind_auto("wayland", 0..=9).unwrap();
println!("Listening on {:?}", socket.socket_name());
Clients discover the compositor socket via the
WAYLAND_DISPLAY environment variable. Set it to the socket name after binding so child processes (your clients) find your compositor automatically.Every capability you expose to clients is a global. Implement
GlobalDispatch<Interface, State> on the user-data type you pass to create_global(). When a client calls wl_registry.bind, the bind() method is invoked and you must initialize the newly created resource.use wayland_server::{
Client, DataInit, DisplayHandle, GlobalDispatch, New,
protocol::wl_compositor::WlCompositor,
};
pub struct CompositorState;
impl GlobalDispatch<WlCompositor, AppState> for CompositorState {
fn bind(
&self,
state: &mut AppState,
_handle: &DisplayHandle,
_client: &Client,
resource: New<WlCompositor>,
data_init: &mut DataInit<'_, AppState>,
) {
// Initialize the resource with per-object user data.
// Failing to call data_init.init() here causes a panic.
data_init.init(resource, ());
}
}
Dispatch<Resource, State> is implemented on your user-data type and drives per-request handling. Every incoming request from a client arrives here.use wayland_server::{
Client, DataInit, Dispatch, DisplayHandle, Resource,
protocol::wl_compositor::{self, WlCompositor},
};
impl Dispatch<WlCompositor, AppState> for () {
fn request(
&self,
state: &mut AppState,
_client: &Client,
resource: &WlCompositor,
request: wl_compositor::Request,
dhandle: &DisplayHandle,
data_init: &mut DataInit<'_, AppState>,
) {
match request {
wl_compositor::Request::CreateSurface { id } => {
// id is a New<WlSurface> — must be initialized
data_init.init(id, ());
}
wl_compositor::Request::CreateRegion { id } => {
data_init.init(id, ());
}
_ => {}
}
}
}
Wire the
Display file descriptor and the ListeningSocket file descriptor into your preferred event loop (here shown with a simple poll-based loop). Call dispatch_clients() whenever the display fd becomes readable and flush_clients() after every iteration.use std::os::unix::io::AsFd;
use wayland_server::{Display, ListeningSocket};
fn run(mut display: Display<AppState>, socket: ListeningSocket, mut state: AppState) {
loop {
// Accept any newly connected clients
if let Some(stream) = socket.accept().unwrap() {
let client_data = std::sync::Arc::new(MyClientData);
display.handle().insert_client(stream, client_data)
.expect("Failed to insert client");
}
// Dispatch all pending requests from every client
display.dispatch_clients(&mut state).unwrap();
// Flush outbound event queues
display.flush_clients().unwrap();
if !state.running {
break;
}
}
}
Complete minimal compositor skeleton
The snippet below combines all the pieces above into a single file that compiles and accepts connections. It does not implement any real rendering — that is left to higher-level crates such as Smithay — but it demonstrates the full structural skeleton of awayland-server compositor.
main.rs
Next steps
Display & DisplayHandle
Deep-dive into Display creation, polling, and the DisplayHandle API.
Advertising globals
Control which clients see which globals with GlobalDispatch and can_view().
Request dispatch
Handle requests, send events, and post protocol errors with Dispatch.
Managing clients
Accept connections, read credentials, and disconnect clients safely.