Rewriting Waypipe in Rust (2024-12)

Waypipe is a proxy for Wayland applications, which makes it possible to run an application on a different computer but interact with it locally, as if it were actually running on the local computer. (Wayland is the slowly-improving window system protocol for Linux, successor to X11; which most applications now support. The protocol sends plain data over a Unix socket, along with file descriptors to share less serializable things like window surface image data.)

It was written by me during the summer of 2019, and was implemented in C because libwayland used C, because most libraries provide a C interface, because other programming languages often aren’t available or are hard to install as a user on old, shared systems, and because no complicated data structures or libraries were used for which C++ would be necessary. The core operations (basic protocol parsing and shared memory buffer replication) did not take long to implement, and were done in a week. Most of Waypipe’s code is spent making this practical: making the buffer replication for displayed windows run fast and only when necessary; handling other Wayland “protocols” (read: Wayland object types and associated methods), supporting replication of DMABUFs (GPU-side memory buffers used to transfer image data between applications; typically used by OpenGL and Vulkan in place of CPU-side shared memory file descriptors.), and optionally video-encoding DMABUFs.

Making Waypipe reliable, secure, and efficient has been challenging. Waypipe receives and sends messages from Wayland applications and compositors, which it should not trust to use the various Wayland protocols properly. In addition to the (currently rather theoretical) risk of malicious applications, regular mistakes and complicated stacks of libraries can use the Wayland protocols in unexpected ways. There are several libraries implementing the base wire protocol, a number of compositors and toolkits that use it, libraries that extend or try to “share” a single Wayland connection with an existing program, and clients that people have written which directly use a wayland library instead of going through a toolkit, similarly to how many people directly used Xlib.

My approach was to try to write reliable code that handles all errors, in some form or another. (Ideally, by cleanly shutting down the connection and sending an error message to the application; this is what libwayland-server also does.) Of course, to make reliable code, I needed to test it. My main strategies were: trying many Wayland clients and subcomponents of Waypipe (worked, but tests take a while to write and still miss things), injecting errors (to check how broken memory allocation failure paths were), using addressanitizer and static analysis tools to detect issues, and fuzzing (to see what crashes when a fuzzer controls the Wayland message inputs and the internal protocol used to connect the local and remote Waypipe instances; like testing, this requires some framework code to let the fuzzer provide and manipulate file descriptors, which still doesn’t cover all cases).

Altogether, these testing approaches appear to have worked, but they require a measure of active maintainance over time as the code is updated. New Wayland protocols and protocol revisions continue to be made and Waypipe has needed and will often need to adapt to them; the wl_drm protocol once used to share DMABUFs has now been entirely replaced by zwp_linux_dmabuf_v1, and new protocols for explicit synchronization, presentation timing, screen capturing, and color management are now done or being designed. There have also been new feature requests and ideas for performance improvements. Implementing all of these required or will require new code, which is not as well tested as the older code and would require a lot of work to bring to the same standard.

Rewriting Waypipe in Rust was expected to have multiple benefits. First, to reduce the cost of making changes and adding new features at the same level of security; Rust provides a framework with which to encapsulate memory-unsafe code, and a safe and comprehensive standard library, which together should significantly reduce the number of places where memory-unsafe bugs could appear in Waypipe. Second, I wanted to change Waypipe’s DMABUF handling backend library from libgbm to vulkan to improve performance, handle explicit synchronization, and more efficiently do RGB to YCbCr conversion for the optional video encoding feature; in total I expected that this would require changing or adding about half of Waypipe’s lines of non-test code. Third: for me to better learn Rust; and fourth: because I had been hearing about other C or C++ to rust rewrite projects, and was curious whether a rewrite would be worth it. The best way to determine that was to try it.

In practice

The rewrite went roughly as expected.

Instead of doing an incremental port of Waypipe, converting its various logical parts piece by piece, I redeveloped the Rust version in parallel, roughly following the same development path as the original Waypipe. (Except this time I knew the end goal.) That is, I started with a simplified form of the command line interface, and then developed a basic main proxy loop, Wayland protocol parsing logic, and shared memory buffer replication. The initial step was easier because I already had written a different (local) Wayland proxy program in Rust (windowtolayer). Once that was ready, I iteratively added back the various features of Waypipe, starting with damage tracking, compression support, and multithreaded buffer diff calculation and application; often testing the code by connecting it to the original Waypipe implementation.

Much of my time in the middle of the port was spent implementing DMABUF support, this time using Vulkan instead of libgbm. I started with a simple, single-threaded implementation and once that worked, progressively introduced multi-threading, buffer update calculations, zwp_linux_dmabuf_v1 protocol handling, and stride adjustments to match the weird way the original C implementation adjusted nominal buffer strides when using libgbm. To implement Waypipe’s optional video encoding feature, I started with the possibly tricky case of hardware video encoding and decoding. As Vulkan hardware video extensions had been released in the last few years, I just used ffmpeg’s encoder/decoder based on them, which was recently added but worked with few issues. Software video encoding and decoding were easy to add afterwards.

The second 90% of the work has been spent on all the miscellaneous tasks: bringing the Rust rewrite up to feature parity with the original version, getting it to integrate with Waypipe’s existing build system (using meson.), and resolving the issues found after I deemed the Rust port good enough and brought it into the main git repository.

Unsorted comments on the port

Things that I’d like to have

Possible improvements for Rust

Having learned more Rust recently, it is my irresponsibility to suggest things wiser programmers probably can explain are bad ideas.

Conclusion

Was the rewrite worth it? I suspect yes: improving the code does seem to be somewhat easier to do in Rust than with the original, where I could never be certain that I was not missing some edge case, and moving DMABUF handling to use Vulkan has significantly improved performance. I will know for certain in a few years when I see what types of bugs I run into. Rewriting the code did take time; I did not precisely measure it but would estimate a month of work so far (spread out over a longer period, since Waypipe was not my sole focus); this is similar to the time needed to develop the program to begin with. Could I have acheived the same effect with a month of work in C? Probably, but I would not have as much confidence that the project quality would remain stable in the future, when I will probably make many changes and spend less time testing them. (For example: I held off on parallelizing buffer diff message application with the C version, because I expected it to be a difficult task to do right.)

Overall, I think Waypipe was appropriate for a Rust rewrite: Waypipe is network facing code, needs to be efficient, does some parsing, and uses multiple threads; and was originally written in C. Interacting with existing libraries’ C APIs was, as expected, more tedious to do than in C, but I think the improvements to Waypipe’s core logic are worth it.

In general, I would pick Rust for new projects that do a lot of parsing or communication with other (untrusted or badly written) processes, are CPU limited and need to be fast or power-efficient, require fast startup, and do not deeply use large and irreplacable libraries from some other language. I would want to switch from C or C++ to Rust if the project is something that I use and make changes to often enough for the cost of making the change to be worth it; but this is rare. Switching from existing memory safe languages is probably only worth it when performance is at stake, and it is not practical to convert just the hot code.

I would not currently use Rust for glue scripts, basic file conversion, data analysis, game scripting, or exploratory programming; languages with a garbage collector and a more compact syntax (like Python, Scheme, Haskell, Clojure) tend to be better there.

Often the choice of language is controlled by which libraries are available: I’ve used C++ for many things because it was the easiest interface for a major library (Qt, OpenCASCADE, CGAL, or SDL/OpenGL). C is OK for small programs where most of the content is interaction with C APIs, but the language itself is the limiting factor beyond a certain scale, when proper number handling, string operations, or nontrivial data structures are required.

Finally, a reminder: Waypipe has been available for five years, and using it exposes one’s local Wayland compositor to an application running on a different computer. Even though Waypipe makes some sanity checks on the messages it receives, it cannot guard against bugs in a Wayland compositor. As before, do not assume that Waypipe itself possibly being more secure makes it safe to waypipe ssh into a compromised computer and run GUI programs; Wayland compositors are in general not well tested against adversarial clients.


Home