A Harsh companion to The Rustonomicon — the dark arts of unsafe Rust, in the same order, with every program written in Harsh and built and run to produce the output you see.
The Rustonomicon is the book for the moment safe Rust is not enough: when
you write a data structure the borrow checker cannot follow, call into C, or
need to know exactly how a value sits in memory. It explains what unsafe
allows, what it does not, and the contracts you take on when you use it.
Harsh is Rust with another layout, and unsafe in Harsh is unsafe in Rust:
the same rules, the same undefined behaviour, the same compiler checking what
it can. Nothing in this book is a Harsh rule. What the companion adds is how
unsafe code reads in Harsh — unsafe: opening a block like any keyword, raw
pointers used with <- like any value — in programs you can run, each short,
each showing one rule at work.
Its chapters and sections are numbered as the original's, so a section here and its original share a number. This companion is shorter than its original, on purpose. Much of The Rustonomicon's value is its careful explanation of why each rule exists; each page here states the rule in a few sentences of its own and links to the original section. Before you write unsafe code for real, read the original.
Every program in this book is well-defined: none relies on undefined behaviour, even to illustrate it. Where a page is about something that must not be done, it shows the compiler refusing it, or the safe form.
The Rustonomicon is by the Rust Project developers, licensed under the Apache License 2.0; this companion is under the Mozilla Public License 2.0, as Harsh is. See ATTRIBUTION.md.
Meet Safe and Unsafe
Safe Rust guarantees there is no undefined behaviour: no dangling reference, no data race, no read of uninitialised memory. Unsafe Rust is the same language with a few more abilities, whose correctness the compiler can no longer check — you promise it instead. The original chapter: Meet Safe and Unsafe.
1.1 How Safe and Unsafe Interact
Original: How Safe and Unsafe Interact
unsafe marks a contract. On a function, it says the caller must uphold
conditions the compiler cannot check; on a block, it says you have checked
them. On a trait, it says implementers must uphold an invariant; on an
impl, that you have. Safe code must never be able to cause undefined
behaviour, however it calls a safe function — that is the safe function's
author's job.
// A safe function around an unsafe operation: the check makes it sound.
fn get (xs: &[i32]) (i: usize) -> Option<i32>:
if i < xs <- len$:
// Sound: `i` was checked against the length just above.
Some (unsafe: *xs <- get_unchecked i)
else:
None
// An unsafe function: the caller promises what the function cannot check.
unsafe fn first_unchecked (xs: &[i32]) -> i32:
*xs <- get_unchecked 0
fn main$:
let xs = [10, 20, 30]
println! "{:?} {:?}" (get (&xs) 1) (get (&xs) 9)
// We promise: `xs` is not empty.
println! "{}" (unsafe: first_unchecked (&xs))
Some(20) None
10
In Harsh: unsafe fn is declared like any function; unsafe: opens a
block, and a short one fits on its line: unsafe: *xs <- get_unchecked i.
1.2 What Unsafe Can Do
Original: What Unsafe Can Do
Only a few things: dereference a raw pointer; call an unsafe function
(including C functions); implement an unsafe trait; access or modify a
static mut; access a union's fields. Everything else — the borrow
checker, the type system — still applies inside unsafe. What unsafe code
must never do is the original's other list, of undefined behaviours: a
dangling or unaligned dereference, breaking the aliasing rules, a data race,
producing an invalid value (a bool that is not 0 or 1, an enum with no
such variant), among others.
static mut COUNTER: u32 = 0
union IntOrFloat
i: u32
f: f32
fn main$:
// 1. Dereference a raw pointer.
let x = 5
let p = &x as *const i32
println! "{}" (unsafe: *p)
// 2. Call an unsafe function.
let v = vec! 1 2 3
println! "{}" (unsafe: *v <- get_unchecked 2)
// 3. Access a `static mut` (one thread only here).
unsafe:
COUNTER += 1
println! "{}" COUNTER
// 4. Read a union's field.
let u = IntOrFloat\ f = 1.0
println! "{:#x}" (unsafe: u <- i)
5
3
1
0x3f800000
In Harsh: each power reads as in Rust; the raw-pointer dereference is
*p, isolated in parentheses where it is an argument, (*p). Mind its
precedence: a prefix operator takes the whole chain after it, so *p <- field
dereferences the field; to reach through the pointer, write (*p) <-
field. The Book's appendix Precedence has the full table.
1.3 Working with Unsafe
Original: Working with Unsafe
Unsafe code depends on invariants that safe code around it can break: a
Vec's safe method that changes len wrongly is as dangerous as the unsafe
read that trusts it. So the boundary of safety is the module: keep the
fields that unsafe code relies on private, and let only the module's own
code touch them.
mod stack
// `len` is private: only this module's code can change it, so the
// unsafe read below can trust it.
pub struct Stack
items: [i32; 8]
len: usize
impl Stack
pub fn new$ -> Self:
Self\ items = [0; 8], len = 0
pub fn push (&mut self) (x: i32) -> bool:
if self <- len == 8:
return false
self <- items[self <- len] = x
self <- len += 1
true
pub fn top (&self) -> Option<i32>:
if self <- len == 0:
None
else:
// Sound because `len` is always <= 8, which only this
// module can break.
Some (unsafe: *self <- items <- get_unchecked (self <- len - 1))
fn main$:
let mut s = stack.Stack.new$
s <- push 1
s <- push 2
println! "{:?}" (s <- top$)
Some(2)
In Harsh: the module and its private fields are the same mod and the
same absence of pub.
Data Layout
How values sit in memory: their size, their alignment, the order of their fields. Unsafe code that reads or writes memory directly depends on it. The original chapter: Data Representation.
2.1 repr(Rust)
Original: repr(Rust)
Every type has a size and an alignment. By default, Rust may reorder a struct's fields to reduce padding, and promises nothing else about their order: two structs with the same fields may be laid out differently.
use std.mem
// Written loosely: a byte, a u32, a byte.
struct Loose
a: u8
b: u32
c: u8
// The same fields, in C's order.
#[repr C]
struct InOrder
a: u8
b: u32
c: u8
fn main$:
// Rust may reorder the fields to pack them; C's order pads.
println!
"Loose: size {} align {}"
(mem.size_of.<Loose>$)
(mem.align_of.<Loose>$)
println!
"InOrder: size {} align {}"
(mem.size_of.<InOrder>$)
(mem.align_of.<InOrder>$)
Loose: size 8 align 4
InOrder: size 12 align 4
In Harsh: mem.size_of<Loose>$ — the generic list follows the name, and
the transpiler writes Rust's turbofish.
2.2 Exotically Sized Types
Original: Exotically Sized Types
Not every type has a known, non-zero size. Dynamically sized types — [T],
str, dyn Trait — live behind pointers that carry their length or vtable,
so those pointers are twice as wide. Zero-sized types take no space at all;
empty types, like an enum with no variants, cannot even be built.
use std.mem
struct Nothing
enum Void
trait Shape
fn area (&self) -> f64
fn main$:
// Zero-sized: no space at all.
println! "() {}" (mem.size_of.<()>$)
println! "Nothing {}" (mem.size_of.<Nothing>$)
println! "Void {}" (mem.size_of.<Void>$)
// A pointer to a dynamically sized type carries its length or vtable.
println! "&u8 {}" (mem.size_of.<&u8>$)
println! "&[u8] {}" (mem.size_of.<&[u8]>$)
println! "&str {}" (mem.size_of.<&str>$)
println! "&dyn Shape {}" (mem.size_of.<&dyn Shape>$)
() 0
Nothing 0
Void 0
&u8 8
&[u8] 16
&str 16
&dyn Shape 16
In Harsh: an empty enum is its header alone, enum Void, like any
declaration with nothing beneath.
2.3 Other reprs
Original: Other reprs
#[repr(C)] lays out fields in order, as C does — required for FFI.
#[repr(u8)] (and the other integers) fixes an enum's discriminant.
#[repr(transparent)] makes a one-field wrapper laid out exactly as its
field. #[repr(packed)] (or packed(n)) removes padding, at the cost of
unaligned fields; #[repr(align(n))] raises a type's alignment.
use std.mem
// A fixed discriminant, one byte.
#[repr u8]
#[derive Clone Copy Debug]
enum Color
Red = 1
Green = 2
// A wrapper laid out exactly as its field.
#[repr transparent]
struct Meters f64
// No padding: the u32 may be unaligned.
#[repr C packed]
struct Packed
a: u8
b: u32
fn main$:
println! "{} {}" (mem.size_of.<Color>$) (Color.Green as u8)
println! "{} {}" (mem.size_of.<Meters>$) (mem.size_of.<f64>$)
println! "{}" (mem.size_of.<Packed>$)
1 2
8 8
5
In Harsh: attribute arguments juxtapose, #[repr C], #[repr u8],
#[repr transparent].
Ownership
The rules the borrow checker enforces — and why unsafe code must uphold them too, since the compiler optimises on the assumption that they hold. The original chapter: Ownership.
3.1 References
Original: References
Two kinds: a shared reference & and a mutable reference &mut. Two rules
govern them: a reference cannot outlive what it refers to, and a mutable
reference cannot be aliased — while it is alive, nothing else reaches the
same value. The compiler refuses a second &mut:
fn main$:
let mut total = 0
let a = &mut total
// Refused: `total` is already borrowed mutably by `a`.
let b = &mut total
*a += 1
*b += 1
error[E0499]: cannot borrow `total` as mutable more than once at a time
--> references.hrs:5:13
|
3 | let a = &mut total
| ---------- first mutable borrow occurs here
5 | let b = &mut total
| ^^^^^^^^^^ second mutable borrow occurs here
6 | *a += 1
| ------- first borrow later used here
In Harsh: the refusal points at the .hrs lines of both borrows.
3.2 Aliasing
Original: Aliasing
Why the rule matters: because a &mut is never aliased, the compiler may
assume a write through it changes nothing else, and a read through a &
sees a value that cannot change underneath it — and optimise accordingly.
Unsafe code that creates two live &mut to one place breaks that assumption:
undefined behaviour, whether or not it ever writes through both.
fn main$:
let mut xs = [1, 2, 3, 4, 5, 6]
// Two `&mut` into one array -- to disjoint halves, so sound.
let (left, right) = xs <- split_at_mut 3
left[0] = 10
right[0] = 40
println! "{:?}" xs
[10, 2, 3, 40, 5, 6]
In Harsh: split_at_mut is the standard way to get two &mut into one
slice — to disjoint halves, which is what makes it sound.
3.3 Lifetimes
Original: Lifetimes
A lifetime is the region of code for which a reference must stay valid. Within a function the compiler infers them; across a function's signature you name them, saying which inputs an output borrows from.
// The output borrows from both inputs: it lives as long as the shorter.
fn longest<'a> (x: &'a str) (y: &'a str) -> &'a str:
if x <- len$ >= y <- len$: x else: y
fn main$:
let a = String.from "a long string"
let result = do:
let b = String.from "short"
longest (&a) (&b) <- to_string$
println! "{result}"
a long string
In Harsh: lifetimes are written as in Rust, fn longest<'a> (x: &'a str)
(y: &'a str) -> &'a str.
3.4 Limits of Lifetimes
Original: Limits of Lifetimes
The borrow checker is conservative: some correct programs are refused, because it reasons about a borrow's whole scope rather than every path. The classic case: a borrow returned on one path of a function stays alive on the others.
use std.collections.HashMap
// Correct, yet refused: the borrow returned on the first path is taken to
// last for the whole function, so the insert conflicts with it.
fn get_default<'m> (map: &'m mut HashMap<i32, String>) (key: i32) -> &'m mut String:
match map <- get_mut (&key)\
Some value => value
None =>
let _ = map <- insert key (String.new$)
map <- get_mut (&key) <- unwrap$
fn main$:
let mut m = HashMap.new$
println! "{}" (get_default (&mut m) 1)
error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> limits.hrs:9:21
|
5 | fn get_default<'m> (map: &'m mut HashMap<i32, String>) (key: i32) -> &'m mut String:
| -- lifetime `'m` defined here
6 | match map <- get_mut (&key)\
| ---------------------------- returning this value requires that `*map` is borrowed for `'m`
| --- first mutable borrow occurs here
9 | let _ = map <- insert key (String.new$)
| ^^^ second mutable borrow occurs here
error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> limits.hrs:10:13
|
5 | fn get_default<'m> (map: &'m mut HashMap<i32, String>) (key: i32) -> &'m mut String:
| -- lifetime `'m` defined here
6 | match map <- get_mut (&key)\
| ---------------------------- returning this value requires that `*map` is borrowed for `'m`
| --- first mutable borrow occurs here
10 | map <- get_mut (&key) <- unwrap$
| ^^^ second mutable borrow occurs here
In Harsh: rustc's refusal points at the .hrs lines; the usual way
around it is to look up twice, as the next program does.
use std.collections.HashMap
// Looking up twice: no borrow outlives a path.
fn get_default<'m> (map: &'m mut HashMap<i32, String>) (key: i32) -> &'m mut String:
if !map <- contains_key (&key):
let _ = map <- insert key (String.from "default")
map <- get_mut (&key) <- unwrap$
fn main$:
let mut m = HashMap.new$
println! "{}" (get_default (&mut m) 1)
default
3.5 Lifetime Elision
Original: Lifetime Elision
Most signatures need no lifetime names: with one reference input, the output
borrows from it; with &self, the output borrows from self. The compiler
fills them in by these rules; you write them only when the rules do not
decide.
struct Doc
text: String
impl Doc
// Elided: the output borrows from `self`.
fn first_word (&self) -> &str:
self <- text
<- split_whitespace$
<- next$
<- unwrap_or ""
// The same signature with its lifetimes written.
fn first_word_explicit<'a> (&'a self) -> &'a str:
self <- text
<- split_whitespace$
<- next$
<- unwrap_or ""
fn main$:
let d = Doc\ text = String.from "hello harsh world"
println! "{} {}" (d <- first_word$) (d <- first_word_explicit$)
hello hello
In Harsh: the elided and the explicit forms side by side, identical to the compiler.
3.6 Unbounded Lifetimes
Original: Unbounded Lifetimes
Dereferencing a raw pointer produces a reference whose lifetime nothing
constrains — it becomes whatever the context asks for, even 'static. Bound it
at once, by returning it from a function whose signature ties it to an input.
struct Owner
value: i32
// The raw pointer's reference is bounded by the signature: it lives no
// longer than `owner`.
fn get<'a> (owner: &'a Owner) -> &'a i32:
let p: *const i32 = &owner <- value
unsafe: &*p
fn main$:
let o = Owner\ value = 42
println! "{}" (get (&o))
42
In Harsh: the signature does the bounding, fn get<'a> (owner: &'a Owner)
-> &'a i32; the unsafe line inside stays one line.
3.7 Higher-Rank Trait Bounds
Original: Higher-Rank Trait Bounds
A closure taking a reference must work for every lifetime its caller might
pass, not one chosen in advance: for<'a> Fn(&'a T) -> &'a U. Usually this is
implied by the Fn(&T) -> &U sugar, which is why it is rarely written.
// The closure must work for every lifetime of the references it is given:
// a `for<'a>` bound, implied by the sugar.
fn apply_all (f: impl Fn (&str) -> usize) (words: &[String]) -> Vec<usize>:
words <- iter$
<- map (|w| f w)
<- collect$
fn main$:
let words = vec! (String.from "harsh") (String.from "rust")
println! "{:?}" (apply_all (|w| w <- len$) (&words))
[5, 4]
In Harsh: the sugar, impl Fn (&str) -> usize, carries the for<'a> as
in Rust.
3.8 Subtyping and Variance
Original: Subtyping and Variance
A longer lifetime can stand in for a shorter one: &'static str is usable
wherever a &'a str is expected. How that extends through a type is its
variance: &'a T is covariant in 'a and T; &'a mut T is invariant in
T; fn(T) is contravariant in T. Getting variance wrong in unsafe code —
through a raw-pointer field, say — lets a short lifetime be stretched.
// Expects references that live for some `'a`...
fn pick<'a> (a: &'a str) (b: &'a str) (first: bool) -> &'a str:
if first: a else: b
fn main$:
let owned = String.from "local"
// ...and a `&'static str` stands in: 'static outlives 'a (covariance).
let s: &'static str = "static"
println! "{} {}" (pick s (&owned) true) (pick s (&owned) false)
static local
In Harsh: nothing to change; variance is the type system's.
3.9 Drop Check
Original: Drop Check
A value with a destructor may use its borrows when it is dropped, so those
borrows must strictly outlive it. The compiler checks this — drop check —
and refuses a program where a borrowed value would be dropped first. The
standard library escapes this check for its collections with an unstable
attribute, #[may_dangle], promising their destructors only drop what they
hold.
struct Inspector<'a>
seen: &'a String
impl<'a> Drop for Inspector<'a>
fn drop (&mut self):
// The destructor uses the borrow: it must still be valid.
println! "inspecting {}" (self <- seen)
fn main$:
let inspector
let value = String.from "treasure"
// Refused: `value` is dropped before `inspector`, whose drop reads it.
inspector = Inspector\ seen = &value
println! "{}" (inspector <- seen)
error[E0597]: `value` does not live long enough
--> dropck.hrs:13:35
|
11 | let value = String.from "treasure"
| ----- binding `value` declared here
13 | inspector = Inspector\ seen = &value
| ^^^^^^ borrowed value does not live long enough
= note: values in a scope are dropped in the opposite order they are defined
In Harsh: declaring the borrowed value first, so it is dropped last, is the fix, as in Rust.
3.10 PhantomData
Original: PhantomData
A struct holding only raw pointers tells the compiler nothing about what it
owns or borrows. PhantomData<T> — a zero-sized field — says "act as if this
struct held a T", restoring the right variance, drop checking and auto
traits.
use std.marker.PhantomData
// Only a raw pointer inside: PhantomData says "this borrows a T for 'a".
struct Slice<'a, T>
start: *const T
len: usize
marker: PhantomData<&'a T>
impl<'a, T: Copy> Slice<'a, T>
fn new (xs: &'a [T]) -> Self:
Self\
start = xs <- as_ptr$
len = xs <- len$
marker = PhantomData
fn get (&self) (i: usize) -> Option<T>:
if i < self <- len:
// Sound: in bounds, and PhantomData keeps `xs` borrowed.
Some (unsafe: *self <- start <- add i)
else:
None
fn main$:
let data = vec! 1 2 3
let s = Slice.new (&data)
println! "{:?} {:?}" (s <- get 2) (s <- get 3)
Some(3) None
In Harsh: PhantomData is a value like any unit struct: marker =
PhantomData in the literal.
3.11 Splitting Borrows
Original: Splitting Borrows
The borrow checker understands a struct's fields as separate places — two
&mut to two fields are fine — but not a slice's halves, which is why
split_at_mut exists, with a little unsafe code inside.
struct Point
x: i32
y: i32
fn bump (a: &mut i32) (b: &mut i32):
*a += 1
*b += 10
fn main$:
let mut p = Point\ x = 0, y = 0
// Two fields: two disjoint places, two `&mut` at once.
bump (&mut p <- x) (&mut p <- y)
println! "{} {}" (p <- x) (p <- y)
1 10
In Harsh: field access is <-, so &mut p <- x and &mut p <- y are the
two disjoint borrows.
Type Conversions
The ways a value changes type: implicitly, with as, or by reinterpreting
its bits. The original chapter: Type Conversions.
4.1 Coercions
Original: Coercions
Some conversions happen on their own, at specific places: &mut T to &T,
&String to &str through Deref, an array to a slice, a concrete type to
dyn Trait. They are never applied to satisfy a trait bound.
use std.fmt.Display
fn count (s: &str) -> usize:
s <- len$
fn total (xs: &[i32]) -> i32:
xs <- iter$ <- sum$
fn main$:
let owned = String.from "harsh"
// &String to &str, through Deref.
println! "{}" (count (&owned))
// An array to a slice.
println! "{}" (total (&[1, 2, 3]))
// A concrete type to a trait object.
let shown: &dyn Display = &42
println! "{shown}"
5
6
42
In Harsh: the same places, the same conversions.
4.2 The Dot Operator
Original: The Dot Operator
A method call finds its method by trying the receiver, then &receiver,
then &mut receiver, then dereferencing and trying again. This is
convenient and occasionally surprising: x.clone() on a &T where T is not
Clone clones the reference.
#[derive Clone Debug]
struct Named
name: String
struct NotClone
fn main$:
let n = Named\ name = String.from "a"
let r = &n
// `Named` is Clone: r <- clone$ is a Named.
let copy: Named = r <- clone$
println! "{:?}" copy
// `NotClone` is not: r2 <- clone$ clones the reference itself.
let x = NotClone
let r2 = &x
let also_ref: &NotClone = r2 <- clone$
println! "{}" (std.ptr.eq r2 also_ref)
Named { name: "a" }
true
In Harsh: <- is the dot and follows the same search. Writing the trait's
function, T.clone x, says which one you mean — found while writing Harsh
Design Patterns, where it mattered.
4.3 Casts
Original: Casts
as converts between primitive types explicitly. Numeric casts truncate or
sign-extend; a float to an integer saturates (and NaN becomes 0); pointer
casts change the type the pointer claims. None of them is undefined
behaviour — but a pointer cast is only as good as what it points to.
fn main$:
// Integer to a smaller integer: truncates. (A literal that does not fit
// is refused outright, by the `overflowing_literals` lint.)
let big: i32 = 300
println! "{}" (big as u8)
// Signed to unsigned: the same bits.
println! "{}" (-1i32 as u32)
// Float to integer: saturates; NaN becomes 0.
println! "{} {} {}" (3.9f64 as i32) (1e20f64 as i32) (f64.NAN as i32)
// A reference to a raw pointer, and to an integer address.
let x = 7
let p = &x as *const i32
println! "{}" ((p as usize) % (std.mem.align_of.<i32>$))
44
4294967295
3 2147483647 0
0
In Harsh: as binds as in Rust, (big as u8).
4.4 Transmutes
Original: Transmutes
mem::transmute reinterprets the bits of one type as another of the same
size. It is the most dangerous tool there is: almost every use has a safer
alternative (f32::to_bits, from_ne_bytes, pointer casts), which the next
program compares it with.
fn main$:
let x = 1.5f32
// The dangerous tool...
let bits: u32 = unsafe: std.mem.transmute x
// ...and the safe one that does the same.
println! "{:#x} {:#x}" bits (x <- to_bits$)
let bytes = 0x12345678u32 <- to_be_bytes$
println! "{:x?}" bytes
println! "{:#x}" (u32.from_be_bytes bytes)
0x3fc00000 0x3fc00000
[12, 34, 56, 78]
0x12345678
In Harsh: mem.transmute<f32, u32> x would need the turbofish before an
argument, which the transpiler does not yet write — one more reason to reach
for to_bits. The program names the types in a binding instead.
Uninitialized Memory
Memory that has been allocated but not yet given a value. Reading it is undefined behaviour; safe Rust makes that impossible, and unsafe code must keep it so. The original chapter: Working With Uninitialized Memory.
5.1 Checked
Original: Checked
A variable may be declared before it is given a value, but the compiler tracks every path and refuses a read that might come first.
fn main$:
let x: i32
let ready = std.env.args$ <- count$ > 5
if ready:
x = 1
// Refused: on the other path, `x` was never given a value.
println! "{x}"
error[E0381]: used binding `x` is possibly-uninitialized
--> checked.hrs:7:16
|
2 | let x: i32
| - binding declared here but left uninitialized
5 | x = 1
| ----- binding initialized here in some conditions
7 | println! "{x}"
| ^ `x` used here but it is possibly-uninitialized
In Harsh: the error points at the .hrs line of the read.
5.2 Drop Flags
Original: Drop Flags
When a value is initialised on some paths only, or moved out on some, the compiler keeps a hidden drop flag to know at run time whether to drop it.
struct Loud
name: &'static str
impl Drop for Loud
fn drop (&mut self):
println! "drop {}" (self <- name)
fn main$:
let decide = std.env.args$ <- count$ < 5
let a
if decide:
a = Loud\ name = "a"
let b = Loud\ name = "b"
let c = Loud\ name = "c"
// `c` moved out on this path only: its drop flag decides at the end.
if decide:
drop c
println! "end of main"
let _keep = &b
drop c
end of main
drop b
drop a
In Harsh: the same; the output shows which values were dropped, and when.
5.3 Unchecked
Original: Unchecked
To initialise memory piece by piece — an array element by element, a buffer
from C — use MaybeUninit<T>: it holds possibly-uninitialised memory, and
assume_init is your promise that every part is now valid.
use std.mem.MaybeUninit
fn main$:
// Memory for four Strings, not yet any String in it.
let mut slots: [MaybeUninit<String>; 4] = std.array.from_fn (|_| MaybeUninit.uninit$)
for (i, slot) in slots <- iter_mut$ <- enumerate$:
let _ = slot <- write (format! "item {i}")
// Our promise: every slot was written.
let items: [String; 4] = slots <- map (|s| unsafe: s <- assume_init$)
println! "{:?}" items
["item 0", "item 1", "item 2", "item 3"]
In Harsh: each write is unsafe:-free here, since MaybeUninit.write
is safe; only the final assume_init needs the promise.
Ownership Based Resource Management
A value owns its resources — memory, files, locks — and releases them when it is dropped. It is the pattern everything else in Rust rests on. The original chapter: OBRM.
6.1 Constructors
Original: Constructors
There is exactly one way to create a value: name its type and give every
field. new functions, Default, Clone are ordinary code built on that one
way; nothing runs implicitly — no copy constructor, no move constructor. A
move is always a plain copy of the bytes, after which the source is no longer
used.
There is no program on this page: there is nothing hidden to show.
6.2 Destructors
Original: Destructors
Drop::drop runs when a value goes out of scope, then its fields are dropped
in declaration order; local variables are dropped in reverse order of
declaration. mem::forget and ManuallyDrop stop a destructor from running.
use std.mem.ManuallyDrop
struct Loud
name: &'static str
impl Drop for Loud
fn drop (&mut self):
println! "drop {}" (self <- name)
struct Pair
first: Loud
second: Loud
fn main$:
let _a = Loud\ name = "local a"
let _b = Loud\ name = "local b"
let _p =
Pair\
first = Loud\ name = "field first"
second = Loud\ name = "field second"
// Never dropped: forgotten, and kept from dropping.
std.mem.forget (Loud\ name = "forgotten")
let _m = ManuallyDrop.new (Loud\ name = "manual")
println! "end of main"
end of main
drop field first
drop field second
drop local b
drop local a
In Harsh: the order of the output is the order the rules predict.
6.3 Leaking
Original: Leaking
Leaking is safe: mem::forget does it on purpose, and two Rcs pointing at
each other do it by accident. So unsafe code must never rely on a destructor
running for soundness — only for tidiness. The original's examples show what
that costs: Vec::drain sets the length to zero before it starts, so a
forgotten Drain leaks rather than exposes moved-out elements; and the early
thread::scoped API was removed because a forgotten guard let a thread
outlive what it borrowed — which thread::scope, used in this book, avoids.
use std.cell.RefCell
use std.rc.( Rc, Weak)
struct Node
next: RefCell<Option<Rc<Node>>>
back: RefCell<Weak<Node>>
fn main$:
// A cycle of strong pointers: neither count ever reaches zero.
let a = Rc.new (Node\ next = RefCell.new None, back = RefCell.new (Weak.new$))
let b = Rc.new (Node\ next = RefCell.new (Some (a <- clone$)), back = RefCell.new (Weak.new$))
*a <- next <- borrow_mut$ = Some (b <- clone$)
println!
"strong: a {} b {} -- a leak when both go"
(Rc.strong_count (&a))
(Rc.strong_count (&b))
// Break it: a Weak pointer back does not keep its target alive.
*a <- next <- borrow_mut$ = None
*a <- back <- borrow_mut$ = Rc.downgrade (&b)
println!
"strong: a {} b {}"
(Rc.strong_count (&a))
(Rc.strong_count (&b))
strong: a 2 b 2 -- a leak when both go
strong: a 2 b 1
In Harsh: Weak breaks the cycle; the counts show it.
Unwinding
A panic unwinds the stack, running destructors as it goes. Unsafe code in the middle of changing an invariant must leave things sound if a panic interrupts it. The original chapter: Unwinding.
7.1 Exception Safety
Original: Exception Safety
Code that calls something which may panic must be exception safe: when the
panic unwinds through it, every invariant unsafe code depends on must still
hold. The usual tool is a guard whose destructor restores the invariant — as
the standard BinaryHeap::sift_up does with a hole that is always filled
back in, even if a comparison panics.
use std.panic
struct Restore<'a>
flag: &'a mut bool
impl<'a> Drop for Restore<'a>
fn drop (&mut self):
// Runs during the unwind too: the invariant comes back.
*self <- flag = true
println! "invariant restored"
fn risky (flag: &mut bool):
*flag = false
let _guard = Restore\ flag = flag
panic! "interrupted in the middle"
fn main$:
// No message on stderr for the expected panic.
panic.set_hook (Box.new (|_| ()))
let mut consistent = true
let result = panic.catch_unwind (panic.AssertUnwindSafe (|| risky (&mut consistent)))
println!
"panicked: {}, consistent: {}"
(result <- is_err$)
consistent
invariant restored
panicked: true, consistent: true
In Harsh: the guard is a struct with a Drop; the output shows it
running during the unwind.
7.2 Poisoning
Original: Poisoning
A Mutex whose holder panicked is poisoned: its data may be half-updated,
so the next lock returns an error. The data is still reachable, for code
that knows how to check it.
use std.sync.( Arc, Mutex)
use std.thread
fn main$:
std.panic.set_hook (Box.new (|_| ()))
let data = Arc.new (Mutex.new (vec! 1 2 3))
let d = data <- clone$
let _ =
thread.spawn (
move ||:
let mut v = d <- lock$ <- unwrap$
v <- push 4
panic! "the holder panicked"
) <- join$
println! "poisoned: {}" (data <- is_poisoned$)
let result = data <- lock$
let shown =
match result\
Ok v => format! "ok {:?}" (*v)
Err poisoned =>
format! "recovered {:?}" (*poisoned <- into_inner$)
println! "{shown}"
poisoned: true
recovered [1, 2, 3, 4]
In Harsh: the panicking thread's closure is a move ||: block; lock's
result is matched like any other.
Concurrency
Safe Rust makes data races impossible; it does not make every race impossible. Unsafe code that shares memory between threads must uphold the same guarantee. The original chapter: Concurrency and Parallelism.
8.1 Races
Original: Races
A data race — two threads touching the same memory unsynchronised, at least one writing — is undefined behaviour, and safe Rust prevents it. A race condition — a result depending on timing — is only a bug, and safe Rust allows it. Atomics and locks are how threads share memory without a data race.
use std.sync.atomic.( AtomicUsize, Ordering)
use std.thread
fn main$:
let counter = AtomicUsize.new 0
thread.scope (|s|:
for _ in 0..4:
let _ = s <- spawn (||:
for _ in 0..1000:
let _ = counter <- fetch_add 1 Ordering.Relaxed
)
)
// Four threads, no data race: always 4000.
println! "{}" (counter <- load Ordering.Relaxed)
4000
In Harsh: the threads are spawned in a scope, so they may borrow the
counter; each adds with an atomic fetch_add.
8.2 Send and Sync
Original: Send and Sync
Send: a value may move to another thread. Sync: a reference to it may be
shared between threads. The compiler derives both from a type's fields; raw
pointers are neither, so a type built on them states its own, with an
unsafe impl — a promise.
use std.thread
// A raw pointer is neither Send nor Sync: the wrapper promises it is safe to
// send, because the data outlives the thread and nothing else touches it.
struct Pointer (*mut i32)
unsafe impl Send for Pointer
fn main$:
let mut value = 41
let p = Pointer (&mut value as *mut i32)
thread.scope (|s|:
let _ = s <- spawn (move ||:
let p = p
unsafe:
*p.0 += 1
)
)
println! "{value}"
42
In Harsh: unsafe impl Send for Pointer is one line with no body.
8.3 Atomics
Original: Atomics
Atomics carry an ordering: Relaxed orders nothing but the atomic itself;
Release on a store and Acquire on the load that sees it make everything
written before the store visible after the load; SeqCst adds a single total
order. Most synchronisation is a Release/Acquire pair.
use std.sync.atomic.( AtomicBool, AtomicU64, Ordering)
use std.thread
static DATA: AtomicU64 = AtomicU64.new 0
static READY: AtomicBool = AtomicBool.new false
fn main$:
thread.scope (|s|:
let _ = s <- spawn (||:
DATA <- store 42 Ordering.Relaxed
// Release: everything before this store is visible to the
// thread whose Acquire load sees it.
READY <- store true Ordering.Release
)
let _ = s <- spawn (||:
while !READY <- load Ordering.Acquire:
std.hint.spin_loop$
println! "data = {}" (DATA <- load Ordering.Relaxed)
)
)
data = 42
In Harsh: Ordering.Release, Ordering.Acquire — paths with dots.
Implementing Vec
Original: Implementing Vec
The original builds Vec in eleven steps. This companion follows them as
sections, each saying what the step does and where it is in one program —
given in full under Final Code — which covers the core in Harsh. Three steps
(IntoIter, RawVec, Drain) and zero-sized types add no new Harsh, only
more of the same care; for them, read the original.
9.1 Layout
Original: Layout
A pointer, a capacity and a length. The pointer is NonNull<T>, never null —
so Option<MyVec<T>> costs nothing extra — and the type owns its Ts, so it
is Send and Sync when they are: two unsafe impl lines.
9.2 Allocating
Original: Allocating
Nothing is allocated until the first push: the pointer is NonNull::dangling.
grow allocates, or reallocates to twice the capacity, with a Layout built
from the element's size and alignment, and aborts through
handle_alloc_error if the allocator fails.
9.3 Push and Pop
Original: Push and Pop
push writes into the slot past the end (ptr::write: the slot holds no
value to drop); pop reads the last element out (ptr::read), and the slot
is uninitialised again, past len.
9.4 Deallocating
Original: Deallocating
Drop drops the len elements in place, then frees the allocation with the
same layout it was made with.
9.5 Deref
Original: Deref
Deref and DerefMut to [T], through slice::from_raw_parts: every slice
method — sort, iter, indexing — becomes the vector's.
9.6 Insert and Remove
Original: Insert and Remove
ptr::copy shifts the tail by one — memmove, since the ranges overlap — to
open a gap for insert or close one after remove.
9.7 IntoIter
Original: IntoIter
A by-value iterator reading elements out from both ends, which takes over the allocation. Not in this program.
9.8 RawVec
Original: RawVec
The allocation logic factored out, shared by the vector and its IntoIter.
Not in this program.
9.9 Drain
Original: Drain
A borrowing iterator that removes elements; it sets the length to zero before
starting, so a leaked Drain is safe (see Leaking). Not in this program.
9.10 Handling Zero-Sized Types
Original: Handling Zero-Sized Types
A zero-sized T needs no allocation and makes pointer offsets meaningless;
the original handles it throughout. This program refuses it, with an
assert! in new.
9.11 Final Code
Original: Final Code
use std.alloc.( self, Layout)
use std.mem
use std.ops.( Deref, DerefMut)
use std.ptr.( self, NonNull)
pub struct MyVec<T>
ptr: NonNull<T>
cap: usize
len: usize
// As Vec: it owns its Ts, so it is Send and Sync when they are.
unsafe impl<T: Send> Send for MyVec<T>
unsafe impl<T: Sync> Sync for MyVec<T>
impl<T> MyVec<T>
pub fn new$ -> Self:
assert!
(mem.size_of.<T>$ != 0)
"zero-sized types are not handled here"
Self\
ptr = NonNull.dangling$
cap = 0
len = 0
fn layout (cap: usize) -> Layout:
let size = cap * mem.size_of.<T>$
Layout.from_size_align size (mem.align_of.<T>$) <- unwrap$
fn grow (&mut self):
let new_cap = if self <- cap == 0: 4 else: 2 * self <- cap
let new_layout = Self.layout new_cap
let raw = do:
if self <- cap == 0: unsafe: alloc.alloc new_layout
else:
let old = self <- ptr <- as_ptr$ as *mut u8
unsafe: alloc.realloc old (Self.layout (self <- cap)) (new_layout <- size$)
self <- ptr =
match NonNull.new (raw as *mut T)\
Some p => p
None => alloc.handle_alloc_error new_layout
self <- cap = new_cap
pub fn push (&mut self) (elem: T):
if self <- len == self <- cap:
self <- grow$
// Written, not assigned: the slot holds no value to drop.
// `add` on a raw pointer is itself unsafe: the chain stays inside.
let slot = unsafe:
self <- ptr
<- as_ptr$
<- add (self <- len)
unsafe: ptr.write slot elem
self <- len += 1
pub fn pop (&mut self) -> Option<T>:
if self <- len == 0:
return None
self <- len -= 1
// Read out: the slot is now uninitialised, and past `len`.
// `add` on a raw pointer is itself unsafe: the chain stays inside.
let slot = unsafe:
self <- ptr
<- as_ptr$
<- add (self <- len)
Some (unsafe: ptr.read slot)
pub fn insert (&mut self) (index: usize) (elem: T):
assert! (index <= self <- len) "index out of bounds"
if self <- len == self <- cap:
self <- grow$
unsafe:
let p =
self <- ptr
<- as_ptr$
<- add index
// Shift the tail right by one, then write into the gap.
ptr.copy p (p <- add 1) (self <- len - index)
ptr.write p elem
self <- len += 1
pub fn remove (&mut self) (index: usize) -> T:
assert! (index < self <- len) "index out of bounds"
self <- len -= 1
unsafe:
let p =
self <- ptr
<- as_ptr$
<- add index
let out = ptr.read p
ptr.copy (p <- add 1) p (self <- len - index)
out
impl<T> Drop for MyVec<T>
fn drop (&mut self):
if self <- cap != 0:
unsafe:
// Drop the elements, then free the memory.
let elems = ptr.slice_from_raw_parts_mut (self <- ptr <- as_ptr$) (self <- len)
ptr.drop_in_place elems
alloc.dealloc
(self <- ptr <- as_ptr$ as *mut u8)
(Self.layout (self <- cap))
impl<T> Deref for MyVec<T>
type Target = [T]
fn deref (&self) -> &[T]:
unsafe:
std.slice.from_raw_parts (self <- ptr <- as_ptr$) (self <- len)
impl<T> DerefMut for MyVec<T>
fn deref_mut (&mut self) -> &mut [T]:
unsafe:
std.slice.from_raw_parts_mut (self <- ptr <- as_ptr$) (self <- len)
fn main$:
let mut v = MyVec.new$
for word in ["delta", "alpha", "charlie"]:
v <- push (word <- to_string$)
v <- insert 1 (String.from "bravo")
println! "{:?} len {} cap {}" (&v[..]) (v <- len$) (v <- cap)
// The slice's methods, through Deref and DerefMut.
v <- sort$
println! "{:?}" (&v[..])
println! "removed {}, popped {:?}" (v <- remove 0) (v <- pop$)
println! "{:?}" (&v[..])
["delta", "bravo", "alpha", "charlie"] len 4 cap 4
["alpha", "bravo", "charlie", "delta"]
removed alpha, popped Some("delta")
["bravo", "charlie"]
In Harsh: each unsafe step is an unsafe: block of a line or two, which
makes the unsafe surface easy to see and to review. The layout comes from
Layout.from_size_align with the size and alignment named, rather than
Layout::array::<T> — Harsh does not yet write a turbofish before an argument.
Implementing Arc and Mutex
Original: Implementing Arc and Mutex
The original's chapter, despite its title, implements Arc; this companion
follows its sections, with the program under Final Code.
10.1 Arc
Original: Arc
A pointer to a heap allocation holding the value and an atomic count of owners, shared across threads.
10.1.1 Layout
Original: Layout
MyArc<T> holds a NonNull<ArcInner<T>> — the count and the data together —
and a PhantomData<ArcInner<T>>, telling the drop checker it owns an
ArcInner<T>. It is Send and Sync only when T is both.
10.1.2 Base Code
Original: Base Code
new boxes the inner value with a count of one and keeps the raw pointer;
Deref reaches the data through it.
10.1.3 Cloning
Original: Cloning
Cloning increments the count, Relaxed — a new owner needs no
synchronisation with the others — and aborts if the count nears overflow.
10.1.4 Dropping
Original: Dropping
Dropping decrements, Release; the owner that brings the count to zero
issues an Acquire fence, so it sees every other owner's writes, and frees
the allocation.
10.1.5 Final Code
Original: Final Code
use std.marker.PhantomData
use std.ops.Deref
use std.ptr.NonNull
use std.sync.atomic.( self, AtomicUsize, Ordering)
use std.thread
struct ArcInner<T>
rc: AtomicUsize
data: T
pub struct MyArc<T>
ptr: NonNull<ArcInner<T>>
phantom: PhantomData<ArcInner<T>>
// Shared across threads: sound when T may be both sent and shared.
unsafe impl<T: Sync + Send> Send for MyArc<T>
unsafe impl<T: Sync + Send> Sync for MyArc<T>
impl<T> MyArc<T>
pub fn new (data: T) -> MyArc<T>:
let boxed = Box.new (ArcInner\ rc = AtomicUsize.new 1, data = data)
MyArc\ ptr = NonNull.new (Box.into_raw boxed) <- unwrap$, phantom = PhantomData
pub fn count (this: &Self) -> usize:
let inner = unsafe: this <- ptr <- as_ref$
inner <- rc <- load Ordering.Acquire
impl<T> Deref for MyArc<T>
type Target = T
fn deref (&self) -> &T:
let inner = unsafe: self <- ptr <- as_ref$
&inner <- data
impl<T> Clone for MyArc<T>
fn clone (&self) -> MyArc<T>:
let inner = unsafe: self <- ptr <- as_ref$
// Relaxed: a new owner needs no synchronisation with the others.
let old = inner <- rc <- fetch_add 1 Ordering.Relaxed
if old >= isize.MAX as usize:
std.process.abort$
MyArc\ ptr = self <- ptr, phantom = PhantomData
impl<T> Drop for MyArc<T>
fn drop (&mut self):
let inner = unsafe: self <- ptr <- as_ref$
let old = inner <- rc <- fetch_sub 1 Ordering.Release
if old != 1:
return
// The last owner: see every other owner's writes, then free.
atomic.fence Ordering.Acquire
unsafe: drop (Box.from_raw (self <- ptr <- as_ptr$))
struct Payload
text: String
impl Drop for Payload
fn drop (&mut self):
println! "payload freed"
fn main$:
let shared = MyArc.new (Payload\ text = String.from "shared text")
let mut handles = Vec.new$
for i in 0..3:
let mine = shared <- clone$
let read = move || format! "thread {i} read {:?}" (mine <- text)
handles <- push (thread.spawn read)
for h in handles:
println! "{}" (h <- join$ <- unwrap$)
println! "owners left: {}" (MyArc.count (&shared))
thread 0 read "shared text"
thread 1 read "shared text"
thread 2 read "shared text"
owners left: 1
payload freed
In Harsh: the counts' orderings read as they are named; the value is shared by three threads, and freed once, after the last owner is gone.
FFI
Original: FFI
Calling C from Rust means declaring its functions in an extern "C" block and
calling them in unsafe — the compiler cannot check C. Strings cross as
CString and *const c_char; callbacks cross as extern "C" fn. The
original's examples link to the snappy library; these use the C standard
library, which every program is linked with already.
use std.ffi.( CStr, CString)
use std.os.raw.( c_char, c_int)
// Declarations of C functions: their bodies are in the C library.
extern "C"
fn abs (x: c_int) -> c_int
fn strlen (s: *const c_char) -> usize
// A safe interface: a `&CStr` is always valid and terminated, so the call
// is sound for every caller.
fn c_len (s: &CStr) -> usize:
unsafe: strlen (s <- as_ptr$)
fn main$:
let s = CString.new "harsh" <- unwrap$
// The compiler cannot check C: each direct call is our promise.
println! "{}" (unsafe: abs (-42))
println! "{}" (c_len (&s))
42
5
The original's next step is a safe interface: a Rust function that takes
Rust types, upholds C's requirements itself, and hides the unsafe from its
callers — c_len above, taking a &CStr that is sure to be valid and
terminated.
A callback: C's qsort sorting an array with a comparison written in Harsh.
use std.ffi.c_void
use std.os.raw.c_int
extern "C"
fn qsort (base: *mut c_void) (n: usize) (size: usize) (compare: extern "C" fn (*const c_void) (*const c_void) -> c_int)
// Written in Harsh, called by C.
extern "C" fn by_value (a: *const c_void) (b: *const c_void) -> c_int:
let (x, y) = unsafe: (*(a as *const i32), *(b as *const i32))
(x > y) as c_int - (x < y) as c_int
fn main$:
let mut xs = [5, 3, 9, 1, 7]
unsafe:
qsort
(xs <- as_mut_ptr$ as *mut c_void)
(xs <- len$)
(std.mem.size_of.<i32>$)
by_value
println! "{:?}" xs
[1, 3, 5, 7, 9]
In Harsh: the extern "C" block holds its declarations beneath its
header, each one line with no body; a function for C to call is extern "C"
fn with its body, like any function.
Beneath std
Original: Beneath std
Everything so far used the standard library. Code for bare metal, kernels or
embedded devices uses #![no_std]: only core (and alloc, if an allocator
is provided). Such a program must define what happens on a panic — a
function marked #[panic_handler] — and, as a binary, its own entry point.
Harsh transpiles such code as it does any other — #![no_std] is an
attribute like the rest — but a no_std binary cannot be built and run the
way this book runs its programs, so this page has none. The original shows
the pieces; with them written in Harsh's spelling, hrs build for an embedded
target works as cargo build does.
12.1 #[panic_handler]
Original: #[panic_handler]
A no_std program has no panic machinery of its own: exactly one function in
the final binary must be marked #[panic_handler], taking a
&core::panic::PanicInfo and never returning (-> !). It may loop, reset the
device, or report over a serial port. In Harsh it is written as any function —
#[panic_handler] above fn panic (info: &PanicInfo) -> !: — with its body
beneath.
That is the end of the dark arts. Use them sparingly, keep them in small
modules, and write down, next to every unsafe:, why it is sound.