Rewriting the world in Rust

Rewriting the world in Rust

“Perhaps we should not replace the payroll system, the avionics stack, and the office kettle firmware all before lunch.”

Image by Cady Galleta

If you have a very large (millions of lines of code) codebase, written in a memory-unsafe programming language (such as C or C++), you can expect at least 65% of your security vulnerabilities to be caused by memory unsafety.
—Alex Gaynor, What science can tell us about C and C++’s security

In recent years, governments have called for universal adoption of memory-safe languages—and especially Rust—to improve software security. Microsoft has also argued for Rust as its default language for new projects.
—The Register, Microsoft wants to replace all its C and C++ code by 2030

More and more critical software is being written in Rust, which is great, but what about the software that’s already been written?

The modern world runs on hundreds of millions of lines of memory-unsafe code: Linux, Windows, databases, cloud infrastructure, mobile operating systems, banking systems, embedded devices, medical equipment, IoT. In other words: basically everything.

Should we rewrite all of it in Rust? Could we?

And where would we even start?

Some things that won’t work

The current proposals seem to boil down to two main approaches: “rewrite everything from scratch”, or “auto-translate it all to bad Rust”. Both of these are horrible, horrible ideas. Let’s not do them.

We’ll touch on a few slightly more practical strategies and tools for migrating large and complex codebases to Rust. But first, let’s explode a couple of myths.

Myth 1: the stop-the-world rewrite

“Good news, everyone: we’re stopping all feature work for the next two years while we rebuild the product from scratch in Rust.”

No maintenance. No bug fixes. No customer-visible improvements. Just an expensive parallel rewrite that probably won’t work properly for quite a while.

No sane business is volunteering for this. Not gonna happen.

There’s a reason you haven’t heard about anyone doing this “stop-the-world” Rust rewrite successfully: it’s a fundamentally bad idea.

“Ah, but wait,” say the AI evangelists. “You haven’t heard my AI pitch yet.”

Myth 2: AI will fix everything

You might have read the post by a slightly over-enthusiastic Microsoft researcher about using AI tools to automatically translate the 50 million lines of C++ code in Windows to Rust by 2030. It got a lot of press, and that’s no surprise: if this kind of thing were really possible, it would be amazing.

Microsoft later walked this back a little, emphasising that it’s a long-term research project aimed at building AI-powered tools to help with code migration. In other words, this isn’t possible yet, and probably never will be, in the sense of “AI just does all the hard work for us”.

Unfortunately, while AI tools are effective at small-scale tasks, their performance gets worse and worse as you ask them to do bigger jobs:

For tasks that would take a human under four minutes—small bug fixes, boilerplate, simple implementations—AI can now do these with near-100% success. For tasks that would take a human around one hour, AI has a roughly 50% success rate. For tasks over four hours, it comes in below a 10% success rate.
—Alasdair Allan, The ladder is missing rungs

So, as a rough guide, if it would take you more than four hours to migrate your project to Rust (and it would), then AI won’t magically solve the problem. Even if you could afford the token costs, you’d probably spend more time reviewing, correcting, and integrating the generated code than you would have spent implementing large parts of it yourself.

AI can be useful as a supervised accelerator, but it’s not an autonomous migration engine. The Ladybird browser project recently migrated a subcomponent to Rust with AI assistance, but this wasn’t a “fire and forget” deal: they used hundreds of small prompts, steering the AI agents carefully at each stage and pausing frequently to clean up the inevitable issues.

Lost in translation

Automated translation, whether via AI or tools like c2rust, can produce working Rust programs, but the result often feels less like native Rust and more like C++ wearing a fake moustache. It’s not idiomatic Rust.

The problem is that Rust is a very different language from C++, and many of the differences are precisely the reasons people want to migrate: memory safety, ownership, data race prevention, and so on. These aren’t surface syntax changes: they’re fundamentally different ways of structuring programs.

Much existing C++ code is also deeply object-oriented, with complex inheritance hierarchies and tightly-coupled mutable state. Rust deliberately pushes developers toward different design patterns.

If you just transpile C/C++ to Rust, you will very likely produce rather inefficient code using refcounts all over, or code which is ‘unsafe’ in Rust’s sense, losing the advantages of the language. This is the painful experience of about everybody who attempted to port an existing code base into Rust.
Wolfgang Grieskamp

Similar problems affect translations from Zig; for example, the notorious recent vibe-porting of the Bun JS runtime from Zig to Rust left the codebase a mess of “unidiomatic, Zig-like code”, as one expert said, with around 13,000 unsafe blocks that will presumably need to be checked or cleaned up. That careful, critical work may take a lot longer than the exhilarating “tests pass, ship it” phase that tends to make ripples on social media.

In other words, the translation is not the hard part, so speeding it up with AI doesn’t help much. It’s just the beginning of a long process of re-engineering a project to actually be sound, memory-safe, and correct.

We need a re-think.

Things that might work

Rust isn’t just another dialect of C++ or Zig, and migrating to Rust isn’t a syntax conversion exercise. Rust’s ownership and borrowing model fundamentally changes how programs are structured.

To get the benefits Rust promises—memory safety, fearless concurrency, fewer security vulnerabilities—you usually have to re-think the architecture of the program itself.

Re-thinking it in Rust

In Rust, you need to carefully reason from the beginning about the ownership of data. This has implications for your data organization as well as some algorithms which propagate into the system architecture. You have to design the code from the ground up for the memory ownership model.
Wolfgang Grieskamp

Rust is a modern, memory-safe language that delivers the same high performance as C++ and Zig, but with dramatically fewer bugs and security holes. It’s now the recommended industry best practice to use Rust for all new software.

Rust’s biggest selling point is that it turns run-time problems into compile-time problems. Safety is built into the language: the compiler can catch problems like dangling references, use-after-free, and data races before the program is even deployed to production.

C and C++ are notoriously difficult to write correctly, largely due to the burden of manual memory management and its associated pitfalls. Rust offers the same low-level performance while providing memory safety by default.
Animaj Rust integration

C++ has caused more than a few situations where we wanted to do something or add a feature and it’s just like… that’s a cool idea, but it’s just not worth the uphill battle against the language. And it was to the point where any change carries the risk of unforeseen consequences.
We rewrote our C++ infrastructure in Rust

Making C++ code more Rust-like

The first step toward safer software may simply be improving the code we already have.

C++ has been around for decades, and developers have gradually evolved safer patterns, abstractions, and tooling that let us bring some of Rust’s new ideas about ownership, lifetimes, and correctness to C++ programs too.

For example, the RCC project can impose a Rust-like profile on C++ code, including ownership and move checks, lifetimes and reference safety, concurrency safety, null-checking and error handling.

There’s also the Safe C++ project, which aims to extend the language with new memory-safe constructs that, like Rust, require an explicit opt-out in order to bypass the compiler’s safety checks.

From spaghetti code to Lego blocks

If your codebase is a tangled bowl of spaghetti, safely changing anything becomes difficult—never mind migrating it to Rust.

The first steps might look like this:

  1. Draw clear API boundaries between parts of the system that need to be decoupled.
  2. Define behaviour contracts that these components must obey.
  3. Add tests to make sure they actually obey them right now.

Once you understand how a component is supposed to behave, and can verify that the current implementation obeys those rules, you’re in a position to start replacing pieces safely, one subsystem at a time. Software design guru Martin Fowler has named this the “Strangler Fig” pattern, and he makes the point that the organisation that produced this legacy code often needs to change as well to make the modernisation project a success.

Research, experience, and plain common sense all point in the same direction: software is easier to maintain, evolve, and replace when it’s built from stable components with well-defined interfaces—more like Lego blocks than spaghetti.

In other words, fix the architecture first, before trying to change the implementation. Even if you never get past this stage, you’ll have added a lot of value, and if you do go further, you’ll find the remaining stages easier.

Rust is eating the world, one bite at a time.

Next, take one subsystem and rewrite it in Rust, validating its behaviour against the existing implementation until the outputs are byte-for-byte identical. Put it in production and let the bugs shake out for a while. Then move on to the next component.

Tools such as Crust can give you a head start on the translation of a particular module, or at least identify any parts that are likely to be major stumbling blocks.

Each subsystem you replace with a safer, more modern, and more performant Rust version is another win. Start with small, low-risk components where you can gain experience quickly and a failure won’t threaten the entire project.

Once you have a component working, replace it in production, and then progressively refactor the code to be more Rust-like, re-thinking data flow and ownership, and gradually shrinking unsafe boundaries.

Rust has excellent interoperability with other languages; you can use libraries such as cxx to manage calls between your Rust and C++ components with contracts that can be checked by static analysis.

Just so you don’t feel over-sold, let’s say explicitly that Rust doesn’t prevent every bug. It’s possible to write incorrect programs in any language, and Rust is no exception. Many software problems are actually caused by bugs or misfeatures in the underlying operating systems, and—until these too are fully rewritten in Rust—we still have to deal with them.

But it would be absurd to say, “Rust only prevents 70% of bugs, therefore it isn’t worth adopting.” Eliminating a large fraction of memory-safety and concurrency bugs is an enormous engineering win.

Because of this, and despite the fact that migrating existing software to Rust is a big challenge—one that we can’t just wave an AI magic wand to solve—people are undertaking it anyway. Rust is eating the world, one bite at a time.

And as we migrate our programs to Rust, carefully and methodically, one by one, we can take the opportunity to re-think them from scratch and make sure we’re actually solving today’s problems, not yesterday’s.

Otherwise, we might just end up replacing one kind of technical debt with another.

Thanks to Matthias Endler for his valuable feedback on this article.

The power of GoLand: 10 hidden spells

The power of GoLand: 10 hidden spells

1