0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Weston/Wayland入門② クライアントを作ってみる

Last updated at Posted at 2025-07-03

Weston/Wayland入門まとめ

今回は実際にwaylandクライアントを作ってwestonと通信してみる。

開発環境

開発環境は以下を想定する。

  • OS: Debian trixie(testing)
  • 言語: C++20
  • Weston14

Weston11以前を利用する場合はAppendix C.を参照してほしい。

何を作るか

今回はweston_capture_v1というインターフェイスを使いWestonのスクリーンショットを取得するクライアントを作成する。

注意
weston_capture_v1はデバッグ実行時のみ有効であるためweston起動時に--debugを付加して実行をおこなうこと。

ハンズオン

必要パッケージ

Debian系であれば、weston, libweston-14-devをインストールしておく。

$ sudo apt install weston libweston-14-dev

Waylandクライアント的 Hello World

まずはWestonとWayland通信する最小構成のコードを書いてみる。

main.cpp
#include <iostream>
#include <format>
#include <cerrno>
#include <cstring>

#include <wayland-client.h>

int main(int argc, char *argv[])
{
    wl_display* display = wl_display_connect(nullptr);
    if (display == nullptr) {
        std::cerr << std::format("failed to create display: {}", strerror(errno))
                  << std::endl;
        return -1;
    }

    wl_display_roundtrip(display);

    return 0;
}

CMakeLists.txtは以下の通り。

CmakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(screen-shooter)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(PkgConfig REQUIRED)
pkg_check_modules(WAYLAND_CLIENT REQUIRED wayland-client)

add_executable(${CMAKE_PROJECT_NAME} main.cpp)

target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${WAYLAND_CLIENT_INCLUDE_DIRS})
target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE ${WAYLAND_CLIENT_LIBRARIES})

ビルド。

$ cmake -S . -B build
$ cmake --build build

westonが起動していることを確認して、実行。

$ build/screen-shooter

複数Waylandサーバが動作している場合は必要に応じて環境変数WAYLAND_DISPLAYwayland-0,wayland-1などの値を指定して実行する。

wl_display_roundtrip()はその関数呼び出し以前の通信が全て処理されたことを確認するために使われる関数である。

image.png

wl_display_roundtrip()を呼び出すとwl_display.sync()リクエストをサーバーに送信し、サーバーからwl_callback.done()イベントが返されるまでブロックする。

sync()の前にどのようなリクエストがされているかはわからないが(上図の)、done()が返ってきたときには全て処理が終わっていることが期待できる。

screen-shooterにWAYLAND_DEBUG=1をつけて実行し実際に通信を確認してみる。

$ WAYLAND_DEBUG=1 build/screen-shooter
[1179395.420] {Default Queue}  -> wl_display#1.sync(new id wl_callback#2)
[1179395.607] {Display Queue} wl_display#1.delete_id(2)
[1179395.649] {Default Queue} wl_callback#2.done(1010)

現在のコードではwl_display_roundtrip()以外呼び出していないのでsync()done()(と厳密にはそれに付随するdelete_id())しかないが、上図のような通信をおこなっていることが見て取れる。

サーバーからインターフェイス通知を受け取る

Weston/Wayland入門①でみたようにクライアントはサーバーからインターフェイス一覧を受け取り、使用したい機能をバインドして処理を始める。

つぎはインターフェイス一覧を受け取ってみよう。

main.cpp(抜粋)
static void handle_global(void* data, wl_registry* registry,
                          uint32_t name, const char *interface, uint32_t version)
{
    std::cout << std::format("recv inerface: {}", interface) << std::endl;
}

static void handle_global_remove(void* data, wl_registry* registry, uint32_t name)
{
    // noop
}

static const wl_registry_listener registry_listener = {
    handle_global,
    handle_global_remove,
};

インターフェイス通知関数(handle_global())とオブジェクト破棄関数(handle_global_remove())を作成し関数ポインタをstruct wl_registry_listenerに登録する。

Tips
関数ポインタを登録するので関数名は任意であるが、多くのクライアントアプリでhandle_global()registry_handle_global()という関数名を採用している。
コードを読むときに上記名前で検索するとそのクライアントアプリが必要とするインターフェイスを素早く知ることができる。

wl_display_roundtrip()を呼び出す前に上記関数の登録をおこなう。

main.cpp(抜粋)
    wl_registry* registry = wl_display_get_registry(display);
    wl_registry_add_listener(registry, &registry_listener, nullptr);

ビルドして実行すると、Westonが持っているインターフェイス(≒機能)の一覧が取得できる。

$ WAYLAND_DISPLAY=wayland-1 build/screen-shooter
recv inerface: wl_compositor
recv inerface: wl_subcompositor
recv inerface: wp_viewporter
recv inerface: zxdg_output_manager_v1
recv inerface: wp_presentation
recv inerface: wp_single_pixel_buffer_manager_v1
recv inerface: wp_tearing_control_manager_v1
recv inerface: zwp_relative_pointer_manager_v1
recv inerface: zwp_pointer_constraints_v1
recv inerface: zwp_input_timestamps_manager_v1
recv inerface: weston_capture_v1
recv inerface: wl_data_device_manager
recv inerface: wl_shm
recv inerface: wl_drm
recv inerface: zwp_linux_dmabuf_v1
recv inerface: wl_seat
recv inerface: zwp_linux_explicit_synchronization_v1
recv inerface: wl_output
recv inerface: zwp_input_panel_v1
recv inerface: zwp_input_method_v1
recv inerface: zwp_text_input_manager_v1
recv inerface: xdg_wm_base
recv inerface: weston_desktop_shell

今回使用するweston_capture_v1が存在しているのが確認できる。

wl_outputwh_shmのバインド

weston_capture_v1をバインドする前にWestonが表示している画面を扱うためのwl_outputとWestonとクライアントが使用する共有メモリを扱うためのwl_shmをバインドしてみよう。

まずはアプリケーション全体の状態を保持するAppクラスを作成する。

main.cpp(抜粋)
struct App {
    wl_output* output = nullptr;
    wl_shm* shm = nullptr;
};

そしてwl_registry_add_listener()の3つめの引数にAppのインスタンスを渡すようにする。

main.cpp(抜粋)
    App app;

    wl_registry* registry = wl_display_get_registry(display);
    wl_registry_add_listener(registry, &registry_listener, &app);

handle_global()はこのようになる。

main.cpp(抜粋)
static void handle_global(void* data, wl_registry* registry,
                          uint32_t name, const char *interface, uint32_t version)
{
    App* app = static_cast<App*>(data);

    if (strcmp(interface, wl_output_interface.name) == 0 && version >= 2) {

        std::cout << std::format("bind wl_output") << std::endl;
        app->output = static_cast<wl_output*>(wl_registry_bind(registry, name, &wl_output_interface, 2));

    } else if (strcmp(interface, "wl_shm") == 0) {

        std::cout << std::format("bind wl_shm") << std::endl;
        app->shm = static_cast<wl_shm*>(wl_registry_bind(registry, name, &wl_shm_interface, 1));

    }
}

確認
実際に動作させてwl_outputwl_shmがバインドされたことを示す出力を確認しよう。

weston_capture_v1をバインドしてみる

同じようにweston_capture_v1も以下のように書けばバインドできると思うかも知れない。

main.cpp(抜粋)
    } else if (strcmp(interface, "weston_capture_v1") == 0) {

        std::cout << std::format("bind weston_capture") << std::endl;
        app->capture = static_cast<weston_capture_v1*>(wl_registry_bind(registry, name, &weston_capture_v1_interface, 1));

    }

確かにコードはこれで正しいがこのままではコンパイルエラーとなる。なぜならwl_outputwl_shmwayland-client.h#includeすることで使用可能になるが、weston_capture_v1のことは何も知らないからである。

ではどうすればweston_capture_v1が使用可能になるだろうか。

プロトコル定義ファイル

Westonが様々なインターフェイスを定義していることは前述のコードで確認した。

これらのインターフェイスの多くは「プロトコル定義ファイル」と呼ばれるファイルにXMLの形で記載されている。

例えばweston_capture_v1であればlibweston-14-devパッケージがインストールされていれば/usr/share/libweston-14/protocols/weston-output-capture.xmlに配置されている。

weston_output_capture.xml(抜粋)
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="weston_output_capture">

  <interface name="weston_capture_v1" version="1">
    <description summary="image capture factory">
      The global interface exposing Weston screenshooting functionality
      intended for single shots.

      This is a privileged inteface.
    </description>

    <request name="destroy" type="destructor">
      <description summary="unbind image capture factory">
        Affects no other protocol objects in any way.
      </description>
    </request>

    <request name="create">
      <arg name="output" type="object" interface="wl_output"
           summary="output to shoot"/>
      <arg name="source" type="uint" enum="source" summary="pixel source"/>
      <arg name="capture_source_new_id" type="new_id"
           interface="weston_capture_source_v1" summary="new object"/>
    </request>

wayland-scannerコマンド(debianであればlibwayland-binパッケージに収録)を利用するとプロトコル定義ファイルからC/C++コードを生成できる。

このコードを使用することでweston_capture_v1インターフェイスが使用可能となる。

CMakeLists.txtにプロトコル定義ファイルからコードを生成する手続きを追加する。

CMakeLists.txt(追加部分)
find_program(WAYLAND_SCANNER wayland-scanner REQUIRED)

set(CAPTURE_XML /usr/share/libweston-14/protocols/weston-output-capture.xml)
set(CAPTURE_HEADER ${CMAKE_CURRENT_BINARY_DIR}/weston-output-capture-client-protocol.h)
set(CAPTURE_SOURCE ${CMAKE_CURRENT_BINARY_DIR}/weston-output-capture-client-protocol.c)

add_custom_command(
        OUTPUT  ${CAPTURE_HEADER}
        COMMAND ${WAYLAND_SCANNER} client-header ${CAPTURE_XML} ${CAPTURE_HEADER}
        DEPENDS ${CAPTURE_XML}
        VERBATIM
)

add_custom_command(
        OUTPUT  ${CAPTURE_SOURCE}
        COMMAND ${WAYLAND_SCANNER} private-code ${CAPTURE_XML} ${CAPTURE_SOURCE}
        DEPENDS ${CAPTURE_XML}
        VERBATIM
)

add_custom_target(protocol_headers ALL DEPENDS ${CAPTURE_HEADER})

既存部分も合わせて修正。

CMakeLists.txt(修正部分)
add_executable(${CMAKE_PROJECT_NAME} main.cpp ${CAPTURE_SOURCE})
add_dependencies(${CMAKE_PROJECT_NAME} protocol_headers)

target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
        ${WAYLAND_CLIENT_INCLUDE_DIRS}
        ${CMAKE_CURRENT_BINARY_DIR}
)

生成されるweston-output-capture-client-protocol.hファイルを#includeすれば前章のコードでweston_capture_v1がバインドできるはずである。

キャプチャの実行

スクリーンショットを取得するにはweston_captureからweston_capture_sourceを作成し、capture()を実行する必要がある。

まずはsourceを作成しリスナーを設定する

main.cpp(抜粋)
    weston_capture_source_v1* source = weston_capture_v1_create(app.capture, app.output,
                                                                WESTON_CAPTURE_V1_SOURCE_FRAMEBUFFER);

    weston_capture_source_v1_add_listener(source, &capture_source_handlers, &app);

リスナーとして設定されているcapture_source_handlersだがcapture_source_handle_complete以外のハンドル関数は空実装となっている。

main.cpp(抜粋)
static const weston_capture_source_v1_listener capture_source_handlers = {
    .format = capture_source_handle_format,
    .size = capture_source_handle_size,
    .complete = capture_source_handle_complete,
    .retry = capture_source_handle_retry,
    .failed = capture_source_handle_failed
};

スクリーンショット取得が終了するとcomplete()イベントが通知されるため、capture_source_handle_complete()にてPNG化をおこなっている(Appendix Bd後述)。

つぎに画面データを格納するための共有メモリを確保する。

main.cpp(抜粋)
    app.shm_buffer = create_shm_buffer(app.shm, 1024, 640);

共有メモリ確保の説明は複雑になるため、今回は割愛する。create_shm_buffer()の実装はAppendix Aに掲載した。

そして、capture()を実行すればスクリーンショットが取得できる。

main.cpp(抜粋)
    weston_capture_source_v1_capture(source, app.shm_buffer->wl_buf);

    while (!app.capture_completed) {
        wl_display_dispatch(display);
    }

確認
実際にスクリーンショットが取れることを確認しよう。

確認
WAYLAND_DEBUG=1を付けて実行しプロトコルを観察、説明できるようにしよう。

発展

記載したコードを簡潔さを優先させているため、エラー処理や後処理などが疎かになっている。
一通り動いたら上記を意識してより堅牢なコードにしてみてほしい

Appendix

Appendix A. wl_shmによる共有メモリの確保

struct ShmBuffer {
    wl_buffer *wl_buf = nullptr;
    void* data = nullptr;
    int width, height, stride;
};

static std::unique_ptr<ShmBuffer> create_shm_buffer(wl_shm* shm, int width, int height)
{
    auto buf = std::make_unique<ShmBuffer>();

    buf->width = width;
    buf->height = height;
    buf->stride = width*4; // ARGB8888

    const size_t size = static_cast<size_t>(buf->stride * height);

    const int fd = syscall(SYS_memfd_create, "shm_buffer", MFD_CLOEXEC);
    if (fd < 0 || ftruncate(fd, size) < 0) {
        return nullptr;
    }

    void* data = mmap(nullptr, size, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, 0);

    if (data == MAP_FAILED) {
        close(fd);
        return nullptr;
    }

    buf->data = data;

    wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
    if (!pool) {
        std::cerr << "Failed to create shm pool" << std::endl;
        return nullptr;
    }

    close(fd);

    buf->wl_buf = wl_shm_pool_create_buffer(
        pool, 0, width, height, buf->stride,
        WL_SHM_FORMAT_ARGB8888);

    wl_shm_pool_destroy(pool);
    if (!buf->wl_buf) {
        return nullptr;
    }

    return buf;
}

Appendix B. capture_source_handle_complete()

PNG化にはヘッダのみで使用できるstb_image_write.hを使用している。

#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"


static void capture_source_handle_complete(void *data,
                                           struct weston_capture_source_v1 *proxy)
{
    auto app = static_cast<App*>(data);

    auto width = app->shm_buffer->width;
    auto height = app->shm_buffer->height;
    auto stride = app->shm_buffer->stride;

    const uint8_t* src = static_cast<uint8_t*>(app->shm_buffer->data);
    std::vector<uint8_t> image;
    image.resize(width * height * 4);
    for (int y=0; y<height; ++y) {
        for (int x=0; x<width; ++x) {
            const int pos = y * stride + x * 4;
            const int idx = (y * width + x) * 4;
            image[idx+0] = src[pos + 2]; // R
            image[idx+1] = src[pos + 1]; // G
            image[idx+2] = src[pos + 0]; // B
            image[idx+3] = src[pos + 3]; // A
        }
    }

    stbi_write_png("screenshot.png", width, height, 4,
                   image.data(), width*4);

    app->capture_completed = true;
}

Appendix C. Weston11以前の場合

weston_capture_v1はWeston12以降の対応であるため、Weston11以前を使用する場合はかわりにweston_screenshooterインターフェイスを使うことになる。

しかしweston_screenshooterプロトコルは公開されていないため、下記をweston-screenshooter.xmlとしてコードディレクトリに保存して使用する。

weston-screenshooter.xml
<protocol name="weston_screenshooter">

  <interface name="weston_screenshooter" version="1">
    <request name="shoot">
      <arg name="output" type="object" interface="wl_output"/>
      <arg name="buffer" type="object" interface="wl_buffer"/>
    </request>
    <event name="done">
    </event>
  </interface>

</protocol>

使い方はweston_capture_v1より簡単で以下のとおりである。

  1. wl_outputwl_bufferを引数にweston_screenshooter_shoot()を呼ぶ
  2. done()イベントを待って共有メモリの内容を画像として出力

変更履歴

  • 2025-07-03: create_shm_buffer()への入力サイズを修正
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?