Rewriting in Rust
During my career facing legacy code has always been an annoying task and it took me quite some years to understand, that oftentimes today’s code is tomorrow’s legacy. Still, legacy code can be a great opportunity to learn something new and especially when you are the original author of the piece.
This post jumps on the bandwagon of rewriting everything in Rust and elaborates a bit on my personal motivation and learnings of rewriting my pet window manager project subtle, which I started ~20 years[1] ago and still use it on a daily basis.
Why? &
Among the many things AI can do for us, migrating code from one language into another is usually a strong selling point and even without AI there are excellent tools on its own, like C2Rust, to get the job done with just a flick of a finger.
So why is an excellent question.
One of my main motivators isn’t just to get the job done, like I lamented on a bit in my {{ site.url }}{% post_url 2025-09-22-fear_of_missing_out_on_ai %}[previous blog post], but to have a learning experience and take something from it besides another code base, which easily ticks every point of the legacy code checklist.
Manual labor isn’t probably the most controversial aspect of it, but porting an X11 application in the day year epoch of Wayland might look like a waste of time.
Alas, the reasoning here is basically the same. Plus I’ve spent many years with X11 learning its core concepts and still like the system and capabilities.
On a side note - I am not entirely certain there is a giant switch to get rid of X11 yet, despite how decisions of e.g. the GNOME project[2] might appear.
Learnings so far &
Porting a codebase, like the one of subtle with 14728 LoC (according to sloccount[3]), brought loads of challenges with it. Some of them were the usual ones like "where to start" and how can this be done in language X, but let us concentrate here on a handful of interesting points.
| The problems are inter-related, and it is sometimes a chicken or the egg-type of problem which to address first, so please be prepared to jump a bit around if necessary. |
God objects &
When I started subtle back then, I didn’t even know that this pattern is called God Object or that it is considered to be prime example of an anti-pattern. To me it was something that I’ve learned by reading other people’s code and looked like a good solution for a problem, which is still relevant today.
Problem &
The main problem is kind of easy to explain and mainly related to software design: Your program needs to keep track of data like state or socket descriptors and many related functions have to access and sometimes mutate them.
There are several ways to tackle it, like moving everything related together, but this can also mean there is basically just one big file and C isn’t the strongest language to enforce a proper structure and coherence. It was way easier to have a global object which included every bit and was available throughout the program.
This might obviously lead to interesting side-effects in multi-threaded applications, but fortunately the design goal of subtle has always been to be single-threaded and no other means of locking were required.
What I did not understand back then and which is more of concern here, is the implicit coupling of everything to this god object. This means changing the god object may require changes of other parts of the program and also may unknowingly break other parts of the application.
Solution &
subtle-rs (as its predecessor) is event-driven and many parts revolve around a single connection to the X11 server. This connection must be available to most parts and moving everything into the holding object made proper separation of concerns more difficult.
Like every worth-while decision this is a classical trade-off and the original design was kept with the addition to carry the dependency explicitly through the codebase.
C version &
void subClientSetSizeHints(SubClient *c, int *flags) {
...
}
Rust version &
pub(crate) fn set_size_hints(&mut self, subtle: &Subtle, mode_flags: &mut ClientFlags) -> Result<()> { (1)
...
}
| 1 | The signature includes a reference to Subtle. |
RAII &
Resource acquisition is initialization (RAII) is another programming idiom, which is less of a concern in C-based languages, but can turn into a problem in strict languages like Rust. Simply put this just means whenever we initialize something like a holding structure, we also have to initialize all of its members due to the general idea of predictable runtime behavior and zero-cost abstraction.
Problem &
This easily turns into a problem, whenever the holding structure contains something, that requires some preparation before it can be initialized - like a socket connection:
struct Holder {
Connection *conn;
}
Holder *holder = calloc(1, sizeof(Holder)); (1)
holder.conn = MagicallyOpenConnection(); (2)
| 1 | Init the holding structure |
| 2 | Open the actual connection |
Solution &
Since this a more general problem in Rust, there exists a bunch of options with different ergonomics. One of the easiest ways is to wrap the connection in {option][Option], which can be initialized with its default value and set later, but as I’ve said the ergonomics of mutating[4] something on the inside are bothersome.
A better option alternative is let one of the many cells[5] handle this job. OnceCell, as the name implies, offers an easy way to initialize our socket once we are prepped.
C version &
struct subtle_t {
...
Display *dpy; ///< Subtle Xorg display
...
} SubSubtle;
extern SubSubtle *subtle; (1)
| 1 | God mode - on! |
void subDisplayInit(const char *display) { (1)
...
/* Connect to display and setup error handler */
if (!(subtle->dpy = XOpenDisplay(display))) {
...
}
| 1 | We usually pass the ENV var DISPLAY, but NULL is also an accepted value. |
int main(int argc, char *argv[]) {
...
/* Create subtle */
subtle = (SubSubtle *) (subSharedMemoryAlloc(1, sizeof(SubSubtle))); (1)
...
}
| 1 | This is just calloc with some error handling. |
Rust version &
pub(crate) struct Subtle {
...
pub(crate) conn: OnceCell<RustConnection>,
...
}
impl Default for Subtle { (1)
fn default() -> Self {
Subtle {
...
conn: OnceCell::new(), (2)
...
}
}
}
| 1 | Unfortunately deriving the Default trait doesn’t work for all members of Subtle. |
| 2 | This initializes our OnceCell with its default value. |
pub(crate) fn init(config: &Config, subtle: &mut Subtle) -> Result<()> {
let (conn, screen_num) = x11rb::connect(Some(&*config.display))?;
....
subtle.conn.set(conn).unwrap(); (1)
....
}
| 1 | Error handling here would require more explanation, so let us just forget about it and move on. |
fn main() -> Result<()> {
...
// Init subtle
let mut subtle = Subtle::from(&config); (1)
...
display::init(&config, &mut subtle)?;
...
}
| 1 | Config holds the configured values - a courtesy of clap - and we convert it with
the help of our From trait implementation. |
Borrow checker &
Did you wonder why the (in)famous borrow checker isn’t number one on our list of problems? Well, simply because you can come pretty far without running into beloved errors like E0499 or E0502 and grouping problems to keep a common thread is quite difficult.
Anyway, back to the topic at hand: Why can’t we just keep a mutable reference of our god object all the time and pass it around?
Problem &
Interestingly this is again more about software design and Rust’s pragmatic way of handling mutability in contrast to other (functional) languages like Haskell. Please have a look at the next code block:
#[derive(Default)] (1)
struct Counter {
number: u32,
}
impl Counter {
fn increment(&mut self) { (2)
self.number += 1;
}
fn print(&mut self) { (3)
println!("number={}", self.number);
}
}
fn increment_counter(counter: &mut Counter) { (4)
counter.number += 1;
}
fn print_counter(counter: &mut Counter) { (5)
println!("counter={}", counter.number);
}
fn main() {
let mut counter = Counter::default();
counter.increment(); (6)
counter.print(); (7)
increment_counter(&mut counter); (8)
print_counter(&mut counter); (9)
}
| 1 | Derive is one of Rust’s real work horses. |
| 2 | Mut required due to write to binding. |
| 3 | Is mut required here? |
| 4 | Mut! |
| 5 | Mut? |
| 6 | Implied mut! |
| 7 | Implied mut? |
| 8 | Mut! |
| 9 | Why mut? |
If you don’t mind trailing all those terribly explicit mut keywords the above code runs fine and
if you don’t try to re-borrow anything the aliasing rules work in your favor.
A different story is the coupling and the cognitive load: When everything gets a mutable reference, everything is coupled together and you can never be sure about the side-effects of calling a certain function.
Solution &
The easiest and most naive solution to this kind of problem is just omit mut wherever possible.
#[derive(Default)]
struct Counter {
number: u32,
}
impl Counter {
fn increment(&mut self) {
self.number += 1;
}
fn print(&self) { (1)
println!("number={}", self.number);
}
}
fn increment_counter(counter: &mut Counter) {
counter.number += 1;
}
fn print_counter(counter: &Counter) { (2)
println!("number={}", counter.number);
}
fn main() {
let mut counter = Counter::default();
counter.increment();
counter.print();
increment_counter(&mut counter);
print_counter(&counter); (3)
}
| 1 | This access is just read-only, so no need for mut and also a promises of being side-effect free. |
| 2 | See ❶! |
| 3 | See ❶! |
Interior mutability &
Now its getting interesting, and we have to talk about given promises of immutability and one more time about ergonomics of our general design.
With the last problem we established the underlying promise of functions, that don’t require a mutable reference, will never change the object itself and only changes made to a mutable reference are of any consequence to you.
Problem &
What happens, when you need to change some internal state, which is just required for internal bookkeeping and doesn’t change anything at all for the caller?
Have a look at following contrived[6] example:
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Default)]
struct Counter {
number: u32,
last_printed: u32,
}
impl Counter {
fn increment(&mut self) {
self.number += 1;
}
fn print(&mut self) { (1)
self.last_printed = SystemTime::now()
.duration_since(UNIX_EPOCh).unwrap().as_secs() as u32; (2)
println!("number={}", self.number);
}
}
fn main() {
let mut counter = Counter::default();
counter.increment();
counter.print();
}
| 1 | To allow our internal bookkeeping the signature must include mut now. |
| 2 | Error checking skipped for brevity - unwrap all the things! |
Here we had to change the methods signature just to allow the pointless action of storing the last printing time, maybe for big data applications, who knows.
From the caller’s perspective it doesn’t make any sense to pass a mutable reference into the
print function and from the counter’s perspective[7] there wasn’t any
actual change of the number.
Solution &
This is a pretty common problem and Rust provides many different options like Cell and RefCell, Atomic and some more advanced options like the smart pointer Arc for more shenanigans.[8]
In our case Cell works splendidly here for our type comes prepared with the copy trait:
C version &
typedef struct subsubtle_t { /* {{{ */
...
int visible_tags; ///< Subtle visible tags
...
} SubSubtle;
void subScreenConfigure(void) {
...
/* Reset visible tags, views and available clients */
subtle->visible_tags = 0; (1)
...
/* Set visible tags and views to ease lookups */
subtle->visible_tags |= v->tags;
...
}
| 1 | No one can stop us from just accessing our god object directly. |
Rust version &
pub(crate) struct Subtle {
...
pub(crate) visible_tags: Cell<Tagging>,
...
}
impl Default for Subtle {
fn default() -> Self {
Subtle {
...
visible_tags: Cell::new(Tagging::empty()),
...
}
}
}
pub(crate) fn configure(subtle: &Subtle) -> Result<()> {
...
// Reset visible tags, views and available clients
let mut visible_tags = Tagging::empty(); (1)
...
// Set visible tags and views to ease lookups
visible_tags.insert(view.tags);
...
subtle.visible_tags.replace(visible_tags); (2)
...
}
| 1 | This is a pretty easy case: We introduce a local variable via let binding first. |
| 2 | And then once we are happy with the result we tell the cell to swap-out the content entirely. |
Explicit copies &
Likewise with mutability, Rust in a similar way annoyingly verbose and explicit with how it handles data and copies of it. Seems like keeping all the guarantees and promises there has to be done some work upfront from every side.
Problem &
In the next example we just continue with the counter from before, but the repetition of the struct definition and implementation itself have been removed, since they just divert from the actual problem:
...
fn print_counter(counter: &Counter) {
counter.print();
}
fn main() {
let mut counter1 = Counter::default();
counter1.increment();
let counter2 = counter1; (1)
print_counter(&counter1);
print_counter(&counter2);
}
| 1 | D’oh! |
The above snippet fails to compile for apparent reasons, still the error message of the compiler is kind of a surprise in its detail and content:
error[E0382]: borrow of moved value: `counter1`
--> src/main.rs:27:19
|
21 | let mut counter1 = Counter::default();
| ------------ move occurs because `counter1` has type `Counter`, which does not implement the `Copy` trait
...
25 | let counter2 = counter1;
| -------- value moved here
26 |
27 | print_counter(&counter1);
| ^^^^^^^^^ value borrowed here after move
|
note: if `Counter` implemented `Clone`, you could clone the value
--> src/main.rs:2:1
|
2 | struct Counter {
| ^^^^^^^^^^^^^^ consider implementing `Clone` for this type
...
25 | let counter2 = counter1;
| -------- you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `example` (bin "example") due to 1 previous error
Solution &
This is just an example of a really overwhelming and also quite helpful error message from our partner in crimes - the Rust compiler. What it points out here is that we can just add the copy trait marker and also implement the clone trait to satisfy this move.
And like our friendly compiler told us, when we just do as suggested the code runs perfectly fine:
#[derive(Default, Clone, Copy)]
struct Counter {
number: u32,
}
This innocent assignment over there just introduced the concept of move semantics, that Rust uses internally in its affine type system:
An affine resource can be used at most once, while a linear one must be used exactly once.
The definition is quite heavy and somehow unwieldy, but what it basically says, is every type that
doesn’t come along with a Copy marker trait is moved and the ownership transferred to the
recipient.
All other types are just copied along the way.
Accessing the object afterward is a violation of the ownership[9] model and hence causes such an error.
Conclusion &
Writing this blog post has been an interesting experience on its own and helped me to sharpen my understanding of how Rust internally works and also helped me to summarize what I actually learned about it over the course of this project.
Porting such a large codebase from my past into a modern language and also re-visiting many of the taken design choices have been a great experience so far. And in regard to the legacy code aspect I mentioned initially - there are tests but still even I don’t understand some of the odd namings for variables and steps in algorithm anymore. Maybe I should have read Clean Code some years earlier [cleancode]..
I currently do not dare to use subtle-rs as my daily window manager yet, mainly because some required features are still missing like something simple to bring e.g. a clock into the panel, but I am eagerly looking at Extism for this matter.
Naturally I’ve read some books about Rust if you are looking for inspiration:
Most of the examples were taken from following repositories:
Bibliography &
-
[idiomaticrust] Brenden Matthwes, Idiomatic Rust: Code like a Rustacean, Manning 2024
-
[coderustpro] Brenden Matthwes, Code Like a Pro in Rust, Manning 2024
-
[asyncrust] Maxwell Flitton, Caroline Morton, Async Rust: Unleashing the Power of Fearless Concurrency, O’Reilly 2024
-
[effectiverust] David Drysdale, Effective Rust: 35 Specific Ways to Improve Your Rust Code, O’Reilly 2024
-
[cleancode] Robert C. Martin, Clean Code: A Handbook of Agile Software Craftsmanship, O’Reilly 2007