The Journal

Why Vela isn't written in Rust

Why Vela's compositor is written in C and its shell in C++, rather than Rust: wlroots, Vulkan, and Qt, a single toolchain, and AI-written code.

Every time I talk about Vela, the same question comes up: why not Rust? In 2026, a new Linux desktop with a compositor written from scratch seems like the perfect candidate for the language that has made memory safety its flagship feature.

The short answer is that Rust is a great language, but it is not the right tool for this project. Vela rests on three foundations written in C and C++: wlroots, Vulkan, and Qt. Choosing Rust would have meant fighting against all three, for a level of safety that, in the places where it truly matters, would have ended up inside unsafe blocks anyway.

What Vela is made of

Vela is a complete Linux desktop: a Wayland compositor, a Vulkan renderer, a Qt Quick shell, a file manager, and a settings app. Every piece rests on a foundation that I didn’t write and that there’s no point in rewriting:

  • wlroots 0.20 for backends, Wayland protocols, and buffer management. It is a C library.
  • Vulkan 1.4 for rendering, with no OpenGL fallback. It is a C API.
  • Qt 6 and Qt Quick for the shell, Files, and Settings. It is a C++ framework.

So the right question is not “Rust or C?” in the abstract. It is: which language speaks best with these three foundations and lets me focus my energy on what makes Vela different?

wlroots speaks C

The compositor calls wlroots in almost every file. The wlroots model is built around signals and listeners: an object is born from an event, registers itself on other objects, and dies when its destroy signal arrives. This is precisely where the most insidious lifetime bugs live.

In Rust, there would be two paths:

  1. Use wlroots via bindings. They exist, but they are poorly maintained and struggle to keep up with wlroots releases. Furthermore, the listener model would still have to be wrapped in unsafe: Rust’s safety would stop right at the threshold of the most delicate code. There, unsafe Rust is no simpler than C itself: it has much stricter aliasing rules for mutable references, and in recursive callbacks across graphs of C pointers, creating a temporary reference at the wrong moment is enough to trigger undefined behavior—right where the compiler has stopped protecting you.
  2. Switch to Smithay, the Rust compositor library behind COSMIC and niri. It is the most serious alternative: Wayland protocols, the DRM backend, and input handling are in safe Rust, without C bindings. However, Smithay only includes OpenGL ES and pixman renderers; for Vulkan, it offers initialization and little else. The renderer, which is the part of Vela that sets it apart from other desktops, would still have to be written from scratch, in unsafe Rust.

I chose to stay close to wlroots and write the compositor in its own style: C17, snake_case, wl_list, and a single destroy function for every object that explicitly removes every listener. wl_list is a doubly linked list—the exact example that antirez, the creator of Redis, uses to explain why he sticks with C: in Rust, such data structures require less direct code and carefully written unsafe blocks, whereas in C they are written in the simplest way possible.

This, for example, is the function that destroys a layer surface, such as the taskbar or the wallpaper, in compositor/src/layer.c:

static void handle_destroy(struct wl_listener *listener, void *data)
{
    struct vela_layer_surface *layer = wl_container_of(listener, layer, destroy);
    wl_list_remove(&layer->link);
    vela_focus_forget_layer(layer->server, layer);
    struct vela_output *output = vela_layer_surface_output(layer);
    if (output) {
        vela_output_arrange_layers(output); // frees the space it took
    }
    wl_list_remove(&layer->map.link);
    wl_list_remove(&layer->unmap.link);
    wl_list_remove(&layer->commit.link);
    wl_list_remove(&layer->destroy.link);
    wl_list_remove(&layer->new_popup.link);
    vela_node_destroy(&layer->surface_node->node);
    vela_node_destroy(&layer->tree->node);
    free(layer);
}

No implicit destructors and no shared references: everything that happens when the object dies is written right there, in order.

The shell is Qt Quick

The taskbar, Start menu, quick settings, notifications, Files, Settings, and lock screen are written in Qt Quick: over a hundred QML files and the C++ models that feed them. Qt is a C++ framework, with its own system of meta-objects, signals, and properties.

You can use Qt from Rust with projects like cxx-qt. However, that becomes an extra layer between Vela’s code and the framework: a binding generator to keep updated, errors crossing two languages, and documentation that remains designed for C++. For a UI where most of the logic lives in QML anyway, the payoff doesn’t justify that extra layer. Nor would it be a choice aligned with Vela’s philosophy.

Vulkan: where memory is no longer yours

The heart of Vela is the renderer: app buffers imported as DMA-BUFs, explicit and implicit fences, timeline semaphores, and memory shared with the driver and the kernel. In this territory, memory safety does not depend on the programming language. It depends on contracts that the compiler cannot see: when the GPU has finished reading a buffer, when an app can reuse it, and when the kernel considers it free.

In Rust, this code would be written with ash, and almost every call would remain unsafe.

With Smithay, this wouldn’t change: the renderer would hook into the library by implementing its traits, just as today it hooks into wlroots by implementing the public wlr_renderer interface. The work is the same; only the language changes. Smithay removes unsafe where Vela needs the least help, and leaves all of it in the renderer, where it would need it the most.

A single toolchain

Vela builds with CMake, a C17 and C++20 compiler, and the libraries that distributions already provide. The Arch package and the Fedora RPM follow the standard packaging guidelines of those distros.

Adding Rust would mean adding Cargo, crates to download and keep aligned with distribution policies, longer compile times, and a second way to build the project. For a desktop that aims to be easy to package, every additional toolchain is a cost for package maintainers, not just for me. Dependencies and compile times are also two of the reasons why antirez sticks with C: a package manager encourages piling dependencies upon dependencies, and Rust’s compile times lengthen the loop between making a change and testing it.

I had already documented this for Vela’s polkit agent, where I evaluated Rust: there was no real security gain, because the password still passes through the Qt prompt, and there was the guaranteed cost of a second toolchain.

What Rust would have truly given me

It wouldn’t be fair to say that Rust wouldn’t have brought anything better than C. There are classes of errors that its compiler rejects and that in C I have to catch with other tools:

  • Ignored return values. An unchecked allocation or a discarded Vulkan result in Rust would be an Option or a Result that cannot be absentmindedly ignored.
  • A consistent error policy. In C, every module can decide in its own way how to react to out-of-memory conditions; in Rust, there is a single decision.
  • Lifetimes in internal code. Where the code doesn’t touch wlroots, Vulkan, or Qt, the borrow checker would have genuinely eliminated entire categories of bugs.
  • Protocols in safe Rust. With Smithay, handling Wayland protocols and object lifecycles would have been safe Rust code instead of C listeners to be registered and removed by hand.

These are real advantages. My assessment is that, in a project where the riskiest parts lie right at the boundaries with C and C++, those advantages do not outweigh the cost of writing everything else in a language different from that of its foundations.

Then there is an aspect regarding how Vela is built. The code is written by LLMs: I review it as a developer and make all the architectural and design decisions. One might think this tips the scales toward Rust: a compiler that rejects entire classes of errors seems like the ideal reviewer for generated code.

My experience says otherwise, and not just mine: as antirez points out, LLMs have seen much more C code than Rust, and they reason better in a semantically simpler language. Rust’s complexity—spanning lifetimes, traits, and wrappers around C libraries—doesn’t just weigh on human programmers; it also weighs on the AI, which struggles more to reason about that code. C is a small, explicit language: every allocation, every pointer, and every destruction is written right there in the code, with no hidden mechanisms. Across a large but linear codebase, the AI makes fewer mistakes, and for both me and the model, it is much more straightforward to inspect the execution flow and spot a memory issue because everything is visible on the surface.

This does not mean trusting either the AI or human review alone: Vela’s safety doesn’t rely on never making a mistake, but on machine-verifiable invariants, deterministic tests, and structural rules that the code is forced to obey.

How I recover those guarantees in C

Not choosing Rust does not mean giving up on rigor. It means moving it elsewhere:

  • Written rules for the core. Every struct declares who creates it, who destroys it, and when. No global state, visible allocations, and no hidden allocations in the frame loop.
  • Tests that define behavior. GoogleTest suites cover the frame clock, geometry, color, and configuration. Functional tests cover windows, monitors, session management, locking, and apps that are slow on the GPU.
  • Proofs instead of promises. A test compares the image of a window bit by bit at 100%, 125%, 150%, 175%, and 200% scaling—freshly opened, snapped left and right, maximized, and restored. During the rewrite of the compositor from C++ to C, an A/B harness compared the old compositor and the new one pixel by pixel.
  • Automated tools. Sanitizers (ASan and UBSan), static analysis, and CodeQL run in CI on every pull request. Vulkan validation layers are used during development and in tests executed on real AMD and NVIDIA GPUs.

The next step is to make the C compiler do part of the work that Rust would do: a single allocation entry point with a single policy, and warn_unused_result on functions whose return values cannot be ignored.

What about the future?

This is not an ideological choice, and the door is not closed. Vela is made of separate processes that talk to each other solely through Wayland protocols and a command socket. This makes it possible to write a component in another language without touching the rest.

If Rust ever enters Vela, it will do so at the edges: in an isolated program like the brightness daemon, the reporting tool, or the updater, where it doesn’t have to cross wlroots, Vulkan, or Qt. The core of the compositor stays in C, in the same language as the library it is built upon.

Conclusion

I would choose Rust without hesitation for a project where the most delicate code is also where the compiler can help. Vela is not that project: its most delicate parts talk to Vulkan, the driver, and the kernel, where Rust’s guarantees stop, and Vela’s quality depends on how well they do so.

I chose the language of the foundations on which Vela rests, and I placed the rigor where the compiler cannot reach: in written rules, tests that prove behavior, and tools that hunt for errors automatically. If you want to judge the choice, the best way is to read the code—and it is all right there.