C/C++ to Rust: 7-Gate Incremental Migration Plan (2026)

A release checklist for C/C++ to Rust migration: baseline behavior, define FFI contracts, compare outputs, and roll back each module safely.

Max Wells

My name is Max and I'm the founder of Rustify.

I've been writing Rust professionally for 2 years and I've helped dozens of engineers make the switch to Rust professionally.

If you want to go further, here's how I can help:

  • Fullstack Bootcamp: structured program with real projects, async support, and private community access.
  • Blockchain Bootcamp: structured program with real projects, async support, and private community access.
  • 1:1 Mentorship: personalized sessions to get you hired faster, with projects tailored to you and mock interviews.

And if you want more Rust content, I post regularly on my YouTube channel:


Migrate a C/C++ codebase to Rust by passing one bounded module through seven release gates, while the existing system keeps shipping. The migration unit is a behavior boundary, not a file count: preserve the old implementation as a reference, compare both versions, and expand only when the new slice is safe to operate.

This guide is for technical leads and engineers responsible for a maintained C or C++ product. It covers migration execution: choosing a slice, keeping both languages in the build, proving behavior, and retiring replaced code. It does not teach Rust concepts to an individual learner. For that, use the C++ developer transition guide. For the prior decision about whether a rewrite is justified, read Should You Rewrite It in Rust?.

TL;DR: A safe C/C++ to Rust migration moves one module through seven gates, not one giant rewrite.

  • Select a bounded module with a measurable reason to change.
  • Record behavior and runtime baselines before porting.
  • Freeze the FFI contract and build both languages in every supported target.
  • Compare old and new outputs on the same corpus before rollout.
  • Keep rollback and a named owner until the Rust module proves safe to operate.

What Does Incremental C/C++ to Rust Migration Mean?

Incremental migration replaces one coherent behavior boundary at a time, while the rest of the application continues to use its existing C or C++ implementation. The codebase can remain mixed-language for a long time. Each Rust module should have a clear contract, a testable result, and an owner who can operate it.

Existing C/C++ application
  ├── unchanged module A
  ├── stable boundary ──► Rust replacement for module B
  └── unchanged module C
              │
              └── compare, release, observe, then choose the next slice

This model avoids making the whole project wait for a complete rewrite. It also creates a real cost: two languages, toolchains, debugging paths, and review skills coexist during the migration. Plan for that operating period instead of treating it as temporary clutter that will disappear after one release.

Android documents this tradeoff directly. Rust is used for native platform code, but Android says rewriting all existing memory-unsafe code, which it describes as over 70% of platform code, in Rust is not feasible. The platform treats Rust as complementary to other memory-safety tools. That is Android's situation, not a universal percentage or a rule for every codebase. (Android memory-safety documentation)

Should You Rewrite Everything or Migrate Modules?

Prefer module-by-module migration when the product must keep shipping, behavior is valuable, or the codebase is too connected to replace safely in one release. A full rewrite can fit a small, isolated library with a clear specification and a low cost of behavioral drift. It is a poor default for a long-lived system whose tests and edge cases encode years of production learning.

ApproachBetter fitMain cost
Full rewriteSmall, isolated component; behavior can be specified; replacement can be validated before cutoverLong period before users get value; forgotten edge cases; difficult rollback
Module replacementLarge maintained product; clear module seams; frequent releasesMixed-language build and ownership; boundary maintenance
Keep C/C++ and add Rust only for new modulesExisting code works and Rust is useful for new native componentsLegacy risk remains in old code; team supports both stacks

The Rust rewrite decision framework covers whether a rewrite is worth its cost. This guide starts after that decision: how to move production code without making one release carry the risk of the whole project.

How Do You Choose the First Module?

Choose a module with a clear contract, manageable dependencies, observable behavior, and enough value to justify the integration work. Do not choose only by line count. A small module with hidden global state can be harder to migrate than a larger leaf component with a stable API.

Score candidate modules against these questions:

  1. Can you state its input, output, and failure behavior without describing its internal implementation?
  2. Can you build and test it independently, or isolate its build dependencies?
  3. Can the old and new versions receive the same inputs?
  4. Can a caller switch between implementations without a broad redesign?
  5. Is there a real reason to change this component, such as safety exposure, maintenance cost, or a measured bottleneck?
  6. Can the team support the mixed-language version after the first author moves on?

Mainmatter's published database modernization case study describes decomposing a mature C query engine, then prioritizing less-entangled leaf modules. The case study also reports adding tests, linting, sanitizers, coverage, training, and pair reviews. It says modules were released and the wider migration was ongoing, but gives no quantitative performance or security outcomes. Treat it as one team's reported method, not a benchmark or universal proof that leaf-first migration always wins.

What Must You Measure Before Porting Code?

Record the old implementation's observable behavior before changing it, or you will not know whether the Rust version preserved the contract. Capture a representative input corpus, outputs, error cases, performance under a named workload, and the build configurations that users depend on.

BaselineWhat to recordWhy it helps
Functional behaviorInputs, outputs, errors, boundary cases, known regressionsGives both implementations the same oracle
RuntimeWorkload, latency distribution, throughput, memory use, CPUSeparates measured improvement from language assumptions
ReliabilityCrashes, sanitizer findings, incident classesShows whether the migration addresses an actual risk
DeliveryBuild time, CI duration, release steps, review effortExposes costs introduced by the mixed-language period
CompatibilityPlatforms, compiler flags, ABI versions, downstream callersPrevents a passing local build from hiding a broken target

Keep each measurement tied to a reproducible command, fixture, or dashboard. Do not compare a warmed-up Rust benchmark with an unoptimized C++ build, or a microbenchmark with production traffic. The measure should answer the migration's stated reason. A memory-safety migration needs safety evidence; a latency migration needs workload-specific latency evidence.

What Are the Seven Release Gates for a Rust Migration Slice?

Do not release a migrated module until it passes seven gates with written evidence. The gates turn “we ported this module” into a repeatable review another engineer can verify.

GateEvidence required to pass
1. ScopeNamed module, migration reason, owner, dependencies, and explicit out-of-scope behavior
2. BaselineReproducible build, fixture corpus, expected outputs and errors, target platforms, and relevant runtime measurements
3. ContractReviewed interface covering layout, ownership, nullability, errors, callbacks, threading, and compatibility
4. BuildRust and C/C++ build together in CI for supported targets, flags, and release configurations
5. ParityOld and new implementations receive the same fixtures; every difference is explained and accepted or fixed
6. RolloutReversible switch, named operational metrics, alert owner, and rollback trigger are documented
7. DecisionAfter a real release, record keep, expand, or stop; remove the old path only when callers and rollback no longer need it

The first slice should test the entire migration system: build integration, FFI review, parity tests, deployment, and rollback. Do not combine unrelated refactors with that first slice. Keep the existing implementation available while the team learns where the written contract misses real behavior.

Use this short record for each candidate:

Module and owner:
Why migrate this module:
Inputs, outputs, errors, and callers:
Supported targets and build flags:
Baseline fixture and runtime measure:
FFI ownership and safety invariants:
Parity evidence:
Rollout and rollback triggers:
Decision after release: keep | expand | stop

The release unit is a behavior boundary, not a file count. The stopping point is when the stated goal is met and remaining modules do not justify their migration cost. “All source files are Rust” is not a release criterion.

Which Tools Fit Each C/C++ to Rust Boundary?

Pick tools by direction and boundary shape; no one tool translates a whole C++ product into idiomatic Rust. Bindings expose declarations, a bridge connects two implementations, and a transpiler mechanically converts some source. Those are different jobs.

ToolDirectionUse it forLimit to remember
bindgenC/C++ headers → Rust declarationsCalling a native library from RustC++ support is partial; generated declarations still need a safe wrapper and review.
cxxRust ↔ compatible C++ APIsA designed bridge with checked shared types and generated glueStay within the types and features the bridge supports.
cbindgenRust C API → C/C++ headersExposing a Rust library through a C-compatible APIIt generates headers; it does not design ownership or error semantics for you.
cheadergenRust pub extern "C" API → C/C++ headersGenerating headers from a Rust C API; a current alternative to evaluate alongside cbindgenVerify toolchain compatibility and output for your supported targets.
C2RustC99 source → Rust sourceBootstrapping a mechanical C translationOutput mirrors C and is unsafe/non-idiomatic. It is not a general C++ translator.

bindgen and cbindgen work in opposite directions. cxx describes a Rust/C++ bridge. cheadergen generates headers from an exported Rust C API. C2Rust translates C source. The C to Rust Migration Book teaches the surrounding migration work too: safe mixed-language code, FFI types, validation, error handling, and production checks.

How Do You Design a Boundary That Can Survive the Migration?

Keep the interface small and make memory ownership explicit on both sides. A C ABI is one common seam, but a subprocess, file format, or network protocol can be a better boundary when independent deployment and isolation matter more than call overhead.

At a C ABI boundary, document these invariants for every function:

  • Layout: use ABI-compatible representations where required. Rust's default struct layout is not a promise of C layout; #[repr(C)] exists for that purpose.
  • Ownership: state which side allocates and frees each object. Do not pass a Rust String, Vec<T>, or reference as if it were a stable C type.
  • Pointers: define nullability, buffer length, lifetime, aliasing, and whether the callee may retain the pointer.
  • Errors: map failures to a stable C-compatible result. Keep Rust errors and C++ exceptions inside their own language boundary unless the ABI explicitly supports unwinding.
  • Callbacks: define callback thread, unregister behavior, and what keeps callback state alive.
  • Versioning: decide how a caller detects incompatible API changes before releasing the first boundary.

The Rustonomicon FFI chapter explains ABI, ownership, callbacks, and unwinding. The Rust 2024 Edition Guide requires unsafe extern blocks in Edition 2024 because the author must ensure declarations match the foreign interface. Keep unsafe code at the boundary; validate foreign inputs there, then pass ordinary Rust types to business logic.

How Do You Prove the Rust Version Preserves Behavior?

Run old and new implementations against the same inputs, then inspect mismatches instead of assuming the tests cover the whole contract. A differential harness is especially useful when the old code is the only complete description of years of edge cases.

Use several layers of evidence:

  1. Characterization tests capture known behavior before the port.
  2. Differential tests feed identical inputs to both versions and compare outputs and errors.
  3. Property tests or fuzzing explore combinations that hand-written examples miss.
  4. Sanitizers and static checks find problems in the remaining C/C++ and the integration boundary.
  5. Rust tests and unsafe review check Rust logic and the contracts the compiler cannot verify.
  6. Production-shaped benchmarks check the original performance goal using the same workload and environment.

Testing cannot make undefined behavior into a stable contract. If the C/C++ implementation has undefined behavior, record the intended behavior and fix the defect instead of requiring Rust to reproduce accidental output. Keep successful test evidence separate from the safety argument for each unsafe boundary.

For Rust test workflow details, see the Rust test suite guide. Keep test commands, compiler versions, C/C++ flags, and fixtures with the project so another maintainer can reproduce the comparison.

How Should You Roll Out Each Replacement?

Make the cutover reversible at the same boundary used for comparison. Release one module at a time, observe the metrics tied to the migration goal, and keep the old path until the new path has passed the team's operational checks.

For a library call, a feature or configuration switch may be enough. For a request handler, shadowing or gradual traffic routing may fit. For an offline tool, run both versions over a saved corpus before replacing the release artifact. The deployment strategy depends on the architecture; do not copy a canary plan that the product cannot support.

Write rollback conditions before rollout. Examples include a mismatch in output, a regression in a named latency percentile, higher memory use than the agreed bound, or failures in a supported platform build. The exact threshold belongs to the project baseline, not a generic migration guide.

The Linux kernel offers a useful coexistence example, not a full migration precedent: its official documentation describes Rust support under CONFIG_RUST while identifying C as the kernel's primary language. This shows a large C project can add Rust support without claiming that all C code is being rewritten. (Linux kernel programming-language documentation)

What Does a Real Migration Case Teach?

The strongest useful lesson from published case studies is process, not a universal speedup number. Mainmatter's AnonDB case describes selecting weakly connected C modules, building migration infrastructure, training the team, and shipping some Rust modules while work continues.

Its account is unusually useful because it names steps: decompose the query engine, start with leaf modules, build CI around tests, linting, sanitizers and coverage, then use workshops and pair reviews to transfer ownership. It also states the limit: the customer is anonymized, the migration is ongoing, and the case page publishes no before/after performance or incident figures. Do not repeat it as proof that Rust made this database faster or removed a measured number of vulnerabilities. (Mainmatter case study)

That distinction changes what a good migration proposal asks for. First prove that the slice builds, behaves, and can be supported. Then measure whether it delivered the safety, reliability, or runtime benefit that justified the work.

What Are the Common C/C++ Migration Mistakes?

Most avoidable failures come from unclear contracts, oversized slices, or treating generated code as finished Rust. Watch for these patterns:

  • Porting by file count: files do not reveal coupling. Map calls, shared state, ownership, and build dependencies first.
  • Starting with the most critical subsystem: the first slice should teach the integration workflow without putting the highest-risk behavior on the line.
  • Keeping the FFI surface broad: a large boundary makes two implementations depend on each other's internal types and ownership rules.
  • Trusting generated bindings without review: declarations can compile and still be wrong for the target headers, flags, or platform.
  • Porting undefined behavior as a requirement: tests should preserve intended behavior, not accidental effects of invalid memory access.
  • Removing the old path too early: keep rollback until new code has completed a real release cycle and passed the agreed operational checks.
  • Measuring only runtime speed: include build, CI, review, debugging, and maintenance costs in the decision.

The counterintuitive lesson: the first Rust module should be selected for learning value and safe feedback, not for the strongest possible benchmark headline.

How Do You Know When to Stop Migrating?

Stop when the original goal is met, remaining modules do not justify their migration cost, or the mixed-language system costs more to maintain than it saves. A successful program may leave stable C/C++ modules in place indefinitely.

Review each wave against four questions: Did it solve the original problem? Did the boundary remain maintainable? Can another engineer build, debug, and release it? Is the next module valuable enough to repeat the process? If the answer changes, update the plan rather than treating “all Rust” as the only successful outcome.

If you are the individual engineer implementing a Rust slice and want expert feedback on your code and project decisions, Rustify's 1:1 Rust Mentorship includes code review and project guidance.

Frequently Asked Questions

Can you migrate a C++ codebase to Rust one module at a time?

Yes. Keep a stable interface between the Rust module and the remaining C++ code, then replace one behavior boundary at a time. Tools such as cxx can help with compatible C++ interfaces, while a C ABI can provide a narrower seam. The correct boundary depends on the API and build system.

Should a team rewrite its whole C or C++ application in Rust?

Usually, evaluate a bounded migration before committing to a full rewrite. A module-by-module path lets the team compare behavior and operating cost earlier. A complete rewrite may fit a small, isolated component with a precise specification, but it carries a larger period without incremental production value.

Does C2Rust convert C++ code to Rust?

No. C2Rust targets C99 source and produces Rust code that mirrors the C structure. Its output is unsafe and non-idiomatic, so treat it as a translation starting point that needs review and redesign. It is not a general C++ transpiler.

What is the difference between bindgen and cbindgen?

bindgen generates Rust declarations from C or some C++ headers so Rust can call a native library. cbindgen generates C or C++ headers from a Rust library that exposes a C-compatible API. They handle opposite directions.

What is cheadergen used for?

cheadergen generates C/C++ headers from Rust libraries that expose a pub extern "C" API. It belongs on the Rust-to-C-header side of the boundary, not the C-to-Rust translation side. Check its current version and target support before adopting it.

How do teams check that a Rust port still behaves like the C version?

Capture behavior first, then run both versions against the same fixtures and compare outputs and errors. Add property tests or fuzzing where input space is large. Test supported build targets and review FFI safety separately; passing tests do not prove an unsafe contract is correct.

Does a mixed C and Rust codebase have to be temporary?

No. Android says Rust is complementary to memory-safety tools and that rewriting all existing memory-unsafe platform code is not feasible. The Linux kernel also documents Rust support inside a codebase whose primary language remains C. A mixed codebase can be a deliberate long-term design.

How should a team choose its first Rust migration module?

Pick a component with a clear contract, understood dependencies, testable behavior, and a named reason to change. A small leaf module can be a good candidate, but a stable API and useful tests matter more than line count alone.

Keep Reading

Sources

Hear from Rustify Alumni

Their experience building real Rust projects with 1:1 Rustify mentorship

Sekou Diarra

Backend Engineer · 13 years of experience

Arik Dutta

Technical Lead · Low-code & Python → Rust

Tiago Afonso

Fullstack Developer

Ready to Land a $80k+ Rust Job in the US or Europe?