How do you rewrite C/C++ projects to Rust?
In a Nutshell
Luca Palmieri argues that Rust migrations succeed when companies treat them as incremental engineering projects rather than big-bang rewrites: start with self-contained C modules that only need to call Rust, integrate build systems and bindings early, and expand the Rust surface while shipping continuously. The biggest risks are underestimating timeline and training, and letting the two codebases drift out of sync; the biggest gains come from using AI to accelerate testing and guard-rail creation while developers stay in control of design and correctness.
These notes were generated by AI and may contain inaccuracies.
Luca Palmieri, book author, speaker, principal engineering consultant, and main matter, joined the Rust Stroa live stream series to discuss migrating C/C++ projects to Rust.
Luca's first programming experience occurred at age 13-14 when he used PHP with the Symfony framework and the LAMP stack to build websites and small blogging platforms. After a significant gap, he studied programming again in university while pursuing mathematics, focusing primarily on linear algebra with C, Python, and MATLAB. His professional programming career began when he left university to work as a machine learning engineer and data scientist, where he was primarily using Python.
While working as an ML engineer, Luca needed to write custom models beyond what could be assembled from NumPy, SciPy, and scikit-learn calls. Since Python wasn't fast enough for this work, native extensions using Cython or C++ were required. His CTO recommended Rust as a fast language that could handle these requirements. Luca downloaded the Rust book in PDF format and read it during a flight, finding it interesting enough to pursue further.
Luca's first PRs were contributed to ndarray, the main NumPy alternative in the Rust ecosystem. He continued using Rust as a side tool for a couple of years until landing at TrueLayer, where he transitioned career-wise to become a backend engineer using Rust primarily for backend work, which led to writing his book.
Luca provided several pieces of advice for teaching Rust, starting with finding an ecosystem gap by surveying existing materials to determine if there's something new to contribute or a different approach to take. He emphasized that creating learning material is usually a significant effort that cannot be completed in a few evenings.
The second piece of advice is to use the exercise of writing learning materials as a way to become an expert on a topic, noting the significant difference between being able to do things and being able to explain concepts effectively to other people. Writing also provides visibility for expertise and reaches people through unexpected channels, potentially leading to new connections, jobs, or clients.
Companies are now actively migrating to Rust because the language has moved from being considered experimental to becoming "boring software" - normal, production-ready, and widely used. This shift occurred because major companies have successfully used Rust for mission-critical software over the past five to six years, removing the risk of being the "canary in the coal mine."
Current migration drivers include production crashes that have been difficult to eliminate, recurring security issues, and codebases that nobody understands. Companies are also refreshing infrastructure and taking the opportunity to improve underlying systems. Many companies spent the past five years running internal experiments in less critical areas to probe complexity before committing to larger migrations.
Rust expertise is diffusing through the industry as more people gain production experience and share knowledge with colleagues. The previous hiring concerns about Rust talent are losing substance as more engineers gain experience. Generative AI also helps reduce the scary ramp-up period for new developers.
Business-driven migrations need to make economic sense through improved maintainability, allowing teams to ship faster or at the same speed with lower defect rates. Projects that benefit most include those that are performance-sensitive, involve multi-threading or concurrency, serve as interesting attack targets for security, or run at significant scale where utilization improvements can reduce hardware costs.
The choice between full rewrite and incremental migration depends on software type, distribution method, and size. A full rewrite can be the fastest approach when the software size is tractable, the API is well-defined, and there's an exhaustive blackbox test suite. Full rewrites work best for services deployed in controlled environments where shadow traffic routing and gradual customer migration can be used to build confidence.
Packaged software like CLIs, desktop applications, and databases present more complex migration challenges because usage patterns are less predictable and test suites cannot cover all configurations. The key differentiator is what mechanisms exist to build confidence in the new implementation.
Codebase velocity significantly impacts strategy - actively developed codebases create moving targets that require constant synchronization, while stable codebases with clear boundaries are ideal for full rewrites. Feature freeze limitations make long-term rewrites impractical for business reasons.
Successful incremental migrations show steady, ideally accelerating progress where modules are integrated and switched on for users progressively. Key indicators include consistent shipping with minimal rework, transitioning from "C codebase with Rust" to "Rust codebase with C," and improving ease of adding more Rust as the migration progresses.
Two main approaches exist for starting migrations: vertical slices and isolating modules. Isolating modules is easiest, involving finding self-contained modules with no dependencies in the C codebase module graph. This allows Rust code to only need to be callable from C rather than invoking C code.
The first priority is figuring out build system integration, as C/C++ codebases have heterogeneous build systems. Complex challenges may include introducing link time optimization across language boundaries and running tests with memory sanitizers across language boundaries. Starting with low-complexity modules allows teams to figure out these integration issues before tackling more complex work.
The goal is to ship a line of Rust that builds correctly, links correctly within the larger system, and works across all target platforms before attempting more substantial software work. This approach involves eating the dependency graph from the leaves, gradually building larger islands of Rust code.
The first approach involves starting from the outside and working inward, creating a C boundary around a Rust core that gradually expands. This method is easiest in some respects but also most challenging because it may feel like you're not adding value for a long time. The work often begins with boundary modules that haven't been touched in a long time, which may not seem immediately useful, but serves as an enabler to handle complex components without the added complexity of managing C underneath.
The polar opposite approach involves driving a specific user flow or feature through Rust by cutting a slice through the stack. This can be done but is very complex because it most likely means that throughout your Rust code you will have a ton of C code that's constantly being called or calling your code, with lots of raw pointers flying around. This results in Rust code that is very C-style Rust, potentially not providing safety benefits from the borrow checker perspective for quite a while because most of the action happens outside the domain that Rust can actually verify.
A third approach involves mechanically translating C code to Rust using tools like C2Rust, then refactoring repeatedly until the code feels more like Rust written in Rust rather than C written in Rust. This approach is not preferred but is worth noting as a potential option.
The biggest struggles when crossing the boundary between Rust and C/C++ relate to the different philosophy between the two languages. Rust developers have a very strict approach toward safety and undefined behavior, while C and C++ developers might have a slightly more cavalier approach. This creates a culture clash where C developers might say something works, but it's not the way it should be done according to Rust standards.
When bridging languages, certain things that may be obvious within a single language environment become non-obvious. A key consideration is whether the two languages are using the same allocator. If they are not, weird things may happen, though sometimes it makes sense not to use the same allocator, perhaps not in production but in tests to check for memory violations. The philosophy should be "whoever allocates it frees it," and this needs to be thought about during development.
When doing a lot of C/C++ to Rust interoperability, developers deal with many raw pointers, which means dealing with quite a lot of unsafe code. This requires brushing up on knowledge about what's sound versus what might be sound but isn't fully known yet. Rust makes strong assumptions that having a reference to something gives strong guarantees around validity, readability, and correct initialization, which differs significantly from the guarantees provided by holding a pointer in C.
Sometimes a lot of work in Rust involves making the behavior that C code already has visible and correct in the eyes of the compiler and MIR, which can feel like busy work but is actually important because the primary selling point of moving to Rust is having all that static analysis.
The interchangeability between Rust and C++ is very different from the interchangeability between Java and Kotlin. Kotlin was explicitly designed to be used as a Java superset or subset and to work out of the box when sprinkled into mostly Java codebases. Rust has features and tooling to make interoperability possible, but there's a chasm between the way C/C++ code is structured and the way Rust code is structured in most projects, requiring a lot of bridging.
Once memory is shared across languages, the safety net is no longer present. When there are many pointers coming from C or being stored, developers must do more manual checking of assumptions toward the liveness and aliasing of each pointer. The codebase becomes more like a secret base because some invariants have to be upheld by the programmer, which is where unsafe comes into place.
The most severe migration mistakes include badly planned migrations where assumptions about how the migration will proceed don't verify, leading to abandonment. From-scratch migrations often end this way after spending years writing code while the other system continues to move, until management loses trust. Incremental migrations can fail when teams underestimate the time required and give up midway, leaving them in a worse state with two languages that need to interoperate.
Another common mistake is underestimating the importance of training developers throughout the migration. While outside expertise can help, the developers who will maintain the codebase going forward must be heavily involved, otherwise institutional knowledge is lost.
When working with incremental rewrites, Rust and C code need to talk to each other, requiring bindings to generate C header definitions and Rust code that matches C calling conventions and type layouts. Historically, Cbindgen and bindgen have been used together for this purpose.
Cbindgen is an older project that takes a parsing-based approach using syn to extract the abstract syntax tree from Rust source code. However, Rust's data model and packaging is more complex with macros that generate code, and code spread across multiple packages, making source parsing complex and time-consuming.
C2Edgen was developed as an alternative that uses rustdoc JSON instead of source parsing. This JSON representation comes from the compiler's analysis pipeline, making it obviously correct. It sees through macros because they've been expanded, and is the same data source that powers cargo doc checks. This approach has nullified problems in generating C headers, requiring less configuration and allowing macros to work out of the box.
The trade-off is that C2Edgen is slower than Cbindgen because it compiles the code, and if the crate graph doesn't compile, C headers cannot be generated. However, for projects with many Rust crates and C files requiring partitioned headers and extensive macro usage, the switch has been worthwhile.
AI can be used to increase both velocity and quality of software and has been used heavily in migration work. However, AI is not magic, and the fundamental requirement remains to understand the software being shipped. Rewriting from C to Rust is never just translating code, as that would simply produce Rust code full of unsafe and pointers without meaningful benefits.
AI can accelerate the migration process but developers must remain in the driver's seat, nudging it in the right direction and picking the migration strategy. AI is particularly helpful for implementing additional guard rails like writing tests, prototyping different solutions to the same problem in parallel, and accelerating the overall process.
The key is feeding AI into a workflow that builds trust into the new code, as migration is fundamentally about building trust. Testing suites alone are not exhaustive, and customers and users need to trust the new software, which requires more signal than just passing tests.
AI works very nicely with Rust in pure Rust codebases because the borrow checker and type system create internal guard rails that catch mistakes at compile time. However, in mixed codebases with unsafe code, especially subtle unsafe code, more care is required because issues are going to slip through the cracks almost inevitably. The effectiveness of AI depends on what an organization is optimizing for, and users need to be intentional about how AI fits into the wider workflow.
I would probably agree that something that was too expensive two years ago is probably now... in the realm where it's I can think of actually you know funding this because it's not as expensive or perhaps there's compressed timelines.
AI has moved migration projects from "way too expensive, won't even think about it" to being within funding consideration due to compressed timelines that are more palatable to management. How much of a force for good AI will be will become apparent in the next few years as these migrations actually come online. The outcome depends entirely on the engineering that goes around the usage of these agents.
Teams must learn Rust first, as it will be the technology of the project going forward. Pick a preferred learning path such as the Rust Book, 100 Exercises to Learn Rust, or Rustlings. Once comfortable with Rust, look into specific migration material.
Main Matter wrote a C to Rust migration book, releasing the first chapter the week prior to this discussion, with plans to expand it over coming months. This addresses the lack of good resources in this space, as people often end up reading the Rustonomicon (whose maintenance status is questionable), cross-checking the Rust reference, and looking at how the standard library handles things.
- The C2Rust migration material written by Jonas
- Open source migrations on GitHub including the speaker's commit projects
- The Rust for Linux project (noting it has 200 more layers of complexity)
- GitHub search as an underrated tool to find examples of specific implementations, such as cross-language LTO flag usage
The experience of migrating is similar to what occurred with Rust backend development five to six years ago, but the path has become much smoother with more libraries, tools, and learning resources available.
In Italy we like to say that you're asking the barman if the cocktails are good.
It depends on the organization. Large companies may have Rust expertise in other parts of the business that can be tapped into. For teams with only C and C++ experience who have never looked at Rust, it makes sense to get training either through public trainings at conferences or other events to avoid obvious mistakes. Engaging consultants or experts who have done many projects can streamline execution, though it comes at a cost that depends on perceived business value and investment willingness.
Yes. But I think there's a more general pattern, right? like it's a bit strange but it is a skill we got to develop in the AI world which is there's a difference between a topic like there's a difference between being in a mental state of I have a problem and I want a quick solution and being in a mental state of I will need to do a lot of work here I need to build up skills to be able to be effective.
For a team with no prior Rust knowledge, writing Rust themselves rather than leaning heavily on AI allows learning from error messages. The distinction matters: redesigning a small website with AI is fine when you won't touch it again, but migrating a million-line codebase with unsafe code requires skill development.
Deliberate practice is essential. When working through designs with AI and consistently seeing patterns like trait usage or unsafe manipulations, teams need to understand from first principles why implementations are necessary, whether they are correct, and how subtle mistakes would be detected. AI agents will convince users they are right when they are often not, and without skills to steer them, problems can emerge hundreds of thousands of lines into a project.
The journey from using AI assistance at zero skill level to taking over and driving the process applies to everything technical, from CMake to linkers. At a certain point, understanding how tools actually work becomes necessary rather than just getting code right through trial and error.
CLion by JetBrains offers CMake debugger functionality for understanding CMake builds, and also has Rust support, making it potentially valuable for C++ and Rust codebases.
We're still talking about COBOL migrations, right?
Migrations will continue for the next decade or two at minimum, and possibly forever. The sheer amount of mission-critical C and C++ software means people wanting to work with C and C++ will find work until retirement. New C and C++ software continues being written daily.
However, the nature of these discussions will likely change as processes become more engineered and predictable with better tools. The frontier will move, making ports possible at lower price points and resulting in safer software across the board.
Most new projects in certain spaces now default to Rust. In the database space over the past five years, only one or two startups launched not using Rust. The fact that exceptions need to be specifically named indicates the default has shifted. Software that would have started in C or C++ five years ago today starts in Rust.
Migrations will remain necessary for legacy software, but they will increasingly resemble COBOL migrations or Java 8 to newer Java version migrations—legacy work while new software is written in different languages.
EuRust (European Rust) changes cities yearly. The 2024 conference runs October 14-17 in Barcelona. The schedule is mostly online with workshops announced. The event includes talks, academies, unconference sessions, and other activities. It attracts practitioners, companies using Rust, and open source ecosystem contributors, providing diverse exposure to the Rust ecosystem.
JetBrains will have a booth at EuRust with activities planned, including participation by their new developer advocate Arun ("Arhun Parakis"), known as the Ratatouille guy.
Keep JetBrains in your library
Save the videos and channels worth coming back to, and find them again in one place.



