Developing Wayland extension protocols for Mir servers
With the Mir 1.2 release I promised:
we have made it possible for a shell developed with Mir to add its own extensions… I’ll be following up with a worked example of this process."
In the course of developing the example I came across a number of “bumps in the road” where it was unnecessarily difficult and have been fixing them in Mir. Most of the fixes have landed on Mir “master”, but while working on this I found that it was possible to extend the mirtest-dev
package to make it very easy to test the extensions in a downstream shell. The last piece of that work has just been merged.
I’ll be looking towards releasing an update to Mir that contains these improvements. (There’s some other work near completion we may want to include, but there won’t be a long delay.)
The worked example
The first thing I needed was a downstream shell that plausibly needed an extension writing. I’m sure it will come as no surprise that I chose egmde as the example server. For the extension protocol I chose “primary selection”. While a protocol that allows every application to “spy” on the clipboard may not meet the security criteria we use for Mir in IoT, for a desktop environment like egmde it is a great convenience.
The changes to egmde can be seen in this pull request. I’ll discuss these in more detail below, but for those that want to follow along the following setup is needed to build the code.
The following Mir packages are needed:
sudo apt install libmiral-dev mir-graphics-drivers-desktop
sudo apt install libmirwayland-dev mirtest-dev wlcs
The libmirwayland-dev
provides the tooling needed to implement the protocol in Mir while mirtest-dev
and wlcs
provide the testing framework.
At the time of writing, the version of Mir I’m using is not yet in any of the Mir PPAs but it will be uploaded to “Latest Integration Build” and available in a couple of hours. To add that PPA:
sudo add-apt-repository --update ppa:mir-team/dev
wlcs tests for primary selection
In parallel with this work I’ve proposed adding some “primary selection” tests to wlcs. These too are currently only available when built from source. Here’s an example of what the tests look like:
TEST_F(PrimarySelection, source_sees_request)
{
MockPrimarySelectionSourceListener source_listener{source_app.source};
PrimarySelectionOfferListener offer_listener;
StubPrimarySelectionDeviceListener device_listener{sink_app.device, offer_listener};
source_app.offer(any_mime_type);
source_app.set_selection();
sink_app.roundtrip();
ASSERT_THAT(device_listener.selected, NotNull());
EXPECT_CALL(source_listener, send(_, _, _))
.Times(1)
.WillRepeatedly(Invoke([&](auto*, auto*, int fd) { close(fd); }));
Pipe pipe;
zwp_primary_selection_offer_v1_receive(device_listener.selected, any_mime_type, pipe.source);
sink_app.roundtrip();
source_app.roundtrip();
}
Implementing a protocol extension
The first step in implementing a protocol extension is to use the protocol generator from the libmirwayland-dev
package, I’ve added two directories to egmde: wayland-protocols
and wayland-generated
. The first of these contains a copy of the protocol definition, the second the generated files and the cmake
script to generate them. Only the latter is worth reproducing here:
set(PROTOCOL "primary-selection-unstable-v1")
set(PROTOCOL_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../wayland-protocols/${PROTOCOL}.xml")
set(GENERATE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/primary-selection-unstable-v1_wrapper")
add_custom_command(
OUTPUT ${GENERATE_FILE}.cpp
OUTPUT ${GENERATE_FILE}.h
DEPENDS ${PROTOCOL_FILE}
COMMAND "sh" "-c" "mir_wayland_generator zwp_ ${PROTOCOL_FILE} header >${GENERATE_FILE}.h"
COMMAND "sh" "-c" "mir_wayland_generator zwp_ ${PROTOCOL_FILE} source >${GENERATE_FILE}.cpp"
)
add_custom_target(primary-selection-unstable
DEPENDS ${GENERATE_FILE}.cpp
DEPENDS ${GENERATE_FILE}.h
SOURCES
${GENERATE_FILE}.cpp
${GENERATE_FILE}.h
)
The generator cannot implement the semantics of the protocol: it just provides the “hooks” into which to write the code. The latter can be found in a couple of files egprimary_selection_device_controller.cpp
and primary_selection.cpp
(this is only split this way as a lot of logic can be shared with gtk_primary_selection
.)
All these generated and and-written files are combined into a static library that is used both by egmde and by a new module egmde-wlcs.so
that provides the wlcs test fixture for egmde.
wlcs test fixture
To run wlcs tests requires a “test fixture” that allows the test executable to load and control the compositor. Mir makes it very easy to implement a fixture: here is the entire “fixture” code for egmde:
#include "primary_selection.h"
#include "gtk_primary_selection.h"
#include <miral/wayland_extensions.h>
#include <miral/test_wlcs_display_server.h>
namespace
{
struct TestWlcsDisplayServer : miral::TestWlcsDisplayServer
{
miral::WaylandExtensions wayland_extensions;
TestWlcsDisplayServer(int argc, char const** argv) :
miral::TestWlcsDisplayServer{argc, argv}
{
wayland_extensions.add_extension(egmde::primary_selection_extension());
wayland_extensions.add_extension(egmde::gtk_primary_selection_extension());
add_server_init(wayland_extensions);
}
};
WlcsExtensionDescriptor const extensions[] = {
{"wl_compositor", 4},
{"wl_shm", 1},
{"wl_data_device_manager", 3},
{"wl_shell", 1},
{"wl_seat", 6},
{"wl_output", 3},
{"wl_subcompositor", 1},
{"xdg_wm_base", 1},
{"zxdg_shell_unstable_v6", 1},
{"wlr_layer_shell_unstable_v1", 1},
{"zwp_primary_selection_device_manager_v1", 1},
{"gtk_primary_selection_device_manager", 1},
};
WlcsIntegrationDescriptor const descriptor{
1,
sizeof(extensions) / sizeof(extensions[0]),
extensions
};
WlcsIntegrationDescriptor const* get_descriptor(WlcsDisplayServer const* /*server*/)
{
return &descriptor;
}
WlcsDisplayServer* wlcs_create_server(int argc, char const** argv)
{
auto server = new TestWlcsDisplayServer(argc, argv);
server->get_descriptor = &get_descriptor;
return server;
}
void wlcs_destroy_server(WlcsDisplayServer* server)
{
delete static_cast<TestWlcsDisplayServer*>(server);
}
}
extern WlcsServerIntegration const wlcs_server_integration {
1,
&wlcs_create_server,
&wlcs_destroy_server,
};
Running the tests
With all this in place, we just need to run our updated wlcs with the egmde test fixture:
$ cmake-build-debug/wlcs ../egmde/cmake-build-debug/egmde-wlcs.so --gtest_filter=Prim*
Note: Google Test filter = Prim*
[==========] Running 5 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 5 tests from PrimarySelection
[ RUN ] PrimarySelection.source_can_offer
[ OK ] PrimarySelection.source_can_offer (9 ms)
[ RUN ] PrimarySelection.sink_can_listen
[ OK ] PrimarySelection.sink_can_listen (8 ms)
[ RUN ] PrimarySelection.sink_can_request
[ OK ] PrimarySelection.sink_can_request (11 ms)
[ RUN ] PrimarySelection.source_sees_request
[ OK ] PrimarySelection.source_sees_request (9 ms)
[ RUN ] PrimarySelection.source_can_supply_request
[ OK ] PrimarySelection.source_can_supply_request (8 ms)
[----------] 5 tests from PrimarySelection (45 ms total)
[----------] Global test environment tear-down
[==========] 5 tests from 1 test cases run. (45ms total elapsed)
[ PASSED ] 5 tests
The real world
I’ve not (yet) found a client toolkit that uses “zwp_primary_selection_device_manager_v1”, so I’ve yet to check whether this works with real applications. But the principle steps of this article remain valid even if there are errors in the implementation of this protocol.