*Disclaimer: This article was created with the assistance of AI and reviewed by the JetBrains RustRover team. *
C and C++ to Rust migrations are no longer just an experimental idea. More teams are now looking at Rust as a practical way to improve memory safety, reduce long-term maintenance costs, and modernize performance-critical systems. But a successful migration is not about rewriting everything just because Rust is popular. The hard question is more practical:
‘’Why should teams consider Rust for existing C or C++ systems, which parts of those systems should move to Rust, and how do you do it without breaking what already works?’’
This was one of the main topics in our recent livestream with Luca Palmieri from Mainmatter, author of 100 Exercises To Learn Rust and the upcoming
, and C to Rust MigrationbookVitaly Bragilevsky from JetBrains. Mainmatter works with teams adopting Rust in real-world software projects, including through consulting, training, and migration support. In the livestream, Luca Palmieri shared what that practical experience shows: where C and C++ to Rust migration projects succeed, where they usually get stuck, and why incremental migration is often the safer path.
The full livestream is available here:
Tldr: For many production systems, the safest C++-to Rust migration strategy is not a full rewrite. It is an incremental migration: start with isolated modules, make Rust work inside the existing build and release process, and expand gradually as confidence grows.
*Related resources: Mainmatter offers Rust consulting for teams planning or running migration projects. For a deeper look at migration patterns, check out Mainmatter’s C to Rust Migration book. And if your team wants to strengthen its Rust foundations first, Luca Palmieri’s 100 Exercises To Learn Rust is available as a JetBrains Academy course. *
Why teams are migrating from C and C++ to Rust #
A few years ago, suggesting a C or C++ to Rust migration could feel risky. Nobody wants to be the canary in the coal mine, especially when the project is load-bearing, serving production traffic, or important to the business. If a team is investing years of engineering effort into a migration, they need to know they will not discover a roadblock halfway through.
That hesitation is weaker today. Companies like Google aren’t just migrating to Rust; they’re publishing data arguing that Rust adoption improves safety, especially by reducing memory-safety vulnerabilities, and reducing defect rates compared to their C++ predecessors. Beyond de-risking, there are several factors that have had an impact on today’s situation:
Expertise is spreading. Engineers who learned Rust at one company bring that knowledge to their next role, creating a multiplier effect across the industry.Tooling has matured. The ecosystem gaps that made early adoption painful have largely been filled.Hiring concerns are fading. The argument that “we can’t find Rust developers” loses substance when more developers have production Rust experience.AI assistance reduces friction. Generative AI tools help flatten the learning curve, making the initial ramp-up less daunting.
The question has changed. Teams are now asking whether their C or C++ codebase has a problem that looks like a good fit for Rust. For a broader look at how the two languages compare, including memory safety, performance, and concurrency, see our Rust vs C++ comparison. Let’s see which projects should migrate and how.
Which Projects Should Migrate? #
Not every C or C++ project benefits from a Rust migration. For hobby projects, the calculus is simple: migrate if you want to learn Rust or if it feels right. For production systems, migration needs to make business sense. The strongest candidates are usually codebases where maintainability is already expensive:
Performance-sensitive codebases where squeezing every ounce of efficiency pushes teams toward complex patterns that are hard to reason about. Rust’s safety guarantees don’t compromise performance, but they do make those complex patterns more manageable.Concurrent or multi-threaded systems where the borrow checker provides safety nets that are difficult to replicate in C++. Data races and memory safety issues that require constant vigilance in C++ become compile-time errors in Rust.Security-critical components where vulnerabilities carry high costs. If your code is an attractive attack target, preventing memory safety issues before they reach production has clear economic value.High-scale deployments where even modest efficiency improvements translate to meaningful infrastructure savings. If migrating to Rust lets you run on less powerful hardware, those savings compound quickly.
The common thread is maintainability. Successful migrations improve the ability to ship features faster, reduce defect rates, or both. The migration cost must be justified by reduced rework, fewer security incidents, improved developer productivity, or operational savings.
C++ to Rust migration: Full Rewrite vs. Incremental Migration #
The choice between a complete rewrite and an incremental migration isn’t ideological. It depends on your deployment model, codebase characteristics, and available testing infrastructure.
A full rewrite can sound appealing. You start from scratch, set up the codebase the way you want, draw the boundaries where you like, and leave old decisions behind. But let’s see where a full rewrite makes sense?
When Full Rewrites C/C++ to Rust Make Sense
- The codebase is relatively small, and the scope is tractable
- You have an exhaustive black-box test suite that validates behavior without assuming internal structure
- The API surface is well-defined and stable
- You control the deployment environment
Services deployed as backends can be good candidates. You can route shadow traffic to the new implementation, compare responses against the old system, and gradually shift load. If something breaks, you have multiple levers to pull: roll back instantly, route only a tiny percentage of traffic, or limit the migration to specific customer segments.
The key is confidence-building mechanisms. You need mechanisms that prove the new implementation behaves like the old one, including the small details that users may unknowingly depend on.
When Incremental Migration Is Better
For many teams, the safest C++ to Rust migration path is incremental, especially for large, active, or customer-deployed systems. Instead of replacing the whole codebase at once, you migrate one piece, ship it, learn from it, and continue. Success looks like steady progress, ideally accelerating progress. One release may contain 95% C or C++ and 5% Rust. A later release may contain 90% C or C++ and 10% Rust. Over time, the Rust part grows, the old code shrinks, and the team keeps watching the important signals such as defect rates, performance, user feedback, and developer velocity.
The value of this approach is that every migrated piece is integrated into the real product. Users get the new code. The team sees whether it performs better or worse. Bugs show up in the normal issue tracker. The migration builds confidence release by release.
Ideally, the more Rust you have, the easier it becomes to add more Rust. The lift-off phase is the hardest. Once the build system, testing setup, FFI conventions, and release process are in place, the next modules should be easier than the first.
Incremental migration also makes sense when institutional knowledge matters. If you migrate piece by piece, the developers who maintain the code stay involved throughout the process. A full rewrite risks knowledge loss: you might end up with better-structured code, but nobody remembers why certain decisions were made.
Where to start: eat the graph from the leaves #
One practical approach is to look at the module graph and find isolated modules. Start from the leaves: modules that have no dependencies, or only a few dependencies on the rest of the system. This is usually the easiest starting point because the first Rust code does not need to call into C or C++ code. It only needs to be callable from the existing C or C++ codebase.
In other words, the Rust module exposes an extern API, but inside it can still be structured as normal Rust. This matters more than it seems. Before you write meaningful Rust code, you need to solve integration problems:
**How does Rust fit into the existing build system?****How do C, C++, and Rust code link together?****Can memory sanitizers still run across language boundaries?****Does everything work on all supported platforms?**How will the team test and release mixed-language code?
Starting with a simple, low-complexity module lets you solve these problems before tackling harder software challenges. You want to ship a line of Rust that does almost nothing but builds correctly, links properly, and works in all your CI flows.
Once that first module is done, you’ve freed other modules from dependencies. You tackle those next, gradually expanding your island of Rust code. Eventually, you have C on the outside and Rust on the inside, and you keep expanding until the C disappears.
The downside is that you might spend months working on modules far from the action, modules that haven’t been touched in years. It can feel like you’re not adding business value. But you’re building the foundation that lets you rewrite the complex, important parts without managing C dependencies underneath.
The alternative: vertical slices #
The opposite approach is to drive a specific user flow or feature through Rust, cutting a vertical slice through the stack. This puts Rust code in the critical path immediately, demonstrating business value from day one.
The challenge is complexity. Your Rust code will constantly call into C and be called from C. You’ll have raw pointers everywhere. It will be Rust, but it will feel like C-style Rust. You won’t get safety benefits for a long time because most of the action happens outside the domain the borrow checker can verify.
This approach can leave teams wondering: “Is this Rust code actually better than the C++ it replaced? It looks and feels the same.” Both strategies have merit. Bottom-up from the leaves is generally cleaner and allows for better restructuring. Vertical slices show business value faster but require navigating more complexity upfront.
Rust FFI is where the migration gets tricky #
The hardest part of incremental migration is crossing the language boundary. Inside pure Rust, the compiler helps enforce ownership, borrowing, and lifetimes. You can try a design, and the borrow checker tells you whether the memory-safety story works.
Once raw pointers cross between Rust and C or C++, that safety net becomes weaker. The programmer has to track assumptions manually:
- Is this pointer still alive?
- Does it have aliases?
- Who owns the value?
- Who is allowed to free it?
At that point, you become the borrow checker. This is where Rust FFI becomes central. The team needs clear rules for ownership, allocation, and cleanup.
A useful principle is: whoever allocates memory should free it. Avoid allocating in C and freeing in Rust, or the other way around, unless the boundary is designed very carefully. Unsafe code is expected in mixed C, C++, and Rust codebases. That does not make the migration wrong.
It means unsafe code needs to be treated as an important design surface, not as glue code nobody reviews. A lot of migration work is about making behavior that already exists in C or C++ visible and correct in the eyes of the Rust compiler. That can feel like hard work, but it is essential. The point of moving to Rust is to benefit from static analysis, and the code has to be structured in a way that lets those tools help.
What to do before you start migrating #
A C++ to Rust migration is not just a rewrite. It is a long-term engineering project that affects the build system, release process, testing strategy, and the people who will maintain the code afterward. That’s why the safest migrations are usually the ones that build confidence step by step. Start small, solve the integration problems early, keep the Rust FFI boundary understandable, and make sure the team learns enough Rust to own the new code.
The practical takeaway is simple: C++ to Rust migration is not about replacing every line of code as quickly as possible. It is about moving the right parts of the system to Rust in a way that reduces risk, preserves knowledge, and keeps the product moving forward.