The Memory Safety Reckoning: Part 4 of 4
In the first three blogs of this series, we looked at why memory safety has become a national security priority, why maintaining large C++ codebases becomes more expensive over time, and why rewriting those systems from scratch is rarely realistic. Taken together, these challenges leave organizations with a clear problem. Memory-unsafe code carries real risk and liability. Mitigating that risk accrues cost throughout the system’s lifetime. And replacing it wholesale with one that is secure by design is rarely practical.
So how does an organization effectively modernize — or secure — its existing software at scale? We argue that two complementary approaches make this achievable in practice: incrementally migrating code into a memory-safe language and automating code migration to reduce necessary human involvement. The previous post in this series made the case for incrementality — starting with the most critical components and gradually pushing the migration frontier outward, rather than treating migration as an all-or-nothing rewrite. That still leaves the question of how to carry out the migration work, at each step, without consuming prohibitive amounts of expert time. That is where automation comes in, and it’s the focus of this post.
Automation Changes the Economics of Modernization
Most of the work in migrating a C or C++ codebase to a memory-safe language is, in essence, transcription: taking the logic already expressed in the source code and re-expressing it, line by line and function by function, in the destination language. That work is necessary, but it is rarely where the risk or difficulty lives. The parts of migration that do require careful engineering judgment are narrower and more specific:
- Reworking code to satisfy the constraints of the destination language’s paradigm — for example, restructuring code so it satisfies Rust’s borrow checker, which often requires rethinking data ownership and lifetimes rather than a line-for-line rewrite.
- Remapping programming language idioms and primitives from the source ecosystem to their closest equivalents in the destination language, which rarely correspond one-to-one.
- Improving the overall system’s design and logic wherever necessary — fixing latent bugs, tightening security beyond memory safety, or correcting design flaws that the migration to a memory-safe language won’t address on its own.
In practice, though, the bulk of a developer’s time and attention goes into the straightforward transcription work, simply because there is so much more of it. That leaves less time and focus for the handful of issues above, even though those are the issues most likely to determine whether the migration succeeds.
One specific challenge where human judgment is essential is ensuring migrated and not-yet-migrated code can still work together. It matters because it is what enables incremental migration, and by extension, makes large-scale migration practical at all. Different languages may use different calling conventions and memory layouts, so every point of contact between the two must be deliberately reconciled. Without that, migration could only happen all at once, which is exactly the scenario the previous post in this series argued against.
Automation changes the balance across all of this. It can take on the repetitive, well-understood transcription work at scale, and in doing so, it also helps engineers move faster on the harder problems — surfacing where the destination paradigm’s constraints apply, flagging library and primitive mappings, and identifying the dependency boundaries that need to be reconciled — rather than leaving those buried in thousands of lines of otherwise mechanical code. The result is a dual benefit: migration takes less time overall, and the engineering time that is still required goes to the places where human judgment actually pays off.
Why Rust Makes Modernization Practical
Several general-purpose memory-safe languages exist today, including Rust, Go, Java, and C#. The question for organizations moving off large C++ codebases is which of these options can take on systems-level work without giving up what C++ was chosen for in the first place. In this race, Rust has emerged as the leading answer, combining memory-safety guarantees with the performance and low-level control that systems programming requires.
Rust’s ownership model allows its compiler to catch some of the most dangerous memory-safety problems — like use-after-free errors, dangling references, and some buffer overflows — at compile time, before the code ever runs. In other memory-safe languages, these same problems are typically mitigated at run-time either by consuming additional resources (e.g., delaying memory deallocation) or by aborting execution when a violation is detected. For critical systems, this often necessitates introducing complex recovery strategies to ensure system availability. Catching these errors at compile time means developers fix the underlying issue during development, rather than discovering it after deployment, which meaningfully improves the resilience and survivability of the resulting system.
Most general-purpose memory-safe languages achieve their safety guarantees by relying on a garbage collector to manage memory automatically. That comes at a cost: garbage collection adds runtime overhead that slows execution, and introduces non-determinism, since developers cannot always predict exactly when the collector will run. For latency-sensitive or resource-constrained systems — exactly the kind of applications organizations tend to build in C++ — that unpredictability can be disqualifying. Rust’s ownership model delivers memory safety without requiring a garbage collector at all, which is what allows it to remain viable for performance-sensitive systems work.
Rust’s ownership model imposes certain constraints on what can be expressed in the language. To make the language more broadly applicable, Rust includes an “unsafe block” mechanism, which allows developers to step outside the compiler’s normal safety checks when necessary — for example, to interact directly with hardware, implement low-level data structures, or squeeze extra performance for a hot execution path. Because the mechanism sidesteps the compiler’s protections, code inside an unsafe block deserves extra human scrutiny to ensure that the overall system security is not compromised. By streamlining the mundane logic transcription, the automation we advocated for in the previous section frees up the resources and expertise to judge whether the unsafe-block mechanism is used appropriately by the migrated system.
Where AI Helps and Determinism Matters
Generative AI has advanced rapidly over the past few years, and large language models are already part of how many developers write, understand, and maintain software. It’s natural to ask whether the same tools could drive the kind of migration this series is describing.
LLMs can help developers understand unfamiliar code, suggest changes, and translate between programming languages. Those capabilities are genuinely valuable during modernization, particularly when developers are learning Rust or working through unfamiliar portions of a legacy system.
On the other hand, LLMs tend to be non-deterministic: asking a model the same question twice, or asking it to translate the same code twice, can produce two different answers, even when nothing about the request has changed. Research has shown that this variability persists in practice, even when specific measures — like setting the model’s temperature to zero — are used to try to reduce randomness.
For many development tasks, that variability is manageable; a developer reviews whatever comes back and adjusts as needed. For mission-critical and high-assurance software, however, organizations also need to be able to reproduce, inspect, test, and verify how software was transformed. A deterministic migration process produces the same result every time it’s given the same input, which gives engineers a stable output they can review and test consistently. That consistency matters beyond any single component, too: because deterministic translation applies the same rules every time, migrated pieces of a system are more likely to agree with each other at the points where they interact — which is exactly what incremental, piece-by-piece migration depends on.
This does not make AI and deterministic automation competing approaches. AI can assist developers with understanding code, exploring solutions, and other development tasks. Deterministic automation addresses a different requirement: making the code migration predictable and repeatable when assurance and traceability matter.
Putting Deterministic Modernization Into Practice
Automation, determinism, and traceability are the principles behind GrammaTech’s C++ to Rust Assisted Migration (CRAM) technology. CRAM automates the bulk of a C++-to-Rust migration — typically 80 to 90% of a codebase — giving teams a fast, reliable way to bootstrap the migration rather than starting from a blank page. The remaining portion still has to be completed manually: some C++ features aren’t yet supported by CRAM, and other portions — like low-level memory management — are exactly the kind of tricky, judgment-heavy code that, as discussed earlier in this series, benefits from focused human expertise. GrammaTech continues to extend CRAM’s coverage over time, steadily shifting more of what can be handled automatically out of developers’ hands and keeping their attention on the parts of migration where it matters most.
One of CRAM’s key capabilities is deterministic transformation: the same C++ input produces the same output every time. This gives engineers a predictable result that can be reproduced, tested, and reviewed. Because module and component interfaces are uniformly translated wherever they appear, CRAM can be applied to individual modules independently – the migrated modules can be integrated to form a complete system with low effort. Working at a smaller module level makes migration results easier for developers to inspect, understand, and validate. It also enables migration to be parallelized across large systems and simplifies collaboration across large teams – developers can work independently on different parts of the same system.
CRAM preserves the structure of the original codebase in its output, organizing translated Rust code the same way the source C++ was organized. This makes it straightforward to trace how any specific part of the original system was translated, without needing to reverse-engineer where a given piece of logic ended up. CRAM also produces an interactive, HTML-based report that visualizes translation outcome and collects key translation metrics. Developers can use the report to inspect and cross-check original and migrated code side by side, and to quickly locate the parts of the codebase where translation is only partial and still needs attention.
A Practical Path Forward
We have been building a case for memory-safe modernization throughout this series. The first post explained why memory safety has become a national security priority. The second showed how legacy C++ can create ongoing security and engineering costs. The third explained why simply rewriting large systems from scratch is often too expensive, slow, and risky. Together, they point toward a different approach — and government guidance increasingly agrees: organizations and software manufacturers are being encouraged to adopt memory-safe languages and publish roadmaps for addressing memory-unsafe code.
Memory-safe modernization does not require organizations to choose between maintaining legacy C++ forever and rewriting critical systems from scratch. This post has argued for a third path. Migration works best as an incremental effort: start with the components where memory safety matters most, and gradually extend that effort outward as resources and confidence allow, rather than treating the system as an all-or-nothing rewrite.
Automation is what makes that effort sustainable at scale — it handles the repetitive, well-understood work of translation, freeing engineers to focus on the judgment calls automation can’t reliably make on its own: deciding how to improve system’s architecture, judging when it is appropriate to use Rust’s unsafe mechanism, and reconciling the interfaces where migrated and not-yet-migrated code meet.
Tools like Rust and technology like CRAM make that path concrete: Rust delivers memory safety without giving up the performance and control that led teams to choose C++ in the first place, and CRAM’s deterministic, traceable automation lets that migration happen incrementally, module by module, rather than as a single, high-risk rewrite.
This is not push-button modernization. It’s a way to make expert-led modernization scalable — turning memory-safe modernization from an overwhelming rewrite into a practical, manageable engineering process for organizations responsible for large C++ codebases.
