Rust memory safety explained

By Serdar Yegulalp

Over the past decade, Rust has emerged as a language of choice for people who want to write fast, machine-native software that also has strong guarantees for memory safety.

Other languages, like C, may run fast and close to the metal, but they lack the language features to ensure program memory is allocated and disposed of properly. As noted recently by the White House Office of the National Cyber Director, these shortcomings enable software insecurities and exploits with costly real-world consequences. Languages like Rust, which put memory safety first, are getting more attention.

How does Rust guarantee memory safety in ways that other languages don't? Let's find out.

Rust memory safety: A native language feature

The first thing to understand about Rust's memory safety features is that they're not provided by way of a library or external analysis tools, either of which would be optional. Rust's memory safety features are baked right into the language. They are not only mandatory but enforced before the code ever runs.

In Rust, behaviors that are not memory-safe are treated not as runtime errors but as compiler errors. Whole classes of problems, like use-after-free errors, are syntactically wrong in Rust. Such invalid code never compiles, and it never makes it into production at all. In many other languages, including C or C++, memory-safety errors are too often only discovered at runtime.

This doesn't mean that code written in Rust is entirely bulletproof or infallible. Some runtime issues, like race conditions, are still the developer's responsibility. But Rust does take many common opportunities for software exploits off the table.

Memory-managed languages, like C#, Java, or Python, relieve the developer almost entirely of doing any manual memory management. Devs can focus on writing code and getting jobs done. But that convenience comes at some other cost, typically speed or the need for a larger runtime. Rust binaries can be highly compact, run at machine-native speed by default, and remain memory-safe.

Rust variables: Immutable by default

One of the first things newbie Rust developers learn is that all variables are immutable by default—meaning they can't be reassigned or modified. They have to be specifically declared as mutable to be changed.

This might seem trivial, but it has the net effect of forcing the developer to be fully conscious of what values need to be mutable in a program, and when. The resulting code is easier to reason about because it tells you what can change and where.

Immutable-by-default is distinct from the concept of a constant. An immutable variable can be computed and then stored as immutable at runtime—that is, it can be computed, stored, and then not changed. A constant, though, must be computable at compile time, before the program ever runs. Many kinds of values—user input, for example—cannot be stored as constants this way.

C++ assumes the opposite of Rust: by default, everything is mutable. You must use the const keyword to declare things immutable. You could adopt a C++ coding style of using const by default, but that would only cover the code you write. Rust ensures all programs written in the language, now and going forward, assume immutability by default.

Ownership, borrowing, and references in Rust

Every value in Rust has an "owner," meaning that only one thing at a time, at any given point in the code, can have full read/write control over a value. Ownership can be given away or "borrowed" temporarily, but this behavior is strictly tracked by Rust's compiler. Any code that violates the ownership rules for a given object simply doesn't compile.

Contrast this approach with what we see in other languages. In C, there's no ownership: anything can be accessed by any other thing at any time. All responsibility for how things are modified rests with the programmer. In managed languages like Python, Java, or C#, ownership rules don't exist, but only because they don't need to. Object access, and thus memory safety, is handled by the runtime. Again, this comes at the cost of speed or the size and presence of a runtime.

Lifetimes in Rust

References to values in Rust don't just have owners, but lifetimes—meaning a scope for which a given reference is valid. In most Rust code, lifetimes can be left implicit, since the compiler traces them. But lifetimes can also be explicitly annotated for more complex use cases. Regardless, attempting to access or modify something outside of its lifetime, or after it's "gone out of scope," results in a compiler error. This again prevents whole classes of dangerous bugs from making it into production with Rust code.

Use-after-free errors or "dangling pointers" emerge when you try to access something that has in theory been deallocated or gone out of scope. These are depressingly common in C and C++. C has no official enforcement at compile time for object lifetimes. C++ has concepts like "smart pointers" to avoid this, but they are not implemented by default; you have to opt-in to using them. Language safety becomes a matter of an individual coding style or an institutional requirement, not something the language ensures altogether.

With managed languages like Java, C#, or Python, memory management is the responsibility of the language's runtime. This comes at the cost of requiring a sizable runtime and sometimes reduces execution speed. Rust enforces lifetime rules before the code ever runs.

Rust's memory safety has costs

Rust's memory safety has costs, too. The first and largest is the need to learn and use the language itself.

Switching to a new language is never easy, and one of the common criticisms of Rust is its initial learning curve, even for experienced programmers. It takes time and work to grasp Rust's memory management model. Rust's learning curve is a constant point of discussion even among supporters of the language.

C, C++, and all the rest have a large and entrenched user base, which is a frequent argument in their favor. They also have plenty of existing code that can be leveraged, including libraries and complete applications. It's not hard to understand why developers choose to use C languages: so much tooling and other resources exist around them.

That said, in the decade or so that Rust has been in existence, it's gained tooling, documentation, and a user community that makes it easier to get up to speed. And the collection of third-party "crates," or Rust libraries, is already expansive and growing daily. Using Rust may require a period of retraining and retooling but users will rarely lack the resources or library support for a given task.

Applying Rust's lessons to other languages

Rust's growth has spurred conversations about transforming existing languages that lack memory safety to adopt Rust-like memory protection features.

There are some ambitious ideas, but they're difficult to implement at best. For one, they'd almost certainly come at the cost of backward compatibility. Rust's behaviors are difficult to introduce into languages where they're not in use without forcing a hard division between existing legacy code and new code with new behaviors.

None of this has stopped people from trying. Various projects have attempted to create extensions to C or C++ with rules about memory safety and ownership. The Carbon and Cppfront projects explore ideas in this vein. Carbon is an entirely new language with migration tools for existing C++ code, and Cppfront proposes an alternative syntax to C++ as a way to write it more safely and conveniently. But both of these projects remain prototypical; Cppfront only released its first feature-complete version in March 2024.

What gives Rust its distinct place in the programming world is that its most powerful and notable features—memory safety and the compile-time behaviors that guarantee it—are indivisibly part of the language; they were built in and not added after the fact. Accessing these features may demand more of the developer initially, but the dividends pay off later.

© Info World