Language Internals
Part 10 of 11 · Rust Language ProficiencyUnsafe, FFI & Performance Mindset
unsafe boundaries, FFI, performance mindset.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does unsafe allow?
Answer
Dereferencing a raw pointer, calling an unsafe function, implementing an unsafe trait, and touching mutable statics, among a short list.
L2
Does unsafe turn off the borrow checker for the whole function?
Answer
No. Only specific operations need an unsafe block. The rest of the function is still checked.
L3
Does unsafe mean the program is insecure?
Answer
It allows a few extra operations (raw pointer deref, unsafe fn calls, mutable statics); you uphold their invariants and everything else is still checked. A sound wrapper can still be a safe public API.
L4
What is the first performance step?
Answer
A release profile. Then cut clones, reallocations, and locks. unsafe is later, if safe code cannot express the win.
L5
What does a SAFETY comment state?
Answer
The invariant that makes this particular unsafe block sound, and why the caller established it.
L6
Who owns a pointer from C?
Answer
The contract says. Rust must not free a pointer C still owns, and must free one C handed off, exactly once.
L7
What is no_std?
Answer
A crate that does not depend on the full standard library. A no_std binary needs a panic handler (a no_std library does not). It is for embedded and kernels, not a switch you flip for speed on a server.
Failure modes
Unsafe to silence the checker
A borrow error is fixed with a raw pointer instead of a shorter borrow.
Missing SAFETY comment
The next edit breaks the invariant and nobody can see what was promised.
Double free across FFI
Both sides free the same allocation, or neither does.
Optimizing a debug build
Bounds checks and the lack of inlining dominate, so the hotspot is fiction.
Misconceptions
unsafe disables all checks in the module.
It unlocks a few operations. Types, moves, and lifetimes still apply around them.
A safe-looking wrapper is automatically sound.
Soundness is the proof in the SAFETY comment. The type signature can still lie.
no_std makes servers faster.
It removes std. It does not remove the need to profile.
Interviewer traps
Reaching for unsafe when a clone shows up in a profile once.
Confirm the release build, then remove the clone in safe code. Unsafe is the last cut.
Treating unsafe like any or a type ignore.
any hides a type. unsafe can cause undefined behavior. The blast radius is larger.
Design scenario
Same prompt for every reader.
Requirements
A release profile first. A safe Rust rewrite of the copy if that is the cost. FFI only if the C library must stay, with one owner of the free.
Traffic / scale
The loop is on the request path.
Latency
The profile must name the copy before any unsafe edit.
Consistency
The safe wrapper does not expose the raw pointer.
Availability
Undefined behavior is not an Err you can catch.
Failure assumptions
- The team profiles a debug build.
- Both Rust and C call free.
Constraints
- Stay on one function. Do not redesign the service.
Prompt
A hot loop copies a buffer into a String on every call, and a C library already produces the bytes.
Measure, then unlock
Prefer
Safe code after a profile
A release build names the clone or the allocation. Most wins never enter unsafe.
- Debug profiles lie about cost.
- A shorter borrow beats a raw pointer.
- The public API stays safe.
Alternative
Escape hatches
TypeScript any and a Python type ignore hide the checker. They do not justify undefined behavior.
- any is still memory-safe in the engine.
- unsafe can be undefined behavior.
- FFI adds a second owner of free.
Overview
unsafe is not a module-wide off switch and it is not a security flag by itself. It is the list of operations whose invariants you uphold. Performance work starts in safe Rust on a release binary. FFI is the case where the other language forces the boundary.
Comparative
| Concern | TypeScript | Python | Rust |
|---|---|---|---|
| Escape the checker | any or ts-ignore | type ignore | unsafe |
| Call C | N-API or wasm | ctypes or cffi | extern C |
| Perf habit | engine tuning | a C extension | measure, then fewer allocations |
Decisions
- 1
Step 1 Measure first - profile with perf or criterion
- nextStep 2 Can safe code meet the target
- ?
Step 2 Can safe code meet the target
- yesStep 3a Stay safe - better algorithm or fewer allocations
- noStep 3b Small unsafe block inside a safe API
- 3
Step 3a Stay safe - better algorithm or fewer allocations
- 4
Step 3b Small unsafe block inside a safe API
- nextStep 4 SAFETY comment lists the invariants
- invariant broken - aliasing or dangling pointerFailure path - undefined behavior, not a panic
- 5
Step 4 SAFETY comment lists the invariants
- nextStep 5 FFI - extern C, repr(C), CString keeps the buffer alive
- 6
Step 5 FFI - extern C, repr(C), CString keeps the buffer alive
- nextStep 6 Test under Miri and sanitizers
- 7
Step 6 Test under Miri and sanitizers
- 8
Failure path - undefined behavior, not a panic
Lesson map
Unsafe, FFI & Performance Mindset
unsafe boundaries, FFI, performance mindset.
Architecture. Step 1 Measure first - profile with perf or criterion Ready. Step 2 Can safe code meet the target Ready. Step 3a Stay safe - better algorithm or fewer allocations Ready. Step 3b Small unsafe block inside a safe API Ready. Step 4 SAFETY comment lists the invariants Ready. Step 5 FFI - extern C, repr(C), CString keeps the buffer alive Ready. Step 6 Test under Miri and sanitizers Ready. Failure path - undefined behavior, not a panic Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB A["Step 1 Measure first - profile with perf or criterion Ready"] B["Step 2 Can safe code meet the target Ready"] C["Step 3a Stay safe - better algorithm or fewer allocations Ready"] D["Step 3b Small unsafe block inside a safe API Ready"] E["Step 4 SAFETY comment lists the invariants Ready"] F["Step 5 FFI - extern C, repr(C), CString keeps the buffer alive Ready"] G["Step 6 Test under Miri and sanitizers Ready"] X["Failure path - undefined behavior, not a panic Ready"] A -->|continues| B B -->|yes| C B -->|no| D D -->|continues| E E -->|continues| F F -->|continues| G D -->|invariant broken - aliasing or dangling pointer| X
Press Run. Snippets must be self-contained — no network, files, or native modules.
Rosetta — a raw pointer kept inside a wrapper
The public function is safe. The unsafe block is the only dereference, and only after the wrapper proved the pointer is in range. This is a sketch of the shape, not an invitation to skip the proof.
fn read_first(buf: &[u8]) -> Option<u8> {
if buf.is_empty() {
return None;
}
// SAFETY: the empty check means index 0 is in range for this slice.
Some(unsafe { *buf.as_ptr() })
}function readFirst(buf: number[]): number | undefined {
return buf[0];
}def read_first(buf: bytes) -> int | None:
return buf[0] if buf else NoneTypeScript and Python cannot express undefined behavior for this read. The Rust sketch is only sound because the length check matches the dereference. A safe buf.first().copied() is the better version. That is the point of profiling and deleting unsafe.
Rosetta — FFI sketch
unsafe extern "C" {
fn abs(n: i32) -> i32;
}
fn abs_safe(n: i32) -> i32 {
// SAFETY: abs is a pure C function of one int and does not retain the argument.
unsafe { abs(n) }
}function absSafe(n: number): number {
return Math.abs(n);
}def abs_safe(n: int) -> int:
return abs(n)If C allocates and Rust frees, say so once. If C keeps the pointer, Rust must not call free. The extern block syntax here matches edition 2024.
Rosetta — allocation awareness
fn join_owned(parts: &[String]) -> String {
parts.iter().map(String::as_str).collect::<Vec<_>>().join(",")
}Prefer borrowing when the caller still owns the strings. parts.join is not on &[String] the way it is on a string slice of pieces you already own as &str. The lesson is the extra Vec, not a raw pointer.
function joinOwned(parts: string[]): string {
return parts.join(",");
}def join_owned(parts: list[str]) -> str:
return ",".join(parts)JavaScript and Python allocate the result too. They do not make the intermediate collection a type error. In Rust, building a Vec<&str> you immediately join is the kind of cost a release profile should confirm before anyone mentions unsafe.
no_std
no_std drops the standard library for firmware and kernels. A no_std binary needs a panic handler (a no_std library does not). It is not how you speed up a Tokio service. The embedded book is the map if you are actually on a microcontroller.
Interview Q&A
Does unsafe mean insecure?
Answer
It means the compiler is not checking a specific invariant. Sound unsafe behind a safe wrapper can be a secure API. An unsound wrapper is the danger.
What is the first performance step?
Answer
Profile a release build. Cut clones and allocations in safe code before you open an unsafe block.
Does unsafe turn off the borrow checker?
Answer
No. It permits a short list of operations. The surrounding code is still checked. rustc 1.98.1 still enforces moves outside the block.
What belongs in a SAFETY comment?
Answer
Why this call is sound: the pointer is in range, aligned, and not aliased in a way the operation forbids, and who owns free.
Who frees an FFI pointer?
Answer
Whichever side the contract names. Both sides freeing, or neither freeing a handed-off buffer, is the bug.
How is this different from TypeScript any?
Answer
any disables a type. The engine still manages memory. unsafe can be undefined behavior.
How is this different from a Python type ignore?
Answer
The ignore hides a checker message. ctypes can still mismanage a pointer, and that bug is closer to unsound FFI than to a type ignore.
Is no_std a performance setting?
Answer
No. It is a portability subset. A no_std binary needs a panic handler (a no_std library does not). Servers stay on std unless they are actually embedded.
Pitfalls
- Raw pointers to quiet a borrow error.
- Profiling only debug builds.
- A safe wrapper whose SAFETY comment is wishful.
- Two owners of free.
- Flipping no_std to chase a benchmark.
A reviewer wants unsafe in a loop that clones a String. Say which command you run first, what safe change you try, and what the SAFETY comment would have to prove if unsafe remained.