Unsafe and macros
unsafe opens five operations the compiler will not check. A macro_rules! arm matches source shape, not a runtime value.
<!-- hal:authoritative:yaml -->
*unsafe opens five operations the compiler will not check. A macro_rules! arm matches source shape, not a runtime value.*
§I - Frame
Duha session 23. Topics #23, TRPL Chapter 20: what unsafe unlocks, and how to read one macro_rules! matcher. The chapter overview also lists advanced traits, advanced types, and advanced functions and closures. Those three stay a map, not the Done cell.
Session 22 shipped value patterns. This row does not re-teach them. A macro matcher compares token trees, not values.
The Rustonomicon is the book's longer guide for unsafe. This lesson does not cite it as an owned tome.
Three moves land by the end:
- The Five Unlocks: the operations that require
unsafe, and whatunsafedoes not disable. - The Safe Wrapper: a safe function whose body holds a small
unsafeblock with a stated contract. - The Matcher: read
( $( $x:expr ),* )in amacro_rules!arm.
Done-criteria: Can name what unsafe unlocks and read a macro_rules! matcher.
§II - The Five Unlocks
unsafe does not turn off the borrow checker. A reference inside an unsafe block is still checked. The keyword only opens five operations the compiler will not prove:
- Dereference a raw pointer.
- Call an
unsafefunction or method. - Access or modify a mutable static variable.
- Implement an
unsafetrait. - Access fields of a
union.
Raw pointers are *const T and *mut T. You may create them in safe code. &raw const num yields *const i32. &raw mut num yields *mut i32. Casting an integer address is also safe to write and is usually the wrong tool. The dereference is the unlock (Listing 20-3):
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
The * reads memory. The pointer construction does not. Raw pointers may be null, may dangle, and may alias. References may not.
An unsafe fn means the caller meets a contract the compiler cannot see. The call sits in an unsafe block. Inside an unsafe fn, each unsafe operation still needs an inner block.
An immutable static is safe to read. A static mut is not. Listing 20-11 touches COUNTER only inside unsafe, and add_to_count is itself unsafe because a second thread on that static is a data race. A comment that starts with SAFETY states the caller rule.
unsafe impl on an unsafe trait is the promise when the invariant is invisible. Send and Sync are the book's case: a raw pointer does not get those markers for free.
A union stores one field at a time. The read is unsafe because Rust does not know which type is live.
Miri (nightly) can report some undefined behavior on paths that actually run. A clean run is not a proof. The book then points at the Rustonomicon. That pointer is not an owned cite.
§III - The Safe Wrapper
Put a safe function around a small unsafe block. Safe Rust cannot return two &mut subslices of one slice: E0499. The halves do not overlap. The checker does not see the split.
Listing 20-6 keeps assert!(mid <= len), takes as_mut_ptr(), and calls the unsafe helpers in one block:
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
from_raw_parts_mut and add trust the pointer and the offset. The assert is why that trust is local: both slices sit inside values and do not overlap. Callers of split_at_mut do not write unsafe. Listing 20-7 drops the assert and is undefined behavior. An unsafe extern "C" item is the same kind of boundary: marking one function safe is a promise, not a recheck.
§IV - The Matcher
A macro writes code before type-checking. A function fixes arity and types, and it runs too late to implement a trait. You must define the macro, or bring it into scope, before the call.
Declarative macros use macro_rules!. Listing 20-35 is a simplified vec!:
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
Read the arm from the left. Outer parentheses wrap the matcher. $ names a macro variable, not a Rust binding. $x:expr captures one expression; the fragment specifier is expr. $( ... ),* means zero or more copies, separated by a literal comma. The * repeats whatever sits immediately before it.
On the right, $( temp_vec.push($x); )* emits one push per captured expression. vec![1, 2, 3] becomes three pushes and the vector. #[macro_export] exports the macro. Without it, callers outside the crate cannot name it.
That arm is the Done-cell reading. Procedural macros (derive, attribute-like, function-like) take a TokenStream in a separate proc-macro crate. They are not this matcher. An expr fragment matches source. A Chapter 19 pattern matches a value.
§V - Proof and close
- List the five operations
unsafeunlocks. Say the borrow checker still checks references. - Point at
*r1and say why that star is insideunsafewhile&raw const numis not. - Say why
split_at_mutcan be a safefnwith anunsafeblock, and whatassert!(mid <= len)is doing. - In
( $( $x:expr ),* ), name the fragment specifier, the separator, and the repetition operator.
The lab Mac /tmp/duha-2026-10-04-unsafe-macros (not in this bundle) dereferences a *const i32 inside unsafe and expands this matcher as stack_vec. cargo test --offline exited 0. Transcript: Duha close note. The exit is a type-check and one test, not a soundness proof.
Done-criteria: Can name what unsafe unlocks and read a macro_rules! matcher.
Next: the book's web server (Topics #24, TRPL Ch.21). Monday 2026-10-05 does not re-author #23.