Waylandプログラミング入門

前回に引き続きX Window Systemの後継、Waylandに関する記事です。
今回は具体的にWaylandのクライアント側で動くWaylandクライアントが実際にどのように動くのかを、筆者が書いた単純なプログラムから説明しようと思います。

前回も書きましたが、Waylandのプロトコルオブジェクト指向の非同期なサーバとクライアント間でのIPCです。
Waylandプロトコルのリファレンス実装であるlibwayland-clientとlibwayland-serverはCで書かれているので、以下ではC言語でプログラムを記述します。

以下に今回の対象であるsimple-clientのソースを示します。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <wayland-client.h>
#include <wayland-client-protocol.h>
#include "os-compatibility.h"

struct simple_client {
    struct wl_display       *display;
    struct wl_registry      *registry;
    struct wl_compositor    *compositor;
    struct wl_buffer        *buffer;
    struct wl_surface       *surface;
    struct wl_shm           *shm;
    struct wl_shell         *shell;
    struct wl_shell_surface *shell_surface;
    void *data;
    int width, height;
};

void die(const char msg[])
{
    fprintf(stderr, "%s", msg);
    exit(EXIT_FAILURE);
}

static void handle_ping(void *data, struct wl_shell_surface *shell_surface, uint32_t serial)
{
    wl_shell_surface_pong(shell_surface, serial);
}

static void registry_handle_global(
    void *data, struct wl_registry *registry, uint32_t name,
    const char *interface, uint32_t version)
{
    struct simple_client *client = data;
    printf("interface=%s name=%0x version=%d\n", interface, name, version);
    if (strcmp(interface, "wl_compositor") == 0)
        client->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1);
    else if (strcmp(interface, "wl_shell") == 0)
        client->shell = wl_registry_bind(registry, name, &wl_shell_interface, 1);
    else if (strcmp(interface, "wl_shm") == 0)
        client->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1);
}

static void create_shm_buffer(struct simple_client *client)
{
    struct wl_shm_pool *pool;
    int fd, size, stride;

    stride = client->width * 4;
    size = stride * client->height;

    fd = os_create_anonymous_file(size);
    if (fd < 0) {
        fprintf(stderr, "creating a buffer file for %d B failed: %m\n", size);
        exit(1);
    }

    client->data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (client->data == MAP_FAILED) {
        fprintf(stderr, "mmap failed: %m\n");
        close(fd);
        exit(1);
    }

    pool = wl_shm_create_pool(client->shm, fd, size);
    client->buffer =
        wl_shm_pool_create_buffer(pool, 0,
            client->width, client->height ,stride, WL_SHM_FORMAT_ARGB8888);
    wl_shm_pool_destroy(pool);

    close(fd);
}

void draw_argb8888(void *d, uint8_t a, uint8_t r, uint8_t g, uint8_t b, size_t count)
{
    while (count-- > 0)
        *((uint32_t *)d + count) = ((a << 24) | (r << 16) | (g << 8) | b);
}

struct simple_client *simple_client_create()
{
    static struct wl_registry_listener registry_listener = {
        registry_handle_global, NULL
    };
    struct simple_client *client = malloc(sizeof (struct simple_client));
    if (!client)
        die("Cannot allocate memory for simple_client\n");

    client->display = wl_display_connect(NULL);
    if (!client->display)
        die("Cannot connect to Wayland display\n");

    client->registry = wl_display_get_registry(client->display);
    if (!client->registry)
        die("Cannot get registry from Wayland display\n");
    wl_registry_add_listener(client->registry, &registry_listener, client);

    wl_display_roundtrip(client->display);
    wl_display_dispatch(client->display);

    client->width = 600;
    client->height = 500;
    client->surface = wl_compositor_create_surface(client->compositor);
    client->shell_surface = wl_shell_get_shell_surface(client->shell, client->surface);

    create_shm_buffer(client);

    if (client->shell_surface) {
        static const struct wl_shell_surface_listener shell_surface_listener = {
            handle_ping, NULL, NULL
        };
        wl_shell_surface_add_listener(
            client->shell_surface, &shell_surface_listener, client);
        wl_shell_surface_set_toplevel(client->shell_surface);
    }

    wl_surface_set_user_data(client->surface, client);
    wl_shell_surface_set_title(client->shell_surface, "simple-client");

    draw_argb8888(client->data, 0x00, 0x00, 0x00, 0xff, client->width * client->height);
    wl_surface_attach(client->surface, client->buffer, 0, 0);
    wl_surface_damage(client->surface, 0, 0, client->width, client->height);
    wl_surface_commit(client->surface);

    return client;
}

int main(int argc, char **argv)
{
    struct simple_client *client = simple_client_create();
    while (wl_display_dispatch(client->display) != -1);
    free(client);
    exit(EXIT_SUCCESS);
}

このプログラムは透明がかった青色の600x500の長方形を画面に表示するだけのプログラムです。
処理がこれだけなのに行数が130行ぐらいあるのはクライアント側ですべて処理するためです。
下画像はこのプログラムの実行結果です。

WaylandのアーキテクチャAPIを記した公式ドキュメントがここにあるので適宜参照してください

[解説]

処理の中心はsimple_client_create()です。ここでstruct simple_clientを初期化、設定して最後にmainの最後でループして終了です。
大まかな流れは下のような感じです:

  1. Waylandコンポジタに接続
  2. Waylandコンポジタ上のグローバルオブジェクトを取得
  3. 描画用の共有メモリのプールを作成
  4. プールから画面に表示するsurfaceのバッファを切り出す
  5. バッファに表示内容、具体的なピクセルデータを書き込む
  6. surfaceにバッファをくっつけて、Waylandコンポジタに表示するよう要求
  7. ループでWaylandコンポジタからのイベント読み続ける。

WaylandプロトコルではWaylandコンポジタへの要求はコンポジタ上のオブジェクトのメソッドの呼び出しとなります。(Request)
またそのコンポジタ上のオブジェクトから通知や、指定したコールバックの呼び出し等が起こります。 (Event)

wl_displayはWaylandコンポジタとクライアントが通信するときに必ず必要になるコアオブジェクトです。
wl_registryからはコンポジタが保持しているオブジェクトを取得できます。
つまり、wl_display_connect()でコンポジタとの通信を確立したあと、そのコアオブジェクトからwl_registryを得て、コンポジタ上のグローバルオブジェクト(e.g. wl_shell, wl_shm)を取得しstrut simple_clientを初期化します。
この際の流れがこれからも重要になるので詳しく解説します。
wl_registry_add_listener()はつまり、サーバ上のwl_rergistry(つまり今取得したregistry)のadd_listenerメソッドを呼び出すわけです。
add_listener()メソッド呼び出し後は、コンポジタからの返答(Event)が帰ってきます。これがwl_registry_listener::globalで、こいつでコンポジタ内のグローバルオブジェクトが通知されます。プログラムではregistry_handle_global()に設定しています。
Waylandではwl_OBJECTNAMEのEventを受け付けるwl_OBJECTNAME_listenerが存在し、wl_OBJECTNAME_*()の命名規則に従った関数でRequestを送ったり、なにか変化があった場合、Eventとしてwl_OBJECTNAME_listenerの関数が呼ばれます。wl_shell_surface_listener::pingではプログラムが応答可能かカーソルがsurfaceの領域に入った時呼ばれるので、wl_shell_surfae_pong()で応答します。
Eventはwl_displayにキューされるので、このキュー内にあるEventを全て処理するのがwl_display_dispatch()、wl_display_roundtrip()は送信したRequestがすべてコンポジタで処理されるまでブロックします。

wl_compositorはコンポジタを表しており、ここから実際に表示するsurfaceを取得します。
wl_shellは表示するsurfaceに対しマウスカーソル、リサイズ、回転等のデスクトップアプリケーション用に(最低限)必要なインターフェイスを実装するためのオブジェクトです。
wl_shell_get_shell_surface()でsurfaceからwl_shell用のshell_surfaceを取得します。
そのあとdraw_argb8888()で600x500の長方形に対し透過した青色の長方形を描画します。
そのあと、surfaceに描画したbufferを関連付け、変化した範囲(今回は全域)を指定し、コミットします。
最後はwl_display_dispatch()でrequestを送信し、コンポジタからのイベントを待ち続けます。
ここで、ついに描画した長方形が画面に表示されます。

どうでしょうか。わかりづらい文章になってしまいましたが細かいことは大体ドキュメントに書いてあります。
Westonのclients/にあるクライアントプログラムはWestonで試験的に導入されていてWaylandのコアプロトコルにはまだ入っていないプロトコルやclients/以下にあるtoytoolkitと呼ばれる簡易ライブラリに依存しているものが多くあり、純粋なWaylandのプロトコルだけで動くのは、simple-shmやsimple-eglぐらいです。
Waylandは必要最低限のAPIしか用意しませんが、今でもxdg_shellやsubsurface等の新しい便利な機能が追加されようとしています。

もしWaylandに興味のある方は、GTKやEFL等が移植されているので試してみてはどうでしょうか。大体実行時にバックエンドの切り替えができるようになっていると思うので、コンパイルし直す必要がないことがほとんどでしょう。