Levans' workshop

Experiments and thoughts about Machine Learning, Rust, and other stuff...

Using Wayland from Rust, Part 1


Hi all. During the past few months, I've been working on some crates for using Wayland in Rust. Now, they have reached a step that I could call "mostly usable": not yet finished, but a good basis to start using them. So I'm writing this note to present them, and explain how they can be used.

What's Wayland?

Wayland logo

I'll not explain you in detail what Wayland is and why it was created. The associated wikipedia page is already fairly detailed. In short: it's the remplacement of good old X11.

What interests me here is how Wayland works. It's an object-oriented protocol. The client creates and manipulate objects, each of it associated with requests and events (you can see requests as function calls from the client to the server, and events as the opposite). And that's with these objects that you can build your GUI.

Main objects

The first object you see during a Wayland session is a wl_display. It is the guard of the socket your client uses to communicate with the server. It has a few requests that are mostly for handling the message-passing, and that I will not detail for now, as they are not necessary for classic applications. But the display also has a very useful request: get_registry.

This request provides you with the wl_registry, the second most important object. Through its events, this object will advertise you the global objects the wayland server supports. In Wayland, all object must be created by an other object. The global objects are the roots of each objects hierarchies, directly linked to the registry, father of all.

Among the global objects, you can find:

  • wl_compositor: this one will allow you to create several wl_surfaces, which are the fundamental building blocks of your wayland GUI. A surface is anything you can draw on: the content of you app, but also its decorations, the buttons, and even the pointer image.
  • wl_seat: this one handles input. You can access a wl_pointer, a wl_keyboard and a wl_touch from it, depending on what you need and what is available.
  • wl_output: used to query informations about the graphical outputs of the computer. In general, there will be one wl_output for each monitor connected to the computer.

... and a few others, these were only examples.

Drawing

Wayland has a very simple approach regarding the drawing: you give it the pixels, and it'll put them on the screen.

What I mean by that, is that the client is fully responsible of drawing the content of the various surfaces, and then it'll pass it to the server through a shared memory, and the server will take care of displaying them on the screen and composing as needed.

That means that to effectively draw something, you'll need to use some tools, such as a drawing library like cairo, or even opengl, and this is mostly out of the scope of this tutorial.

Getting Wayland

It's easier and easier as KDE and Gnome are getting more and more compatible. Because there is not one wayland server as there is one X server. Each desktop environment or window manager can include or be a wayland server.

The most classic is weston, the reference implementation of server-side wayland, but it's quite limited for everyday use. There are other servers in the works, including Gnome and KWin. Your linux distribution has probably some documentation for you about how to get a wayland server working on your computer.

Getting to Rust

So, in the context of Rust, what am I offering to you? Nothing less than 3 crates (and maybe more in the future):

  • wayland-client: this is a very light shim over the wayland C library, which offers you direct access to the wayland objects. Most of its contents are generated from the XML descriptions of the Wayland protocol, which will allow for easy integration of protocol extensions that are currently in development.
  • wayland-window: this is a small wrapper that handles for you the fastidious work of creating a decorated window handling moving and resizing (because yeah, it's the client's job in wayland!).
  • wayland-kbd: an other small crate doing the hard work for you, but this time regarding keyboard events. It uses libxkbcommon to interpret the keymap provided by the compositor, to give you utf8 input from the keystrokes.

And right now, let's do some simple things.

First, list all the globals

So, starting from a new project, we'll use only wayland-client. At the time of writing this tutorial, I'm using the version 0.4.3, so my Cargo.toml contains:

[dependencies]
wayland-client = "0.4.3"

Let's write come code:

#[macro_use]
extern crate wayland_client;

wayland_env!(WaylandEnv,
);

Okay, what's this macro already? Keeping track of all the wayland globals is quite cumbersome, so I made a macro to facilitate the job. It creates a struct (here named WaylandEnv) that will automatically fetch the registry and all the globals we ask it to fetch (here: none).

fn main() {
    use wayland_client::wayland::get_display;

    let display = match get_display() {
        Some(d) => d,
        None => panic!("Unable to connect to a wayland server!")
    };

First thing, you see that there is a wayland module inside the wayland_client crate. The reason is simple: this module contains the core protocol. New ones will be created alongside as I add protocols to the crate.

Secondly, get_display() returns an Option<WlDisplay>. Because if you start this program from a tty or an X11 session, it won't be able to connect to the server. And I wanted to let you decide what to do in this case (one option could be to fallback on X11, for example).

Here, we just panic, as our application cannot work without a wayland server.

    let (env, events) = WaylandEnv::init(display);

Okay, now, we hand out the display to the WaylandEnv struct created by the macro, and it returns us 2 objects:

  • An instance of WaylandEnv that I named env, containing several wayland objects.
  • An EventIterator that I named events. As its name indicates, it's an iterator, and it will yield all the events the wayland server sends us. Part of these events are processed by the init(...) function to get the global objects, but then for upcoming events it's up to us to handle them (but we won't do it in this example).

What interests us now is one of the fields of our env object: the field globals. It lists the identifiers of all the global objects that the compositor has advertised. Its type is Vec<(u32, String, u32)>. Each entry is a global object, and the values are:

  • the global id of this object
  • the name of the interface of this object
  • the version of the interface supported by the server

The interface are versionned, and new versions can add new requests or events (but must remain retro-compatible). For each object, you must use a version older or equal to the newest version supported by both the wayland_client library you are using and the server. If you use the wayland_env!() macro to manage your globals, it will automatically use the newest version supported by both.

For now, let's just print them:

    for &(id, ref interface, version) in &env.globals {
        println!("{: >4} {} ({})", id, interface, version);
    }
}

Then, running it in a weston session, I get that on my computer:

   1 wl_compositor (3)
   2 wl_subcompositor (1)
   3 wl_scaler (2)
   4 wl_text_input_manager (1)
   5 wl_data_device_manager (1)
   6 wl_shm (1)
   7 wl_drm (2)
   8 wl_seat (4)
   9 wl_input_method (1)
  10 wl_output (2)
  11 wl_input_panel (1)
  12 wl_shell (1)
  13 xdg_shell (1)
  14 desktop_shell (3)
  15 screensaver (1)
  16 workspace_manager (1)
  17 screenshooter (1)

A few of these objects are not part of the core wayland protocol, and are still experimental, and so cannot yet be used with wayland_client.

Sample window

Okay, this article is getting long. But let's make a more elaborate example before I leave you, in order to do something visible!

As the example is getting a little big, I won't copy the whole file here, but you can view it on github.

The interesting things here are:

wayland_env!(WaylandEnv,
    compositor: WlCompositor,
    shell: WlShell,
    shm: WlShm
);

Here, we are using again the macro, but asking it to automatically bind 3 globals: wl_compositor, to create surfaces, wl_shell, which allows us to make surfaces into real windows, and wl_shm, which allows us to share memory with the compositor.

The WaylandEnv struct will get 3 new fields, named compositor, shell and shm of type Option<(T,u32)>, giving the object T and the version that was bound. The Option part means that the field will be None if no global of this type was advertised by the server.

As here we need all 3 without any version constraints (we'll use only version 1 of the interfaces), let's extract them in a quick and dirty way:

let compositor = env.compositor.as_ref().map(|o| &o.0).unwrap();
let shell = env.shell.as_ref().map(|o| &o.0).unwrap();
let shm = env.shm.as_ref().map(|o| &o.0).unwrap();

We then create two objects: a wl_surface to show some pixels on the screen, and a wl_shell_surface to give our surface the role of a toplevel surface (a simple window).

let surface = compositor.create_surface();
let shell_surface = shell.get_shell_surface(&surface);

Then, after writing some content to a tempfile (the pixels to be displayed), we create a memory pool on the file descriptor of this tempfile: it'll be the shared memory between the server and our client:

// 40_000 is the number of bytes to map
let pool = shm.create_pool(tmp.as_raw_fd(), 40_000);

After that, we create a buffer in this memory pool, mapping a part of this pool into a 100x100 rectangle:

// 400 is the number of bytes to jump between the start of 2 lines.
// The `as u32` here is necessary because of some missing things in the XML protocol
// files, but it should hopefully not be necessary any more in a few weeks.
let buffer = pool.create_buffer(0, 100, 100, 400, WlShmFormat::Argb8888 as u32);

Then, set the shell surface as toplevel (as opposed to popup or fullscreen):

shell_surface.set_toplevel();

We attach the buffer on the surface, to define its contents:

surface.attach(Some(&buffer), 0, 0);

And then we commit our changes to the server. None of them take effect before that.

surface.commit();

Finally we sync with the server to make sure all these requests are properly processed:

env.display.sync_roundtrip().unwrap();

And we just loop { }, to keep the window displayed.

What now?

Okay, this was quite simple, and we didn't handle a single event, not very crazy, is it?

But I'll stop here on this note, which is already quite long. A new one will follow using wayland-window and wayland-kbd to do something more fancy.

I the meanwhile, you can have a look on the example I wrote for these two crates: window example, keyboard example. These examples do not yet use the wayland_env! macro, this is a new addition I made and I have not updated them yet.