Unsafe Code¶
FastC is safe by default, but provides unsafe for low-level operations.
What Unsafe Enables¶
Inside an unsafe block, you can:
- Dereference raw pointers (
raw(T),rawm(T)) - Call unsafe functions
- Bypass runtime safety checks
Unsafe Blocks¶
fn example() {
let x: i32 = 42;
let ptr: rawm(i32) = addr(x);
unsafe {
// Can dereference raw pointer here
deref(ptr) = 100;
}
// Outside unsafe: safe code only
}
Unsafe Functions¶
Declare functions that require unsafe context:
unsafe fn dangerous_read(ptr: raw(i32)) -> i32 {
return deref(ptr);
}
unsafe fn dangerous_write(ptr: rawm(i32), value: i32) {
deref(ptr) = value;
}
Calling Unsafe Functions¶
fn main() -> i32 {
let x: i32 = 42;
unsafe {
let value: i32 = dangerous_read(addr(x));
dangerous_write(addr(x), value + 1);
}
return x;
}
Safe Wrappers¶
Wrap unsafe operations in safe APIs:
// Unsafe low-level function
unsafe fn raw_get(ptr: raw(i32), index: i32) -> i32 {
return deref(ptr + index);
}
// Safe wrapper with bounds checking
fn safe_get(data: slice(i32), index: i32) -> opt(i32) {
if index < 0 || index >= len(data) {
return none(i32);
}
unsafe {
return some(raw_get(data.data, index));
}
}
Common Use Cases¶
Memory Allocation¶
extern "C" {
fn malloc(size: usize) -> rawm(u8);
fn free(ptr: rawm(u8));
}
fn allocate(size: usize) -> rawm(u8) {
unsafe {
return malloc(size);
}
}
fn deallocate(ptr: rawm(u8)) {
unsafe {
free(ptr);
}
}
Type Punning¶
unsafe fn reinterpret_as_bytes(value: raw(i32)) -> raw(u8) {
return cast(raw(u8), value);
}
fn get_bytes(x: i32) -> arr(u8, 4) {
let bytes: arr(u8, 4) = [0, 0, 0, 0];
unsafe {
let src: raw(u8) = reinterpret_as_bytes(addr(x));
for let i: i32 = 0; i < 4; i = i + 1 {
at(bytes, i) = deref(src + i);
}
}
return bytes;
}
FFI Calls¶
extern "C" {
fn strlen(s: raw(u8)) -> usize;
fn memcpy(dest: rawm(u8), src: raw(u8), n: usize) -> rawm(u8);
}
fn string_length(s: slice(u8)) -> usize {
unsafe {
return strlen(s.data);
}
}
fn copy_memory(dest: rawm(u8), src: raw(u8), len: usize) {
unsafe {
discard memcpy(dest, src, len);
}
}
Hardware Access¶
unsafe fn read_port(port: u16) -> u8 {
// Platform-specific I/O
let ptr: raw(u8) = cast(raw(u8), port);
return deref(ptr);
}
unsafe fn write_port(port: u16, value: u8) {
let ptr: rawm(u8) = cast(rawm(u8), port);
deref(ptr) = value;
}
What Remains Checked¶
Even in unsafe blocks:
- Type checking is still enforced
- Syntax errors are still caught
- Function signatures must match
Unsafe only bypasses runtime safety checks.
Unsafe Guidelines¶
Keep Unsafe Blocks Small¶
// Bad: large unsafe block
unsafe {
// 50 lines of code...
}
// Good: minimal unsafe
let result: i32 = unsafe {
deref(ptr)
};
// Rest of function is safe
Document Unsafe Invariants¶
// SAFETY: ptr must be non-null and point to valid i32
// The caller guarantees the pointer is valid for the lifetime of this call
unsafe fn read_value(ptr: raw(i32)) -> i32 {
return deref(ptr);
}
Encapsulate Unsafe¶
// Internal unsafe implementation
unsafe fn raw_swap(a: rawm(i32), b: rawm(i32)) {
let temp: i32 = deref(a);
deref(a) = deref(b);
deref(b) = temp;
}
// Safe public API
fn swap(a: mref(i32), b: mref(i32)) {
unsafe {
raw_swap(a, b);
}
}
Unsafe vs Safe¶
| Operation | Safe | Unsafe |
|---|---|---|
ref(T) dereference |
Yes | Yes |
mref(T) dereference |
Yes | Yes |
raw(T) dereference |
No | Yes |
rawm(T) dereference |
No | Yes |
| Pointer arithmetic | No | Yes |
| Call unsafe fn | No | Yes |
| Bounds-checked access | Yes | Optional |
| Overflow checking | Yes | Optional |
Disabling Runtime Checks¶
In release builds with --release, some checks may be optimized away. For explicit unsafe arithmetic:
Best Practices¶
- Minimize unsafe surface area - Keep unsafe blocks as small as possible
- Create safe abstractions - Wrap unsafe in safe APIs
- Document safety requirements - Explain what callers must guarantee
- Review unsafe carefully - Unsafe code needs extra scrutiny
- Test unsafe code thoroughly - Bugs in unsafe are harder to catch
See Also¶
- Pointers - Pointer types and operations
- C Interoperability - FFI with C
- Safety Guarantees - What safe code prevents