Developing Wayland extension protocols for Mir servers

With the Mir 1.3 release we smoothed the way for shells adding Wayland extension protocols in Mir based shells.

The worked example

To create a “worked example” the first thing I needed was a downstream shell that plausibly needed an extension writing. For obvious reasons 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 for building egmde and for developing the Wayland extension:

sudo add-apt-repository --update ppa:mir-team/release
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.

wlcs tests for primary selection

While working on this I proposed adding some “primary selection” tests to wlcs and they are included in the 1.1 release of wlcs. 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[] = {
    {primary_selection_extension.name.c_str(), 1},
    {gtk_primary_selection_extension.name.c_str(), 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:

$ `pkg-config --variable test_runner wlcs` 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.