To Port or Not to Port: C++ to Rust, and the Trap of the 90% Demo
Ownership was never in the source code. That's the whole migration — and it's exactly the part the demo skips.
There is a demo making the rounds in every engineering org right now. You point an LLM at a gnarly C++ file, and ninety seconds later you have Rust that compiles. It looks like the migration problem just got solved. Leadership sees it, a quarter gets funded, and a team starts pointing the model at the codebase file by file.
Then, somewhere around the third month, the project stalls. Not dramatically — there’s no postmortem, no cancellation email. It just quietly stops moving. The remaining work never gets easier, the Rust never gets good, and eventually the branch goes stale.
This isn’t a failure of nerve or tooling. It’s a structural property of what porting C++ to Rust actually is — and it’s worth understanding before you spend a quarter learning it the expensive way.
The seduction is real, and so is the wall
The reason the demo is so convincing is that the first 90% genuinely is easy now. Translating C++ control flow, arithmetic, and data structures into syntactically valid Rust is exactly the kind of pattern-matching LLMs are good at. The compiler even helps: if it builds, a lot of bugs are already gone.
The problem is what’s hiding in the last 10%, and it’s the same thing whether a human or a model is doing the work.
C++ does not encode ownership. When you write T* p, the language does not record whether p owns that memory, borrows it, shares it, or aliases something else that’s about to be freed. Those facts live in the programmer’s head, in comments, in naming conventions, and in tribal knowledge about “who frees what.” Rust’s entire value proposition is that these facts must be written down and checked. So a C++→Rust port is not a translation. It’s a reconstruction of invariants that were never explicitly recorded.
So a C++→Rust port is not a translation. It’s a reconstruction of invariants that were never explicitly recorded.
That’s why automatic transpilers are so instructive. Tools like c2rust produce Rust that is “almost always correct” — and almost always unsafe and unidiomatic. They faithfully preserve C’s pointer semantics by wrapping everything in unsafe, raw pointers, and libc calls. The output compiles. It is also, in the words of the research, “very different from the kind of Rust code usually written by humans, making it hard to read and maintain” — and it sails straight past the linters that are supposed to catch the problems. You get Rust syntax with C semantics: all of the migration cost, none of the safety payoff.
LLMs improve on this — they can sometimes infer ownership and produce idiomatic code — but the failure mode is sneakier. As DARPA’s program manager for TRACTOR (the agency’s program to automate exactly this translation) put it: ask any model to translate C to safe idiomatic Rust and “something comes out, and it’s often very good, but not always.” When it’s not, the model tends to reach for unsafe to make the borrow checker stop complaining — which is precisely how you end up with code that looks migrated but carries the same latent bugs it always had, now harder to spot because everyone assumes “it’s in Rust, so it’s safe.”
This is the mechanism behind the stall. The borrow checker doesn’t get tired and wave the last 10% through. Every place where the original C++ had a fuzzy ownership story becomes a hard stop that requires a human to decide what the ownership story should be — and frequently to redesign the data structures to express it. That’s design work, not typing work. LLMs removed the typing bottleneck, which was never the bottleneck.
The evidence cuts both ways — but cleanly
The honest read of the industry data is not “Rust good” or “rewrites bad.” It’s that the shape of the project predicts the outcome far better than the language choice does.
Look at what the wins have in common. Android never ported anything — Google’s entire strategy is that new code is Rust and old C++ is left in place; the safety gains come from the fact that “most bugs live in new code”, so you don’t need to touch the old stuff to bend the curve. Pingora wasn’t a translation at all; it was a clean-room reimplementation against a known spec, where the team got to design the ownership model from scratch instead of recovering it from someone else’s pointers. Mozilla picked off self-contained, parallelism-hungry components one at a time and integrated each before starting the next.
Now look at curl. Daniel Stenberg’s team spent roughly five years (2020–2025) trying to let curl use the Rust hyper library as an HTTP backend through an FFI glue layer — and ultimately ripped it out. The reasons are a near-perfect catalog of why these projects die:
The last 5% was the whole problem. “The final few percent would turn out to be friction enough to now eventually make us admit defeat.”
Nobody to do the hard part. It needed someone fluent in both C and Rust and the HTTP protocols to drive the integration across the FFI boundary — and almost no one wanted to.
The FFI seam leaked. They had to remove HTTP/2 support because they’d misunderstood how the API integrated across the language boundary.
No demand pulling it forward. curl users didn’t care that a backend was in Rust; Rust users didn’t want to maintain glue for a C project.
Stenberg’s conclusion is the bluntest data point in the whole debate: “We’re not going to rewrite curl in Rust. In any language — we’re not going to rewrite it at all.” This from someone who openly acknowledges that roughly half of curl’s historical vulnerabilities are C memory mistakes. He knows the safety case cold. He’s declining anyway, because the migration cost is real and the existing C is hardened by decades of fuzzing and CVE wrangling.
The hidden tax: comprehension debt
There’s a second-order cost that the demo never shows you, and it’s worse with LLM-driven ports than with hand ports.
When a model emits 4,000 lines of plausible Rust, no one on your team owns the mental model of that code. The 2025 research on LLM-generated code calls this comprehension debt and “ownership debt”: “it compiles” is not the same as “someone understands why it’s correct and can change it safely.” With a hand-written port, the engineer who fought the borrow checker comes out the other side understanding the ownership model — that understanding is half the point. With a generated port, you’ve skipped the understanding and kept only the artifact. The first time it needs a non-trivial change, you pay the comprehension cost you deferred, except now under deadline and without the person who’d have built the model.
This is why “the LLM did 90% in a week” is a misleading metric. The week of typing was never the expensive part. The expensive part is owning a correct mental model of a safety-critical system — and that’s exactly what gets skipped when the code arrives pre-written.
A decision framework that actually predicts outcomes
Stop asking “should we port to Rust?” It’s the wrong altitude. Ask these instead:
1. Is this new code or existing code?
If new, strongly favor Rust, and stop reading. The Android data is overwhelming: the cheapest safety win is to stop adding to the C++ pile. No migration risk, all the upside.
2. If existing — is the component self-contained, or is it threaded through the codebase?
A module with a narrow, well-defined interface (a parser, a codec, a proxy data path) is a Stylo/Pingora candidate. A module whose pointers reach into everything will trap you in FFI hell: every call across the C++/Rust seam is an unsafe boundary where Rust’s guarantees evaporate and new bugs breed. The curl/hyper failure was fundamentally an FFI-boundary failure.
3. Do you have a spec, or only the code?
If you can reimplement against a specification or a strong test suite (Pingora had the HTTP spec; you can have a differential test harness), favor a clean-room rewrite over a line-by-line port. You’ll design the ownership model instead of reverse-engineering it, and you can run old and new side by side to prove equivalence.
4. Will someone own the result?
If the answer is “the LLM wrote it and we’ll figure it out later,” you don’t have a migration plan, you have a future incident. Budget the human comprehension cost up front or don’t start.
5. Is the existing C++ actually your risk?
Battle-hardened, heavily-fuzzed C++ that hasn’t thrown a CVE in years is not where your memory-safety risk concentrates. New and frequently-changed code is. Spend the migration budget where the bugs actually are.
The verdict
Use the LLM. It genuinely removed a real cost — the mechanical translation that used to make these projects unaffordable to even start. But understand precisely what it removed and what it didn’t. It removed the typing. It did not remove the thinking: recovering ownership invariants that C++ never recorded, designing data structures that satisfy the borrow checker, and proving the result is equivalent to the original.
That thinking is the project. It was always the project. The reason ports stall at 90% is that the demo completes the 90% that was never hard, and then hands you the 10% that was the entire point — now disguised as “almost done.”
Port new code by default. Port self-contained components against a spec. Leave hardened, stable C++ alone. And treat any plan whose timeline is built on “the model already wrote most of it” as a plan that hasn’t started yet.



