This document is intended as an introduction to Rust, targeted at engineers with deep exposure to embedded systems C, and little to no experience with C++ and no knowledge of Rust.
This document will:

  • Provide Rust analogues of everything in the embedded C toolkit.
  • Discuss how some of those analogues might be importantly different from C’s.
  • Point out where Rust’s memory and execution models materially differ from C.
  • Introduce Rust-specific features that are either key to using Rust at all or otherwise extremely useful (references, lifetimes, generics, traits).

While this document takes a casual approach to Rust, language-lawyer-ey notes are included in footnotes.
These are not required to get the most out of the document.

Playing with the compiler, and seeing what compiles, is a great way to learn Rust.
rustc, the standard Rust compiler, has excellent error messages.
Matt Godbolt’s Compiler Explorer is useful for getting a feel for the assembly that Rust produces.
The Rust Playground can also be used to see what Rust code does when executed, though it is even more limited.

This document is written with a view towards embedded development.
We will not be discussing any non-embedded Rust features: see

C++ users are cautioned: Rust shares a lot of terminology and concepts (ownership, lifetimes, destructors, polymorphism) with it, but Rust’s realization of them tends to have significantly different semantics.
Experience in C++ should not be expected to translate cleanly.

What is Rust?

Rust is a general-purpose programming language with a focus on maximum programmer control and zero runtime overhead, while eliminating the sharp edges traditionally associated with such languages.
It is also sometimes called a “systems language”.

Syntactically and philosophically, Rust most resembles C++ and ML (a family of functional programming languages), though semantically it is very different from both.
Rust is the first popular, well-supported language that provides absolute memory safety2 without the use of automatic reference counting or a garbage collector.
This is primarily achieved through a technology called the borrow checker, which uses source code annotations to statically prove that memory is not accessed after it is no longer valid, with no runtime checking.

Rust compiles to native code and rivals C and C++ for memory and compute performance, and can seamlessly integrate with anything using a C calling convention.
It also statically eliminates a large class of memory bugs associated with security vulnerabilities.
The Rust toolchain is built on LLVM, so all work towards LLVM performance is surfaced in Rust.

Rust also contains a special dialect called Unsafe Rust, which disables some static checking for the rare times when it is necessary to perform low-level operations.
We will make reference to this dialect throughout the document.

A complete Rust toolchain consists of a few major parts:

  • rustc, the Rust compiler3.
  • rustup, the Rust toolchain installer4.
  • cargo, a build system for Rust (though rustc can be invoked directly or by other tools).
  • std and core, the standard libraries.

The Rust toolchain is on a six-week release cycle, similar to Chrome’s: every six weeks, a release branch is cut as the next beta, and after six weeks it becomes the next stable.
Nightly Rust is cut from master every day; it is on nightly that unstable features can be enabled.
Some unstable features5 are very useful for embedded, so it is not uncommon for embedded Rust projects to use a nightly compiler.

rustup is used for managing Rust installations.
This is mostly necessary because Rust releases too frequently for operating system package managers to keep up, and projects can pin specific versions of Rust.
When the Rust toolchain is installed through rustup, components such as rustc and cargo will be aware of it; rustc +nightly-2020-03-22 will defer to rustup to download and execute the rustc nightly built on March 22.
A file named rust-toolchain in a project directory can be used to the same effect.6

Cargo is a build system/package manager for Rust.
It can build projects (that is, directories with a Cargo.toml file in them) and their dependencies automatically.
Individual units of compilation in Rust are called “crates”, which are either static libraries7 (i.e., comparable to a .a file) or fully linked native binaries.
This is in contrast to C, where each .c file generates a separate object file.8 Rust also does not have headers, though it provides a module system for intra-crate code organization that will be discussed later on.
Tock boards are a good example of what a more-complex cargo file looks like:

Some useful cargo subcommands include:

  • cargo check runs rustc’s checks, but stops before it starts emitting code and optimizing it.
    This is useful for checking for errors during development.
  • cargo build builds a library or binary, depending on the crate type.
  • cargo clippy runs the Rust linter, Clippy.
  • cargo doc --open builds crate documentation and then opens it in the browser.
  • cargo fmt runs the Rust formatter9.

Also, the contents of the RUSTFLAGS environment variable are passed to rustc, as a mechanism for injecting flags.

The Rust standard library, like libc, is smaller in embedded environments.
The standard library consists of three crates: core10, alloc, and std.
core, sometimes called libcore, is all of the fundamental definitions, which don’t rely on operating system support.
Nothing in core can heap-allocate.
alloc does not require OS support, but does require malloc and free symbols.
std is core + alloc, as well as OS APIs like file and thread support.
The #[no_std] pragma disables std and alloc, leaving behind core.
Throughout this document, we will only use core types, though we may refer to them through the std namespace (they’re aliases).
That is, we may refer to std::mem::drop, though in #[no_std] code it must be named core::mem::drop.

rustc has a number of flags. The most salient of these are:

  • --emit asm and --emit llvm-ir, useful for inspecting compiler output.
  • --target, which sets the cross-compilation target.
    It has a similar role to Clang’s -target, -march, and -mabi flags.
    It accepts a target definition (which in many cases resembles an LLVM target triple) that defines the platform.
    For example, OpenTitan software uses the riscv32imc-unknown-none-elf target.
    Using a target that isn’t the host target (e.g.,x86_64-unknown-linux-musl) requires installing the corresponding standard library build with rustup component install rust-std-.
    See rustc --print targets.
    --target is also accepted directly by Cargo, unlike most rustc flags.
  • -C link-arg, equivalent to Clang’s -T.
  • -C opt-level, equivalent to Clang’s -O (we mostly use -C opt-level=z for embedded).
  • -C lto, equivalent to Clang’s -flto.
  • -C force-frame-pointers, equivalent to Clang’s -fno-omit-frame-pointer.
  • -D warnings is roughly equivalent to -Werror.

Other interesting flags can be found under rustc -C help and, on nightly, under rustc -Z help.

Part I: Rewriting your C in Rust

Before diving into Rust’s specific features, we will begin by exploring how C concepts map onto Rust, as well as Unsafe Rust, a dialect of Rust that is free of many of Rust’s restrictions, but also most of its safety guarantees.


Rust and C have roughly the same approach to types, though Rust has few implicit conversions (for example, it lacks integer promotion like C).
In this section, we will discuss how to translate C types into Rust types.


Rust lacks C’s int, long, unsigned, and other types with an implementation-defined size.
Instead, Rust’s primitive integer types are exact-size types: i8, i16, i32, i64, and i128 are signed integers of 8, 16, 32, 64, and 128 bits, respectively, while u8, u16, u32, u64, and u128 are their unsigned variants.
Rust also provides isize and usize, which correspond to intptr_t and uintptr_t11.
Alignment requirements are exactly the same as in C.

Rust supports all of the usual binary operations on all of its integer types1213, though you can’t mix different types when doing arithmetic, and, unlike in C, there is no integer promotion.
Overflow in Rust is different from C14: it is implementation-defined, and must either crash the program15 or wrap around16.
Casting is done with the as keyword, and behaves exactly the same way as in C: (uint8_t) x is written x as u8. Integer types never implicitly convert between each other, even between signed and unsigned variants.

Rust has the usual integer literals: 123 for decimal, 0xdead for hex, 0b1010 for binary, and 0o777 for octal.
Underscores may be arbitrarily interspersed throughout an integer literal to separate groups of digits: 0xdead_beef, 1_000_000.
They may also be suffixed with the name of a primitive integer type to force their type: 0u8, 0o777i16, 12_34_usize; they will otherwise default to whatever type inference (more on this later) chooses, or i32 if unconstrained.

Rust also has a dedicated bool type.
It is not implicitly convertible with integers, and is otherwise a u8 that is guaranteed to have either the value 0x00 or 0x0117, with respective literals false and true.
bool supports all of the bitwise operations, and is the only type compatible with short-circuiting && and ||.
It is also the only type that can be used in if and while conditions.

Integers have an extensive set of built-in operations for bit-twiddling, exposed as methods, such as x.count_zeros() and x.next_power_of_two()18.
See for examples.

Structs and Tuples

Structs are declared almost like in C:

struct MyStruct {
  pub foo: i32,
  pub bar: u8,

Rust has per-field visibility modifiers (pub); we’ll give visibility a more thorough treatment later.

Struct values can be created using an analogue of C’s designated initialization19 syntax:

MyStruct { foo: -42, bar: 0xf, }

Rust structs are not laid out like C structs, though; in fact, Rust does not specify the layout of its structs20.
A C struct can be specified in Rust using the #[repr(C)] attribute:

struct MyCStruct {
  a: u8,
  b: u32,
  c: u8,

This is guaranteed to lay out fields in declaration order, adding padding for alignment.
#[repr(Rust)] is the implicit default.
#[repr(packed)] is analogous to __attribute__((packed)), and will not produce any padding21.
The alignment of the whole struct can be forced to a larger value using #[repr(align(N))], similar to _Alignas.

Fields can be accessed using the same dot syntax as C:, = 5;.

Rust also provides “tuple-like structs”, which are structs with numbered, rather than named, fields:

struct MyTuple(pub u32, pub u8);

Fields are accessed with a similar dot syntax: tuple.0, tuple.1, and are constructed with function-call-like syntax: MyTuple(1, 2).
Other than syntax, they are indistinguishable from normal structs. The fields on a tuple-like struct may be omitted to declare a zero-byte22 struct:

Anonymous versions of tuples are also available: (u32, u8).
These are essentially anonymous structs with unnamed fields.
The empty tuple type, (), is called “unit”, and serves as Rust’s void type (unlike void, () has exactly one value, also named (), which is zero-sized).
Rust has another void-like type, !, which we’ll discuss when we talk about functions.

If every field of a struct can be compared with ==, the compiler can generate an equality function for your struct:

#[derive(PartialEq, Eq)]
struct MyStruct {
  a: u32,
  b: u8,

This makes it possible to use == on MyStruct values, which will just perform field-wise equality.
The same can be done for ordering operations like < and >=: #[derive(PartialOrd, Ord)] will define comparison functions that compare structs lexicographically.

Enums and Unions

Much like C, Rust has enumerations for describing a type with a fixed set of values:

enum MyEnum {
    Banana, Apple, Pineapple,

Unlike C, though, MyEnum is a real type, and not just an alias for an integer type.
Also unlike C, the enum’s variants are not dumped into the global namespace, and must instead be accessed through the enum type: MyEnum::Banana.
Note that, unlike structs, the variants of an enum are automatically public.

While Rust does represent enum values with integers (these integers are called discriminants), the way they’re laid out is unspecified.
To get an enum whose discriminants are allocated like in C, we can employ a repr attribute:

enum MyCEnum {
  Banana = 0,
  Apple = 5,
  Pineapple = 7,

Unlike C, though, Rust will only guarantee discriminant values that are explicitly written down.
Such enums may be safely cast into integer types (like MyCEnum::Apple as u32), but not back: the compiler always assumes that the underlying value of a MyCEnum is 0, 5, or 7, and violating this constraint is Undefined Behavior (UB)23.
If we want to require that an enum is an exact integer width, we can use #[repr(T)], where T is an integral type like u16 or i8.

Unions in Rust are a fairly recent feature, and are generally not used for much in normal code.
They are declared much like structs:

union MyUnion {
  pub foo: i32,
  pub bar: u8,

and created much like structs:

MyUnion { bar: 0xa, }  // `bar` is the active variant.

Assigning to union variants is the same as in structs, but reading a variant requires Unsafe Rust, since the compiler can’t prove that you’re not reading uninitialized or invalid data24, so you’d need to write

unsafe { }  // I assert that bar is the active variant.

Unions also have restrictions on what types can be used as variants, due to concerns about destructors.

Since unions are so useful in C, but utterly unsafe, Rust provides built-in tagged unions25, which are accessed through the enum syntax:

enum MyEnum {
  FooVariant { foo: i32 },

Tagged-union enum variants use the same syntax as Rust structs; enum consists of a tag value (the discriminant) big enough to distinguish between all the variants, and a compiler-tracked union of variants.
However, effective use of such enums requires pattern matching, which requires its own section to explain.
We’ll see these enums again when we discuss patterns.

Just like with structs, #[derive] can be used on enums to define comparison operators, which are defined analogously to the struct case.


Rust arrays are just C arrays: inline storage of a compile-time-known number of values.
T[N] in C is spelled [T; N] in Rust.
Arrays are created with [a, b, c] syntax, and an array with lots of copies of the same value can be created using [0x55u8; 1024]26.
A multi-dimensional array can be declared as an array of arrays: [[T; N]; M].

Array elements can be accessed with x[index], exactly like in C.
Note, however, that Rust automatically inserts bounds checks around every array access27; failing the bounds check will trigger a panic in the program.
Unsafe Rust can be used to cheat the bounds check when it is known (to the programmer, not rustc!) to be unnecessary to perform it, but when it is performance critical to elide it.

Rust arrays are “real” types, unlike in C. They can be passed by value into functions, and returned by value from functions.
They also don’t decay into pointers when passed into functions.


Like every other embedded language, Rust has pointers.
These are usually referred to as raw pointers, to distinguish them from the myriad of smart pointer types.
Rust spells T* and const T* as *mut T and *const T.
Unlike in C, pointers do not need to be aligned to their pointee type28 until they are dereferenced (like C, Rust assumes that all pointer reads/writes are well-aligned).

Note that C’s type-based strict aliasing does not exist in Rust29.
As we’ll learn later, Rust has different aliasing rules for references that are both more powerful and which the compiler can check automatically.

usize, isize, and all pointer types may be freely cast back and forth.30 Null pointers may be created using the std::ptr::null() and std::ptr::null_mut() functions31.
Rust pointers do not support arithmetic operators; instead, a method fills this role: instead of ptr + 4, write ptr.offset(4).
Equality among pointers is simply address equality.

Pointers can be dereferenced with the *ptr syntax32, though this is Unsafe Rust and requires uttering unsafe.
When pointers are dereferenced, they must be well-aligned and point to valid memory, like in C; failure to do so is UB.
Unlike in C, the address-of operator, &x, produces a reference33, rather than a pointer.
&x as *const T will create a pointer, instead.

Pointer dereference is still subject to move semantics, like in normal Rust34.
the read() and write() methods on pointers can be used to ignore these rules35.
read_unaligned()36 and write_unaligned()37 can be used to perform safe38 unaligned access, and copy_to()39 and copy_nonoverlapping_to()40 are analogous to memmove() and memcpy(), respectively.
See for other useful pointer methods.
Volatile operations are also performed using pointer methods, which are discussed separately later on.

Since all of these operations dereference a pointer, they naturally are restricted to Unsafe Rust.

As we will discover later, Rust has many other pointer types beyond raw pointers.
In general, raw pointers are only used in Rust to point to potentially uninitialized memory41 and generally denote addresses, rather than references to actual memory.
For that, we use references, which are discussed much later.

We will touch on function pointers when we encounter functions.


Like C, Rust has globals and functions.
These, along with the type definitions above, are called items in the grammar, to avoid confusion with C’s declaration/definition distinction.
Unlike C, Rust does not have forward declaration or declaration-order semantics; everything is visible to the entire file.
Items are imported through dedicated import statements, rather than textual inclusion; more on this later.

Constants and Globals

Rust has dedicated syntax for compile-time constants, which serve the same purpose as #defined constants do in C.
Their syntax is

const MY_CONSTANT: u32 = 0x42;

The type is required here, and the right-hand side must be a constant expression, which is roughly any combination of literals, numeric operators, and const fns (more on those in a moment).

Constants do not exist at runtime.
They can be thought of best as fixed expressions that get copy+pasted into wherever they get used, similar to #defines and enum declarators in C.
It is possible to take the address of a constant, but it is not guaranteed to be consistent.42

Globals look like constants, but with the keyword static43:

static MY_GLOBAL: u8 = 0x00;
static mut MY_MUTABLE_GLOBAL: Foo = Foo::new();

Globals are guaranteed to live in .rodata, .data, or .bss, depending on their mutability and initialization.
Unlike constants, they have unique addresses, but as with constants, they must be initialized with constant expressions.44

Mutable globals are particularly dangerous, since they can be a source of data races in multicore systems.
Mutable globals can also be a source other racy behavior due to IRQ control flow.
As such, reading or writing to a mutable global, or creating a reference to one, requires Unsafe Rust.


In C and Rust, functions are the most important syntactic construct. Rust declares functions like so:

fn my_function(x: u32, y: *mut u32) -> bool {
  // Function body.

The return type, which follows the -> token, can be omitted when it is () (“unit”, the empty tuple), which serves as Rust’s equivalent of the void type.
Functions are called with the usual foo(a, b, c) syntax.

The body of a function consists of a list of statements, potentially ending in an expression; that expression is the function’s return value (no return keyword needed).
If the expression is missing, then () is assumed as the return type. Items can be mixed in with the statements, which are local to their current scope but visible in all of it.

Rust functions can be marked as unsafe fn.
This means the function cannot be called normally, and must instead be called using Unsafe Rust.
The body of an unsafe fn behaves like an unsafe block; we’ll go more into this when we discuss Unsafe Rust in detail.

Rust functions have an unspecified calling convention45.
To declare a function with a different calling convention, the function is declared extern "ABI" fn foo(), where ABI is a supported ABI.
"C" is the only one we really care about, which switches the calling convention to the system’s C ABI. The default, implicit calling convention is extern "Rust"46.

Marking functions as extern does not disable name mangling47; this must be done by adding the #[no_mangle] attribute to the function.
Unmangled functions can then be called by C, allowing a Rust library to have a C interface.
A mangled, extern "C" function’s main use is to be turned into a function pointer to pass to C.

Function pointer types look like functions with all the variable names taken out: fn(u32, *mut u32) -> bool.
Function pointers cannot be null, and must always point to a valid function with the correct ABI.
Function pointers can be created by implicitly converting a function into one (no & operator).
Function pointers also include the unsafe-ness and extern-ness of a function: unsafe extern "C" fn() -> u32.

Functions that never return have a special return type !, called the “never type”.
This is analogous to the noreturn annotation in C.
However, using an expression of type ! is necessarily dead code, and, as such, ! will implicitly coerce to all types (this simplifies type-checking, and is perfectly fine since this all occurs in provably-dead code).

Functions can also be marked as const.
This makes the function available for constant evaluation, but greatly restricts the operations available.
The syntax available in const functions increases in every release, though.
Most standard library functions that can be const are already const.


Rust, like C, has macros.
Rust macros are much more powerful than C macros, and operate on Rust syntax trees, rather than by string replacement.
Macro calls are differentiated from function calls with a ! following the macro name.
For example, file!() expands to a string literal with the file name.
To learn more about macros, see


Rust has type, which works exactly like typedef in C.
Its syntax is

Expressions and Statements

Very much unlike C, Rust has virtually no statements in its grammar: almost everything is an expression of some kind, and can be used in expression context.
Roughly speaking, the only statement in the language is creating a binding:

The type after the : is optional, and if missing, the compiler will use all information in the current scope, both before and after the let, to infer one48.

An expression followed by a semicolon simply evaluates the expression for side-effects, like in other languages49.
Some expressions, like ifs, whiles, and fors, don’t need to be followed by a semicolon.
If they are not used in an expression, they will be executed for side-effects.

let bindings are immutable by default, but let mut x = /* ... */; will make it mutable.

Like in C, reassignment is an expression, but unlike in C, it evaluates to () rather than to the assigned value.

Like in almost all other languages, literals, operators, function calls, variable references, and so on are all standard expressions, which we’ve already seen how Rust spells already.
Let’s dive into some of Rust’s other expressions.


Blocks in Rust are like better versions of C’s blocks; in Rust, every block is an expression.
A block is delimited by { }, consists of a set of a list of statements and items, and potentially an ending expression, much like a function.
The block will then evaluate to the final expression. For example:

let foo = {
  let bar = 5;
  bar ^ 2

Blocks are like local functions that execute immediately, and can be useful for constraining the scope of variables.

If a block does not end in an expression (that is, every statement within ends in a semicolon), it will implicitly return (), just like a function does.
This automatic () is important when dealing with constructs like if and match expressions that need to unify the types of multiple branches of execution into one.

Conditionals: if and match

Rust’s if expression is, syntactically, similar to C’s. The full syntax is

if cond1 {
  // ...
} else if cond2 {
  // ...
} else {
  // ...

Conditions must be expressions that evaluate to bool.
A conditional may have zero or more else if clauses, and the else clause is optional.
Because of this, Rust does not need (and, thus, does not have) a ternary operator:

let x = if c { a } else { b };

Braces are required on if expressions in Rust.

if expressions evaluate to the value of the block that winds up getting executed.
As a consequence, all blocks must have the same type50.
For example, the following won’t compile, because an errant semicolon causes type checking to fail:

if cond() {
  my_int(4)   // Type is i32.
} else {
  my_int(7);  // Type is (), due to the ;

i32 and () are different types, so the compiler can’t unify them into an overall type for the whole if.

In general, it is a good idea to end all final expressions in if clauses with a semicolon, unless its value is needed.

Like C, Rust has a switch-like construct called match. You can match integers:

let y = match x {
  0 => 0x00,       // Match 0.
  1..=10 => 0x0f,  // Match all integers from 1 to 10, inclusive.
  _ => 0xf0,       // Match anything, like a `default:` case.

Like if expressions, match expressions produce a value.
The syntax case val: stuff; break; roughly translates into val => stuff, in Rust.
Rust calls these case clauses “match arms”51.

match statements do not have fallthrough, unlike C.
In particular, only one arm is ever executed.
Rust, however, allows a single match arm to match multiple values:

match x {
  0 | 2 | 4 => /* ... */,
  _ => /* ... */,

Rust will statically check that every possible case is covered.
This is especially useful when matching on an enum:

enum Color { Red, Green, Blue, }
let c: Color = /* ... */;
match c {
  Color::Red =>   /* ... */,
  Color::Green => /* ... */,
  Color::Blue =>  /* ... */,

No _ => case is required, like a default: would in C, since Rust statically knows that all cases are covered (because enums cannot take on values not listed as a variant).
If this behavior is not desired in an enum (because more variants will be added in the future), the #[non_exhaustive] attribute can be applied to an enum definition to require a default arm.

We will see later that pattern matching makes match far more powerful than C’s switch.

Loops: loop and while

Rust has three kinds of loops: loop, while, and for.
for is not a C-style for, so we’ll discuss it later.
while is simply the standard C while loop, with slightly different syntax:

while loop_condition { /* Stuff. */ }

It can be used as an expression, but it always has type (); this is most notable when it is the last expression in a block.

loop is unique to Rust; it is simply an infinite loop52:

Because infinite loops can never end, the type of the loop expression (without a break in it!) is !, since any code after the loop is dead.
Having an unconditional infinite loop allows Rust to perform better type and lifetime analysis on loops.
Under the hood, all of Rust’s control flow is implemented in terms of loop, match, and break.

Control Flow

Rust has return, break, and continue, which have their usual meanings from C.
They are also expressions, and, much like loop {}, have type ! because all code that follows them is never executed (since they yank control flow).

return x exits a function early with the value x.
return is just syntax for return ().
break and continue do the usual things to loops.

All kinds of loops may be annotated with labels (the only place where Rust allows labels):

break and continue may be used with those labels (e.g. break 'a), which will break or continue the enclosing loop with that label (rather than the closest enclosing loop).
While C lacks this feature, most languages without goto have it.

It is also possible to break-with-value out of an infinite loop, which will cause the loop expression to evaluate to that value, rather than !53.

let value = loop {
  let attempt = get();
  if successful(attempt) {
    break attempt;

Talking to C

One of Rust’s great benefits is mostly-seamless interop with existing C libraries.
Because Rust essentially has no runtime, Rust types that correspond to C types can be trivially shared, and Rust can call C functions with almost no overhead54.
The names of external symbols can be “forward-declared” using an extern block, which allows Rust to name, and later link with, those symbols:

extern "C" {
  fn malloc(bytes: usize) -> *mut u8;
  static mut errno: i32;

When the ABI specified is "C", it can be left off; extern {} is implicitly extern "C" {}.

It is the linker’s responsibility to make sure those symbols wind up existing.
Also, some care must be taken with what types are sent over the boundary.
See for more details.

Analogues of other functionality


Rust does not have a volatile qualifier.
Instead, volatile reads can be performed using the read_volatile()55 and write_volatile()56 methods on pointers, which behave exactly like volatile pointer dereference in C.

Note that these methods work on types wider than the architecture’s volatile loads and stores, which will expand into a series of volatile accesses, so beware.
The same caveat applies in C: volatile uint64_t will emit multiple accesses on a 32-bit machine.

Inline Assembly

Rust does not quite support inline assembly yet.
Clang’s inline assembly syntax is available behind the unstable macro llvm_asm!(), which will eventually be replaced with a Rust-specific syntax that better integrates with the language.
global_asm!() is the same, but usable in global scope, for defining whole functions.
Naked functions can be created using #[naked]. See

Note that this syntax is currently in the process of being redesigned and stabilized.

Bit Casting

Rust provides a type system trap-door for bitcasting any type to any other type of the same size:

let x = /* ... */;
let y = std::mem::transmute<A, B>(x);

This trap-door is extremely dangerous, and should only be used when as casts are insufficient.
The primary embedded-relevant use-case is for summoning function pointers from the aether, since Rust does not allow casting raw pointers into function pointers (since the latter are assumed valid). has a list of uses, many of which do not actually require transmute.

Linker Shenanigans and Other Attributes

Below are miscellaneous attributes relevant to embedded programming.
Many of these subtly affect linker/optimizer behavior, and are very much in the “you probably don’t need to worry about it” category.

  • #[link_section = ".my_section"] is the rust spelling of __attribute__((section(".my_section"))), which will stick a symbol in the given ELF (or equivalent) section.
  • #[used] can be used to force a symbol to be kept by the linker57 (this is often done in C by marking the symbol as volatile). The usual caveats for __attribute__((used)), and other linker hints, apply here.
  • #[inline] is analogous to C’s inline, and is merely a hint; #[inline(always)] and #[inline(never)] will always58 or never be inlined, respectively.
  • #[cold] can also be used to pessimize inlining for functions that are unlikely to ever be called.

Part II: Rust-Specific Features

The previous part established enough vocabulary to take roughly any embedded C program and manually translate it into Rust.
However, those Rust programs are probably about as safe and ergonomic as the C they came from.
This section will focus on introducing features that make Rust safer and easier to write.


Double-free or, generally, double-use, are a large class of insidious bugs in C, which don’t look obviously wrong at a glance:

// `handle` is a managed resource to a peripheral, that should be
// destroyed to signal to the hardware that the resource is not in use.
my_handle_t handle = new_handle(0x40000);
use_for_scheduling(handle);  // Does something with `handle` and destroys it.
// ... 200 lines of scheduler code later ...
use_for_scheduling(handle);  // Oops double free.

Double-free and use-after-free are a common source of crashes and security vulnerabilities in C code.
Let’s see what happens if we were to try this in Rust.

Consider the equivalent code written in Rust:

let handle = new_handle(0x40000);
// ...

If you then attempt to compile this code, you get an error:

error[E0382]: use of moved value: `handle`
  --> src/
7  |     let handle = new_handle(0x40000);
   |         ------ move occurs because `handle` has type `Handle`,
   |                which does not implement the `Copy` trait
8  |     use_for_scheduling(handle);
   |                        ------ value moved here
9  |     // ...
10 |     use_for_scheduling(handle);
   |                        ^^^^^^ value used here after move

Use-after-free errors (and double-free errors) are impossible in Safe Rust.
This particular class of errors (which don’t directly involve pointers) are prevented by move semantics.
As the error above illustrates, using a variable marks it as having been “moved from”: the variable is now an empty slot of uninitialized memory.
The compiler tracks this statically, and compilation fails if you try to move out again.
The variable in which a value is currently stored is said to be its “owner”59; an owner is entitled to hand over ownership to another variable60, but may only do so once.

The error also notes that “Handle does not implement the Copy trait.”
Traits proper are a topic for later; all this means right now is that Handle has move semantics (the default for new types).
Types which do implement Copy have copy semantics; this is how all types passed-by-value in C behave: in C, passing a struct-by-value always copies the whole struct, while passing a struct-by-reference merely makes a copy of the pointer to the struct.
This is why moves are not relevant when handling integers and raw pointers: they’re all Copy types.

Note that any structs and enums you define won’t be Copy by default, even if all of their fields are.
If you want a struct whose fields are all Copy to also be Copy, you can use the following special syntax:

#[derive(Clone, Copy)]
struct MyPodType {
  // ...

Of course, the copy/move distinction is something of a misnomer: reassignment due to copy and move semantics compiles to the same memcpy or register move code.
The distinction is purely for static analysis.

References and Lifetimes

Another class of use-after-free involves stack variables after the stack is destroyed.
Consider the following C:

const int* alloc_int(void) {
  int x = 0;
  return &x;

This function is obviously wrong, but such bugs, where a pointer outlives the data it points to, are as insidious as they are common in C.

Rust’s primary pointer type, references, make this impossible.
References are like raw pointers, except they are always well-aligned, non-null, and point to valid memory; they also have stronger aliasing restrictions than C pointers.
Let’s explore how Rust achieves this last guarantee.

Consider the following Rust program:

fn alloc_int() -> &i32 {
  let x = 0i32;

This program will fail to compile with a cryptic error: missing lifetime specifier.
Clearly, we’re missing something, but at least the compiler didn’t let this obviously wrong program through.

A lifetime, denoted by a symbol like 'a61 (the apostrophe is often pronounced “tick”), labels a region of source code62.
Every reference in Rust has a lifetime attached, representing the region in which a reference is known to point to valid memory: this is specified by the syntax &'a i32: reference to i32 during 'a.
Lifetimes, much like types, do not exist at runtime; they only exist for the compiler to perform borrow checking, in which the compiler ensures that references only exist within their respective lifetimes.
A special lifetime, 'static represents the entire program.
It is the lifetime of constants and global variables.

Consider the following Rust code:

let x: i32 = 42;
let y: &'a i32 = &x;  // Start of 'a.
use_value(x);  // End of 'a, because x has been moved.
use_reference(y);  // Error: use of y outside of 'a.

Reference lifetimes start when the reference is taken, and end either when the lifetime goes out of scope or when the value referenced is moved.
Trying to use the reference outside of the lifetime is an error, since it is now a dangling pointer.

Rust often refers to references as borrows: a reference can borrow a value from its owner for a limited time (the lifetime), but must return it before the owner gives the value up to someone else.
It is also possible for a reference to be a borrow of a borrow, or a reborrow: it is always possible to create a reference with a shorter lifetime but with the same value as another one.
Reborrowing is usually performed implicitly by the compiler, usually around call sites, but can be performed explicitly by writing &*x.

Lifetimes can be elided in most places they are used:

fn get_field(m: &MyStruct) -> &u32 {
  &m.field  // For references, unlike for raw pointers, . acts the same way -> does in C.

Here, the compiler assumes that the lifetime of the return type should be the same as the lifetime of the m63.
However, we can write this out explicitly:

fn get_field<'a>(m: &'a MyStruct) -> &'a u32 { /* ... */ }

The <'a> syntax introduces a new named lifetime for use in the function signature, so that we can explicitly tell the compiler “these two references have the same lifetime”.
This is especially useful for specifying many lifetimes, when the compiler can’t make any assumptions:

fn get_fields<'a, 'b>(m1: &'a MyStruct, m2: &'b MyStruct) -> (&'a u32, &'b u32) {
  (&m1.field, &m2.field)

Now we can try to fix our erroneous stack-returning function.
We need to introduce a new lifetime for this function, since there’s no function arguments to get a lifetime from:

fn alloc_int<'a>() -> &'a i32 {
  let x = 0i32;

This now gives us a straightforward error, showing the borrow-checking prevents erroneous stack returns:

error[E0515]: cannot return reference to local variable `x`
 --> src/
9 |   &x
  |   ^^ returns a reference to data owned by the current function

This <'a> syntax can also be applied to items like structs: If you’re creating a type containing a reference, the <'a> is required:

struct MyRef<'a> {
  meta: MyMetadata,
  ptr: &'a u32,  // Lifetime elision not allowed here.

Rust’s references come in two flavors: shared and unique.
A shared reference, &T, provides immutable access to a value of type T, and can be freely duplicated: &T is Copy.
A unique reference, &mut T, provides mutable access to a value of type T, but is subject to Rust’s aliasing rules, which are far more restrictive than C’s strict aliasing, and can’t be turned off.

There can only be one &mut T active for a given value at a time.
This means that no other references may be created within the lifetime of the unique reference.
However, a &mut T may be reborrowed, usually for passing to a function.
During the reborrow lifetime, the original reference cannot be used.
This means the following code works fine:

fn do_mut(p: &mut Handle) { /* ... */ }

let handle: &mut Handle = /* ... */;
do_mut(handle);  // Reborrow of handle for the duration of do_mut.
// handle is valid again.
do_mut(handle);  // Reborrow again.

In other words, Rust does not have a safe equivalent to int*; it only has equivalents for const int* and int* restrict … plus casting away const is instant Undefined Behavior64.
Rust will aggressively collapse loads and stores, and assume that no mutable references alias for the purpose of its alias analysis.
This means more optimization opportunities, without safe code needing to do anything.65

Finally, it should go without saying that references are only useful for main memory; Rust is entitled to generate spurious loads and stores for (possibly unused!) references66, so MMIO should be performed exclusively through raw pointers. elaborates further on the various lifetime rules.

Operations on References

Rust references behave much more like scalar values than like pointers (borrow-checking aside).
Because it is statically known that every reference, at all times, points to a valid, initialized value of type T, explicit dereferencing is elided most of the time (though, when necessary, they can be dereferenced: *x is an lvalue that can be assigned to).

Rust does not have a -> operator, but, for x: &T, the dot operator behaves as if x were a T.
If field is a field of T, for example, x.field is the lvalue of field (which would be spelled x->field in C).
This applies even for heavily nested references: the dot operator on &&&T will trigger three memory lookups.
This is called the “auto-deref” behavior.

Equality of references is defined as equality of underlying values: x == y, for x: &T and y: &T, becomes *x == *y.
Pointer equality is still possible with std::ptr::eq(x, y).
References can be coerced into raw pointers: x as *const T, and compared directly.


While Rust is not an object-oriented language, it does provide a mechanism for namespacing functions under types: impl (for implementation) blocks.
These also allow you to make use of Rust’s visibility annotations, to make implementation details and helpers inaccessible to outside users.

Here’s an example of a type with methods.

pub struct Counter(u64);  // Non-public field!
impl Counter {
  /// Creates a new `Counter`.
  pub fn new() -> Self {

  /// Private helper.
  fn add(&mut self, x: u64) {
    self.0 += x;

  /// Get the current counter value.
  pub fn get(&self) -> u64 {

  /// Increment the counter and return the previous value.
  pub fn inc(&mut self) -> u64 {
    let prev = self.get();

  /// Consumes the counter, returning its final value.
  pub fn consume(self) -> u64 {

Outside modules cannot access anything not marked as pub, allowing us to enforce an invariant on Counter: it is monotonic.
Let’s unpack the syntax.

Functions in an impl block are called “inherent functions” or “methods”, depending on whether they take a self parameter.
Inherent functions don’t take a self, and are called like Counter::new()67.

A self parameter is a parameter named self (which is a keyword) and having a type involving Self68 (another keyword, which is a type alias for the type the impl block is for), such as &Self69.
The syntax self, &self, mut self, and &mut self are sugar for the common forms self: Self, self: &Self, mut self: Self and self: &mut Self, representing self-by-value, self-by-reference, self-by-mut-value, and self-by-mut-reference.

Methods can, thus, be called like so:
Methods are really just normal functions: you could also have called this like Counter::inc(&mut my_counter).
Note that calling a function that takes &self or &mut self triggers a borrow of the receiving type; if a &self function is called on a non-reference value, the value will have its address taken, which gets passed into the method.

impl blocks, like other items, can be parameterized by lifetimes.
In order to add methods to a struct with a reference in it, the following syntax can be used:

impl<'a> MyStruct<'a> { /* ... */ }

If 'a is never actually used inside the impl block, this can be written using a placeholder lifetime:

impl MyStruct<'_> { /* ... */ }

As we’ve already seen, many primitive types have methods, too; these are defined in special impl blocks in the standard library.

Slices and for

References also do not allow for pointer arithmetic, so a &u32 cannot be used to point to a buffer of words.
Static buffers can be passed around as arrays, like &[u32; 1024], but often we want to pass a pointer to contiguous memory of a runtime-known value.
Slices are Rust’s solution to pointer+length.

A slice of T is the type [T]; this type is most like a “flexible array member” in C:

struct slice {
  size_t len;
  T values[];

Then, a slice* would point to a length followed by that many Ts; it can’t reasonably exist except behind a pointer.
Similarly, [T] is what Rust calls a dynamically sized type70, which needs to exist behind a reference: it is much more common to see &[T] and &mut [T].

However, Rust still differs from the C version: &[T] is a fat pointer, being two words wide.
It essentially looks like this:

struct Slice {
  len: usize,
  values: *const T,

A reference to a slice otherwise works like an array reference: &x[n] extracts a reference to the nth element in the slice (with bounds checking), x[n] = y assigns to it.
The length of a slice can also be extracted with the len method: x.len().

str71 is a slice-like type that is guaranteed to contain UTF-8 string data.

Slices can be created from arrays and other slices using a “ranged index operation”: &x[a..b]72. This takes the array or slice x and creates a slice with the elements from index a to index b (inclusive of a, exclusive of b), of length b - a.
&x[a..] is the suffix starting at a, &x[..b] is the prefix ending at b, and &x[..] is the whole slice, useful for converting arrays into slices.
Inclusive ranges are also available, with the syntax a..=b.

Slices can be iterated over, using for loops:

let slice: &[u32] = /* ... */;
for x in slice {
  // x is a reference to the nth element in slice.

If an index is desired, it is possible to iterate over a range directly:

for i in 0..slice.len() {
  let x = &slice[i];
  // ...

This can be combined with the _ pattern to simply repeat an operation n times:

One important note with slices, as pertains to borrowing, is unique references.
If we have a unique reference to a slice, it’s not possible to take unique references to multiple elements at once:

let slice: &mut [u32] = /* ... */;
let x = &mut slice[0];
let y = &mut slice[1];  // Error: slice is already borrowed.

The method split_at_mut()73 can be used to split a unique slice reference into two non-overlapping unique slice references:

let slice: &mut [u32] = /* ... */;
let (slice1, slice2) = slice.split_at_mut(1);
let x = &mut slice1[0];  // slice[0]
let y = &mut slice2[0];  // slice[1]

It is usually possible to structure code in such a way that avoids it, but this escape hatch exists for when necessary.
Slices can also be decomposed into their pointer and length parts, using the as_ptr() and len() functions, and reassembled with std::slice::from_raw_parts()74.
This operation is unsafe, but useful for bridging C and Rust, or Rust and Rust across a syscall or IPC boundary.

More slice operations can be found at and

String Literals

Rust string literals are much like C string literals: "abcd...".
Arbitrary ASCII-range bytes can be inserted with xNN, and supports most of the usual escape sequences.
However, all Rust strings are UTF-8 encoded byte slices: &str is a wrapper type around &[u8] that guarantees that the bytes inside are valid UTF-8.
The type of all string literals is &'static str.

Rust string literals can contain arbitrary newlines in them, which can be escaped:

// Equivalent to "foon  bar".
let s = "foo
// Equivalent to "foo  bar".
let s = "foo

Raw strings disable escape sequences, and are delimited by an arbitrary, matching number of pound signs:

let s = r"...";
let s = r#" ..."#;
let s = r#####"..."#####;

If instead a byte string with no encoding is required, byte strings can be used: b"...".
Their contents must be ASCII (or escaped bytes), and their type is &[u8].
Raw strings can also be byte strings: br#"..."#.

Rust also has character literals in the form of 'z'75, though their type is char, a 32-bit Unicode code-point.
To get an ASCII byte of type u8, instead, use b'z'.

Destructors and RAII

Destructors are special functions that perform cleanup logic when a value has become unreachable (i.e., both the let that originally declared it can no longer be named, and the last reference to it expired).
After the destructor is run, each of the value’s fields, if it’s a struct or enum, are also destroyed (or “dropped”).

Destructors are declared with a special kind of impl block (we’ll see more like this, later):

impl Drop for MyType {
  fn drop(&mut self) {
    // Dtor code.

If several values go out of scope simultaneously, they are dropped in reverse order of declaration.

The drop method can’t be called manually; however, the standard library function std::mem::drop() 76 can be used to give up ownership of a value and immediately destroy it.
Unions77 and types with copy semantics cannot have destructors.

Destructors enable the resource acquisition is initialization (RAII) idiom.
A type that holds some kind of temporary resource, like a handle to a peripheral, can have a destructor to automatically free that resource.
The resource is cleaned up as soon as the handle goes out of scope.

The classic example of RAII is dynamic memory management: you allocate memory with malloc, stash the returned pointer in a struct, and then that struct’s destructor calls free on that pointer.
Since the struct has gone out of scope when free is called, UAF is impossible.
Thanks to Rust’s move semantics, this struct can’t be duplicated, so the destructor can’t be called twice.
Thus, double-free is also impossible78.

In some situations, calling a destructor might be undesirable (for example, during certain Unsafe Rust operations).
The standard library provides the special function std::mem::forget()79, which consumes a value without calling its destructor.
The std::mem::ManuallyDrop80 type is a smart pointer81 that holds a T, while inhibiting its destructor.
For this reason, there is no expectation that a destructor actually runs.

The function std::mem::needs_drop()82 can be used to discover if a type needs to be dropped; even if it doesn’t have a drop method, it may recursively have a field which does.
std::ptr::drop_in_place()83 can be used to run the destructor in the value behind a raw pointer, without technically giving up access to it.

Pattern Matching

References cannot be null, but it turns out that a null value is sometimes useful.
Option is a standard library type representing a “possibly absent T"84.
It is implemented as an enum:

enum Option<T> {

The is similar to the lifetime syntax we saw before; it means that Option is a generic type; we’ll dig into those soon.

If we have a value of type Option (or, any other enum, really), we can write code conditioned on the value’s discriminant using pattern matching, which is accessed through the match expression:

let x: Option<u32> = /* ... */;
let y = match x {
  Some(val) => val,  // If `x` is a `Some`, bind the value inside to `val`.
  None => 42,  // If `x` is a `None`, do this instead.

The key thing pattern matching gives us is the ability to inspect the union within an enum safely: the tag check is enforced by the compiler.

Patterns are like expressions, forming a mini-language.
If expressions build up a value by combining existing values, patterns do the opposite: they build up values by deconstructing values.
In particular, a pattern, applied to an expression, performs the following operations:

  • Checks that the expression’s value actually matches that pattern.
    (Note that type-checking doesn’t go into this; patterns can’t be used for conditioning on the type of an expression.)
  • Optionally binds that expression’s value to a name.
  • Optionally recurs into subpatterns.

Here’s a few examples of patterns.
Keep in mind the matching, binding, and recurrence properties of each.
In general, patterns look like the value of the expression they match.

  • _, an underscore85 pattern.
    The match always succeeds, but it throws away the matched value.
    This is the _ in the equivalent of a default: case.
  • foo, an identifier pattern.
    This pattern is exactly like _, but it binds the matched value to its name.
    This is the val in the Some(val) above.
    This can also be used by itself as a default case that wants to do something with the matched-on value.
    The binding can be made mutable by writing Some(mut val) instead.
  • Any numeric literal, for a literal pattern.
    This match compares the matched value against the literal value, and doesn’t match anything.
    These can also be inclusive ranges: 5..=1686.
  • (pat1, pat2, /* etc */), a tuple pattern.
    This match operates on tuple types, and always succeeds: it extracts the individual elements of a tuple, and applies them to the pattern’s subpatterns.
    In particular, the () pattern matches the unit value ().
let x: (u32, u32) = /* ... */;
match x {
  (5, u) => /* ... */,  // Check that first element is five,
                        // bind the second element to `u`.
  (u, _) => /* ... */,  // Bind the first element to `u`,
                        // discard the second element.

let y: (u32, (u32, u32)) = /* ... */;
match y {
  // All patterns can nest arbitrarily, like expressions.
  (42, (u, _)) =>  /* ... */,
  // `..` can be used to match either a head or a tail of tuple.
  (.., u) => /* ... */,
  (u, ..) => /* ... */,
  (..) =>    /* ... */,  // Synonymous with _.
  • Struct patterns are analogous to tuple patterns.
    For a tuple-like struct, they have the exact same syntax, but start with the name of the struct: MyTuple(a, b, _).
    Regular structs are much more interesting syntax-wise:
struct MyStruct { a: i32, b: u32 }
match my_struct {
  MyStruct { a, b } => /* ... */,  // Bind the fields `a` and `b` to
                                   // names `a` and `b`, respectively.
  MyStruct { a: foo, b: _ } => /* ... */,  // Bind the field `a` to the name
                                           // `foo`, and discard the field `b`.
  MyStruct { a: -5, .. } => /* ... */  // Check that `a` is -5, and ignore
                                       // other fields.
  • Enum patterns are probably the most important kind of pattern, and are what we saw in the match statement with Option above.
    They’re very similar to struct patterns, except that instead of always succeeding, they check that the enum discriminant is the one named in the pattern.
enum MyEnum { A, B{u32), C { a: i32, b: i32 }, }
match my_enum {
  MyEnum::A =>    /* ... */,  // Match for variant `A`.
  MyEnum::B(7) => /* ... */,  // Match for variant `B`, with 7 as the value inside.
  MyEnum::B(x) => /* ... */,  // Match for variant `B`, binding the value inside to
                              // `x`.

  MyEnum::C { a: 7, .. } => /* ... */,  // Match for variant `C`, with 7 as the
                                        // value in `a` and all other fields ignored.

  MyEnum::C { b, .. } => /* ... */,  // Match for variant `C`, binding b to b.

A complete treatment of the pattern syntax can be found at

A match expression will evaluate each pattern against a value until one matches, in order; the compiler will warn about patterns which are unreachable87.
The compiler will also ensure that every value will get matched with one of the match arms, either because every case is covered (e.g., every enum variant is present) or an irrefutable pattern is present (i.e., a pattern which matches all values). _, foo, (a, _), and MyStruct { a, .. } are all examples of irrefutable patterns.

If the value being matched on is a reference of some kind, bound names will be references, too.

For example:

match &my_struct {
  MyStruct { a, .. } => {
    // Here, `a` is a `&i32`, which is a reference to the `a` field in my_struct.

This feature is sometimes called match ergonomics, since before it was added, explicit dereference and special ref pattern qualifiers had to be added to matches on references.

In addition, match statements support two additional features on top of the pattern syntax discussed above:

  • Multi-match arms can allow a match arm to match on one of multiple patterns: a | b | c => /* ... */,.
    If any of the patterns match, the arm executes.
  • Match guards give you a shorthand for conditioning an arm on some expression: Some(foo) if foo.has_condition() => /* ... */,.

Also, the standard library provides the matches!() macro as a shorthand for the following common match expression:

match expr {
  <some_complex_match_arm> => true,
  _ => false,
// ... can be replaced with ...
matches!(expr, some_complex_match_arm)

matches! supports multi-match and match guards as well.

Irrefutable patterns can be used with normal variable declaration.
The syntax let x = /* ... */; actually uses a pattern: x is a pattern.
When we write let mut x = /* ... */;, we are using a mut x pattern instead.
Other irrefutable patterns can be used there:

// Destructure a tuple, rather than using clunky `.0` and `.1` field names.
let (a, b) = /* ... */;

// Destructure a struct, to access its fields directly.
let Foo { foo, bar, baz } = /* ... */;

// Syntactically valid but not allowed: `42` is not an irrefutable pattern.
let 42 = /* ... */;

Special variants of if and while exist to take advantage of patterns, too:

if let Some(x) = my_option {
  // If the pattern succeeds, the body will be executed, and `x` will be bound
  // to the value inside the Option.
} else {
  // Else block is optional; `x` is undefined here.
  // do_thing(x);  // Error.

while let Some(x) = some_func() {
  // Loop terminates once the pattern match fails. Again, `x` is bound
  // to the value inside the Option.

Unlike normal let statements, if let and while let expressions are meant to be used with refutable patterns.

In general, almost every place where a value is bound can be an irrefutable pattern, such as function parameters and for loop variables:

fn get_first((x, _): (u32, u32)) -> u32 { x }

for (k, v) in my_key_values {
  // ...


Traits are Rust’s core code-reuse abstraction.
Rust traits are like interfaces in other languages: a list of methods that a type must implement.
Traits themselves, however, are not types.

A very simple trait from the standard library is Clone88:

trait Clone {
  fn clone(&self) -> Self;

A type satisfying Clone’s interface (in Rust parlance, “implements Clone") has a clone method with the given signature, which returns a duplicate of self.
To implement a trait, you use a slightly funny impl syntax:

impl Clone for MyType {
  fn clone(&self) -> Self { /* implementation */ }

This gives us a consistent way to spell “I want a duplicate of this value”.
The standard library provides traits for a number of similar operations, such as Default89, for providing a default value, PartialEq90 and Eq, for equality, PartialOrd91 and Ord, for ordering, and Hash92, for non-cryptographic hashing.

The above traits are special in that they have trivial implementations for a struct or enum, assuming that all fields of that struct or enum implement it.
The #[derive()] syntax described in the “Ownership” section can be used with any of these traits to automatically implement them for a type.
It is not uncommon for Plain Old Data (POD) types to look like this:

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct MyPod {
  pub a: u8,
  pub b: u8,
  // The following line wouldn't compile, because `derive(Eq)` requires
  // all fields to be `Eq`.
  // c: NonEq,

Traits can also provide built-in methods implemented in terms of other methods, to provide a default implementation (which can be overridden if a more efficient one is available for a particular type).
The full Clone trait actually looks like this:

pub trait Clone {
  fn clone(&self) -> Self;
  fn clone_from(&mut self, source: &Self) {
    *self = source.clone();

Implementers don’t need to provide clone_from, but are allowed to do so if the default implementation isn’t good enough.

Traits, and types that implement them, can be defined in different modules, so long as the implementing module defines either the trait or the type.
This means that trait methods aren’t really part of the type, but rather part of the trait plus the type.
Thus, in order to call trait methods on a particular type, that trait has to be in scope, too.
When unambiguous, trait functions can be called as either foo.trait_fn(), Foo::trait_fn(foo), or Trait::trait_fn(foo).
However, since names can sometimes be ambiguous, there is a fully unambiguous syntax93: ::trait_fn(foo).
This last syntax is also useful in generic contexts, or for being precise about the exact function being referred to.

Traits are also the vehicle for operator overloading: these traits are found in the std::ops94 module of the standard library.

Trait Objects

Traits can be used for dynamic dispatch (also known as virtual polymorphism) through a mechanism called trait objects.
Given a trait Trait, and a type T that implements it, we can as-cast a reference &T into a dynamic trait object: &dyn Trait.
For example:

trait Id {
    fn get_id(&self) -> usize;
impl Id for Device {
  // ...

let device: Device = /* ... */;
let dyn_id = &device as &dyn Id;  // Create a vtable.
let id = dyn_id.get_id();  // Indirect procedure call.

dyn Trait is a dynamically-sized type, much like slices, and can only exist behind a pointer.
The reference &dyn Trait looks something like this:

struct TraitObject {
  value: *mut (),
  vtable: *mut Vtable,

struct Vtable {
  size: usize,
  align: usize,
  dtor: fn(&mut T),
  // Other trait methods.

Thus, the dynamic function call to get_id would compile to something like the following:

let device: Device = /* ... */;
let dyn_id = &device as IdTraitObject;
let id = (dyn_id.vtable.get_id)(dyn_id.value);

There are some limitations on what traits can be made into trait objects: namely, functions cannot take or return functions of type Self; only &Self or &mut Self95.
In other words, all of the functions must treat Self as if it were not sized and only accessible through a pointer.96 A trait that can be made into a trait object is called object safe.
The type dyn Trait always behaves as if it implemented Trait, which is relevant for generics, discussed below.

Unsafe Traits

It is possible to mark a trait as unsafe by writing unsafe trait MyTrait { /* ... */ }; the only difference with normal traits is that it requires unsafe impl to be implemented.
Unsafe traits typically enforce some kind of additional constraint in addition to their methods; in fact, unsafe traits frequently don’t have methods at all.
For example, the standard library trait Sync is implemented by all types which synchronize access97.
Because the invariant this trait asserts is beyond what the compiler can check, it is an unsafe trait.

Trait methods may separately be marked as unsafe.
This is usually done to indicate that not only does care need to be taken in implementing the trait, but calling the function also requires care (and uttering unsafe).
This is separate from marking a trait as unsafe, and it is not necessary to mark a trait as unsafe for it to have unsafe methods.

Auto Traits

Auto traits are a compiler mechanism for automatically implementing certain traits; in the standard library’s source code, this shows up as auto trait Foo {} (though this syntax is unavailable for normal libraries).
Auto traits are implemented automatically for a struct or enum type if all of its fields also implement that trait, and are used for exposing transitive properties to the trait system.
For example, Send and Sync are auto traits; a number of other marker traits98 are also auto traits.

Auto traits are always markers that you don’t really want to opt out of.
They’re like the opposite of derive() traits, which you need to opt into, since they meaningfully affect the API of your type in a way that it is important to be able to control.


Generic programming is writing source code that can be compiled for many types.
Generics are one of Rust’s core features, which enable polymorphic99 static dispatch.

Functions can be made generic by introducing type parameters, using a syntax similar to explicit lifetimes100:

fn identity<T>(x: T) -> T {

This function accepts a value of any type and immediately returns it.
It can then be called like this: identity::(42)101.
Using a generic function with all of its type parameters filled in causes it to be instantiated (or monomorphized), resulting in code being generated for it.
This process essentially consists of replacing each occurrence of T with its concrete value.

Each distinct instantiation is a separate function at runtime, with a separate address, though for functions which generate identical code, like identity:: and identity::, the linker may deduplicate them.
Overzealous use of generic code can lead to binary bloat.

Most of the time, the ::<> bit (referred to by some reference materials as the “turbofish”) is unnecessary, since Rust type deduction can infer it: let x: u64 = identity(42); will infer that T = u64.
It can, however, be useful to include when otherwise unnecessary to help with readability.

Types can also be generic, like the Option type from before102:

struct MyWrapper<T> {
  foo: usize,
  bar: T,

The concrete type MyWrapper consists of replacing all occurrences of T in the definition with i32, which we can otherwise use as a normal type:

fn get_foo(mw: MyWrapper) -> usize {

Note that MyWrapper on its own is not a type.

Note that different generic instantiations are different types, with different layouts and sizes, which cannot be converted between each other in general.103

Unsurprisingly, we can combine generic functions with generic types.
In this case, we don’t really need to know that T = i32, so we factor it out.

fn get_foo<T>(mw: MyWrapper<T>) -> usize {

We can also build a generic function to extract the generic field:

fn get_bar<T>(mw: MyWrapper<T>) -> T {

Just like with lifetimes, impl blocks need to introduce type parameters before they are used:

impl<T> MyWrapper<T> {
  // ...

Generic Bounds

However, generics alone have one limitation: the function is only type and borrow checked once, in its generic form, rather than per instantiation; this means that generic code can’t just call inherent methods of T and expect the lookup to succeed104.
For example, this code won’t compile:

fn generic_add<T>(x: T, y: T) -> T {
  x + y

The error looks like this:

error[E0369]: cannot add `T` to `T`
 --> src/
2 |     x+y
  |     -^- T
  |     |
  |     T
  = note: T might need a bound for std::ops::Add

The compiler helpfully suggests that we need a “bound” of some sort.
Generic bounds are where traits really shine.

Add is a standard library trait, that looks something like the following:

trait Add<Rhs> {
  type Output;
  fn add(self, other: Rhs) -> Self::Output;

Not only is this trait generic, but it also defines an associated type, which allows implementations to choose the return type of the addition operation105.
Thus, for any types T and U, we can add them together if T implements Add; the return type of the operation is the type >::Output106.

Thus, our generic_add function should be rewritten into

fn generic_add<T: Add<T>>(x: T, y: T) -> T::Output {
  x + y

The T: Add part is a generic bound, asserting that this function can only compile when the chosen T implements Add.

If we want to ensure we return a T, we can change the bound to require that Output be T:

fn generic_add<T>(x: T, y: T) -> T
  where T: Add<T, Output=T>
  // ...

Note that this bound is included in a where clause, after the return type.
This is identical to placing it in the angle brackets, but is recommended for complicated bounds to keep them out of the way.
In-bracket bounds and where clauses are available for all other items that can have generic bounds, such as traits, impls, structs, and enums107.

Bound generics can be used to emulate all kinds of other behavior.
For example, the From and Into traits represent lossless conversions, so a function that wants any value that can be converted into MyType might look like

fn foo<T: Into<MyType>>(x: T) {
  // ...

You could then implement From on MyType for all T that can be converted into MyType.
When U implements From, a generic impl in the standard library causes T to implement Into.
At the call-site, this looks like an overloaded function108.

Bound generics can also be used to pass in constants. Imagine that we define a trait like

trait DriverId {
  const VALUE: u8;

This trait could then be implemented by various zero-sized types that exist only to be passed in as type parameters:

struct GpioDriverId;
impl DriverId for GpioDriverId {
  const VALUE: u8 = 0x4a;

Then, functions that need to accept a constant id for a driver can be written and called like this:

fn get_device_addr<Id: DriverId>() -> usize {
  // use Id::VALUE somehow ...
// ...

Types can also be bound by lifetimes.
The bound T: 'a says that every reference in T is longer than 'a; this kind of bound will be implicitly inserted whenever a generic &'a T is passed around.
Bounds may be combined: T: Clone + Default and T: Clone + 'a are both valid bounds.
Finally, lifetimes may be bound by other lifetimes: 'a: 'b means that the lifetime 'a is longer than 'b.

Phantom Data

The following is an error in Rust:

error[E0392]: parameter `T` is never used
 --> src/
2 | struct Foo<T>;
  |            ^ unused parameter
  = help: consider removing `T`, referring to it in a field,
    or using a marker such as `std::marker::PhantomData`

Rust requires that all lifetime and type parameters be used, since generating code to call destructors requires knowing if a particular type owns a T.
This is not always ideal, since it’s sometimes useful to expose a T in your type even though you don’t own one; we can work around this using the compiler’s suggestion: PhantomData.
For more information on how to use it, refer to the type documentation109 or the relevant Rustonomicon entry110.

Smart Pointers

In Rust, a “smart pointer”[^1112] is any type that implements std::ops::Deref111, the dereference operator112.
Deref is defined like this:

trait Deref {
  type Target;
  fn deref(&self) -> &Self::Target;

Types which implement Deref can also implement the mutable variant:

trait DerefMut: Deref {
  fn deref_mut(&mut self) -> &mut Self::Target;

Implementing the Deref trait gives a type T two features:

  • It can be dereferenced: *x becomes syntax sugar for *(x.deref()) or *(x.deref_mut()), depending on whether the resulting lvalue is assigned to.
  • It gains auto-deref: if is not a field or method of T, then it expands into x.deref().foo or x.deref_mut().foo, again depending on use.

Furthermore, deref and deref_mut are called by doing an explicit reborrow: &*x and &mut *x.

One example of a smart pointer is ManuallyDrop.
Even though this type contains a T directly (rather than through a reference), it’s still called a “smart pointer”, because it can be dereferenced to obtain the T inside, and methods of T can be called on it.
As we will see later, the RefCell type also produces smart pointers.
It is not uncommon for generic wrapper types, which restrict access to a value, to be smart pointers.

Note that, because Target is an associated type, a type can only dereference to one other type.

While not quite as relevant to smart pointers, the Index and IndexMut traits are analogous to the Deref and DerefMut traits, which enable the x[foo] subscript syntax.
Index looks like this:

trait Index<Idx> {
  type Output;
  fn index(&self, index: Idx) -> &Self::Output;

An indexing operation, much like a dereference operation, expands from x[idx] into *(x.index(idx)).
Note that indexing can be overloaded, and is a useful example of how this overloading through traits can be useful.
For example, <[u8] as Index>::Output is u8, while <[u8] as Index>::Output is [u8].
Indexing with a single index produces a single byte, while indexing with a range produces another slice.


Closures (sometimes called “lambda expressions” in other languages) are function literals that capture some portion of their environment, which can be passed into other functions to customize behavior.

Closures are not mere function pointers, because of this captured state.
The closest analogue to this in C is a function that takes a function pointer and some “context”.
For example, the Linux pthread_create() function takes a void* (*start_routine)(void*) argument and a void* arg argument.
arg represents state needed by start_routine to execute.
In a similar way, Rust closures need extra state to execute, except arg becomes part of the start_routine value.
Not only that, Rust will synthesize a bespoke context struct for arg, where normally the programmer would need to do this manually.
Rust makes this idiom much easier to use, and, as such, much more common.

As we’ll see, Rust has a number of different ABIs for closures, some of which closely resemble what pthread_create does; in some cases, the function pointer and its context can even be inlined.

In Rust, the syntax for creating a closure is |arg1, arg2| expr.
They can be very simple, like |(k, _)| k (which uses pattern-matching to extract the first element of a tuple) or complex, using a block expression to create a longer function: |foo| { /* ... */ }.
The types of arguments can be optionally specified as |foo: Foo| { /* ... */ }, and the return type as |foo| -> Bar { /* ... */ }, though in almost all cases type inference can figure them out correctly.
A closure that takes no arguments can be written as || /* ... */.

Closures capture their environment by reference; the mutable-ness of that reference is inferred from use.
For example:

let x = /* ... */;
let y = /* ... */;
let f = |arg| {
  x.do_thing(arg);  // Takes &self, so this implicitly produces a shared reference.
  y.do_mut_thing(arg);  // Takes &mut self, so it takes a unique reference instead.
// Note: f holds a unique borrow of y.
let z = &mut y;  // Error!

Above, f captures x by shared reference and y by unique reference.
The actual closure value f is a synthetic struct containing the captures:

struct MyClosure<'a> {
  x: &'a X,
  y: &'a mut Y,

Calling a closure, like f(), calls a synthetic function that takes MyClosure as its first argument.
It’s possible to instead capture by moving into the closure; this can be done with the move |arg| { /* ... */ } syntax.
If it were applied to f, MyClosure would become

struct MyClosure<'a> {
  x: X,
  y: Y,

Rust does not cleanly support mixing capture-by-move and capture-by-reference, but it is possible to mix them by capturing references by move:

let x = /* ... */;
let y = /* ... */;
let x_ref = &x;
let f = move |arg| {
  x_ref.do_thing(arg);  // Capture x_ref by move, aka capture x by shared ref.
  y.do_mut_thing(arg);  // Capture y by move.

The distinction between capture-by-move and capture-by-ref is mostly irrelevant for Copy types.

To be polymorphic over different closure types, we use the special Fn, FnMut, and FnOnce traits.
These represent functions that can be called by shared reference, unique reference, or by move.
Closures that only capture shared references implement all three; closures that capture by unique reference implement only the latter two, and closures that capture by move implement only the last one113.
Function pointers, function items114, and closures that don’t capture also implement all three, and can all be converted to function pointers.

These traits use special syntax similar to function pointers115.
For example, Fn(i32) -> i32 represents taking an i32 argument and returning another i32.
Closures also implement Copy and Clone if all of their captures do, too.

Closures as Function Arguments

There are roughly two ways of writing a function that accepts a closure argument: through dynamic dispatch, or through static dispatch, which have a performance and a size penalty, respectively.

Fn and FnMut closures can be accepted using trait objects:

fn my_do_thing(func: &dyn Fn(i32) -> i32) -> i32 {

This is completely identical to the C approach: the synthetic function lives in the trait object vtable, while the captures are behind the actual trait object pointer itself.
In other words:

struct DynClosure {
  vtable: *mut Vtable,
  captures: *mut Captures,

Of course, the vtable call has a performance penalty, but avoids the code size overhead of generic instantiation.

Using generics allows passing in closures implementing Fn, FnMut, or FnOnce, by specializing the called function for each function type:

fn my_do_thing<F: Fn(i32) -> i32>(func: F) -> i32 {

This will translate to a direct call to the synthetic closure function with no overhead, but will duplicate the function for each closure passed in, which can result in a big size hit if used on large functions.

It is possible to use a shorthand for declaring this type of function, that avoids having to declare a type parameter:

fn my_do_thing(func: impl Fn(i32) -> i32) -> i32 { /* ... */ }

The pseudotype impl Trait can be used in function argument position to say “this parameter can be of any type that implements Trait”, which effectively declares an anonymous generic parameter.
Note that Trait can technically be any generic bound involving at least one trait: impl Clone + Default and impl Clone + 'a are valid.

Closures as Function Returns

Closure types are generally unnameable. The canonical way to return closures is with impl Trait in return position:

fn new_fn() -> impl Fn(i32) -> i32 {
  |x| x * x

Return-position impl Trait means “this function returns some unspecified type that implements Trait”.
Callers of the function are not able to use the actual type, only functions provided through Trait.
impl Trait can also be used to hide implementation details, when a returned value only exists to implement some trait.

Return-position impl Trait has a major caveat: it cannot return multiple types that implement the trait.
For example, the following code is a type error: