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 C to Rust Migration book, and Vitaly 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.
Facts Only
* C and C++ to Rust migrations are being explored as a way to improve memory safety and reduce maintenance costs in performance-critical systems.
* A successful migration strategy involves incremental migration, starting with isolated modules and integrating them into existing build processes.
* Migration candidates include performance-sensitive codebases, concurrent systems, security-critical components, and high-scale deployments where efficiency savings translate to infrastructure reductions.
* The option between a full rewrite and incremental migration depends on the deployment model and testing infrastructure.
* Full rewrites are considered when the codebase is small, has exhaustive test suites, a well-defined API surface, and controlled deployment environments.
* Incremental migration is preferred for large or active systems, allowing gradual integration and confidence building through successive releases.
* A practical starting point involves examining module graphs to find isolated modules with no dependencies on the rest of the system.
* Integration problems involve fitting Rust into existing build systems, managing linking between languages, and ensuring cross-language memory sanitization.
* Crossing language boundaries via raw pointers requires manual tracking of ownership and cleanup rules for safety.
Executive Summary
A safe migration strategy for C/C++ systems to Rust is incremental rather than a full rewrite, starting with isolated modules and gradually expanding confidence. This approach involves integrating new Rust code into the existing build and release processes before attempting larger shifts. Teams should target migrations in areas where maintainability is already a concern, such as performance-sensitive codebases, concurrent systems, or security-critical components, as these areas offer the most justification for migration based on reduced rework, defect rates, or operational savings.
The process of migrating requires managing the boundary between the two languages, especially concerning Foreign Function Interface (FFI). When crossing boundaries via raw pointers, developers must manually enforce memory safety rules, which necessitates treating unsafe code as an explicit design surface to ensure static analysis tools can assist. The method for starting involves isolating small modules with minimal dependencies to resolve integration problems first, or focusing on vertical slices of functionality that demonstrate immediate business value while managing increased complexity.
Full Take
The shift in discourse from viewing migration as a binary choice (rewrite vs. incremental) to understanding it as a phased engineering project reveals a pattern focused on managing risk and institutional knowledge alongside technical modernization. The core tension lies between the perceived appeal of a clean slate offered by a rewrite and the practical necessity of building confidence through observable, integrated progress inherent in incremental changes.
The pattern of suggesting incremental migration is strongly supported by the need to preserve knowledge when dealing with complex legacy systems; replacing code risks discarding crucial historical context. This implies that successful system modernization is less about syntactic replacement and more about layering new safety guarantees onto existing operational structures. The difficulty introduced by Rust's FFI boundary—where compile-time safety must be mapped onto runtime behavior involving raw pointers—highlights a systemic challenge: making runtime safety visible to the compiler requires defining the integration layer as rigorously as the application logic itself.
The dichotomy between bottom-up development (starting from module leaves) and vertical slicing (driving user flows) presents different trade-offs regarding immediate value versus structural integrity. Bottom-up migration prioritizes solving foundational integration problems, which can lead to protracted work on seemingly irrelevant modules; vertical slicing prioritizes demonstrable business impact but risks creating "C-style Rust" where the safety benefits of the borrow checker are suppressed by constant external pointer manipulation. The implication is that true success depends not just on the chosen technical strategy, but on establishing robust governance over what constitutes a successful boundary and how institutional knowledge is successfully transferred across the migration phases.
Sentinel — Human
The text reads like a well-researched synthesis of industry consultation, offering pragmatic migration advice based on observed patterns rather than pure, synthesized assertion.
