今回は実際に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通信する最小構成のコードを書いてみる。
#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は以下の通り。
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_DISPLAY
にwayland-0
,wayland-1
などの値を指定して実行する。
wl_display_roundtrip()
はその関数呼び出し以前の通信が全て処理されたことを確認するために使われる関数である。
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入門①でみたようにクライアントはサーバーからインターフェイス一覧を受け取り、使用したい機能をバインドして処理を始める。
つぎはインターフェイス一覧を受け取ってみよう。
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()
を呼び出す前に上記関数の登録をおこなう。
wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_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_output
、wh_shm
のバインド
weston_capture_v1
をバインドする前にWestonが表示している画面を扱うためのwl_output
とWestonとクライアントが使用する共有メモリを扱うためのwl_shm
をバインドしてみよう。
まずはアプリケーション全体の状態を保持するApp
クラスを作成する。
struct App {
wl_output* output = nullptr;
wl_shm* shm = nullptr;
};
そしてwl_registry_add_listener()
の3つめの引数にApp
のインスタンスを渡すようにする。
App app;
wl_registry* registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, ®istry_listener, &app);
handle_global()
はこのようになる。
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_output
、wl_shm
がバインドされたことを示す出力を確認しよう。
weston_capture_v1
をバインドしてみる
同じようにweston_capture_v1
も以下のように書けばバインドできると思うかも知れない。
} 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_output
やwl_shm
はwayland-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に配置されている。
<?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にプロトコル定義ファイルからコードを生成する手続きを追加する。
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})
既存部分も合わせて修正。
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
を作成しリスナーを設定する
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
以外のハンドル関数は空実装となっている。
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後述)。
つぎに画面データを格納するための共有メモリを確保する。
app.shm_buffer = create_shm_buffer(app.shm, 1024, 640);
共有メモリ確保の説明は複雑になるため、今回は割愛する。create_shm_buffer()
の実装はAppendix Aに掲載した。
そして、capture()
を実行すればスクリーンショットが取得できる。
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としてコードディレクトリに保存して使用する。
<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
より簡単で以下のとおりである。
-
wl_output
、wl_buffer
を引数にweston_screenshooter_shoot()
を呼ぶ -
done()
イベントを待って共有メモリの内容を画像として出力
変更履歴
- 2025-07-03:
create_shm_buffer()
への入力サイズを修正