The last time Hackerfall tried to access this page, it returned a not found error. A cached version of the page is below, or clickhereto continue anyway

Using Wayland from Rust, Part 1 Levans' workshop

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?

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:

... 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):

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:

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 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
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
   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.

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.

Continue reading on blog.levans.fr