Unsafe Code
By default, Oxide enforces strict safety rules at compile time. The borrow checker, ownership system, and type system all work together to prevent entire classes of bugs. However, sometimes you need to do things that the compiler can't prove are safe. In these cases, Oxide provides unsafe code blocks.
Unsafe Oxide allows you to:
- Dereference raw pointers
- Call unsafe functions
- Mutate statics
- Implement unsafe traits
It's important to understand that unsafe doesn't mean "no rules"—it means "I promise the compiler that these rules are satisfied" where the compiler can't verify them itself.
Raw Pointers
Unsafe Oxide has two new pointer types called raw pointers: *const T (immutable) and *mut T (mutable). These are like references, but without the borrow checker's guarantees.
Creating Raw Pointers
You can create raw pointers from safe code:
fn main() {
var num = 5
// Create immutable and mutable raw pointers
let r1 = &num as *const Int
let r2 = &mut num as *mut Int
unsafe {
println!("r1 is: \((*r1)?)")
println!("r2 is: \((*r2)?)")
}
}
Note that creating raw pointers is safe—dereferencing them is what requires unsafe.
Dereferencing Raw Pointers
To read or write through a raw pointer, you must use the dereference operator *, and you must do it in an unsafe block:
fn main() {
var x = 5
let r = &mut x as *mut Int
unsafe {
*r = 10
println!("x is now: \(x)") // Prints: x is now: 10
}
}
Why Raw Pointers?
Raw pointers are useful when:
- Interfacing with C code - C libraries use raw pointers extensively
- Performance-critical code - Sometimes avoiding the borrow checker's overhead matters
- Complex pointer manipulations - Like building custom data structures
Here's an example that demonstrates raw pointers' flexibility:
fn main() {
var data = vec![1, 2, 3, 4, 5]
let ptr = data.asMutPtr()
unsafe {
// Access the pointer directly
*ptr = 100
*(ptr.offset(1)) = 101
*(ptr.offset(2)) = 102
}
println!("\(data:?)") // Prints: [100, 101, 102, 4, 5]
}
Calling Unsafe Functions
An unsafe function is one that has requirements that the compiler can't check. You must call them in an unsafe block:
fn unsafeOperation() {
println!("This is an unsafe function")
}
unsafe fn veryUnsafeOperation() {
println!("This does something dangerous")
}
fn main() {
unsafeOperation() // OK - not marked unsafe
unsafe {
veryUnsafeOperation() // OK - inside unsafe block
}
// veryUnsafeOperation() // Error: unsafe function requires unsafe block
}
Declaring Unsafe Functions
When you declare a function as unsafe, you're making a contract: callers must ensure safety preconditions are met:
/// Divides a by b. Caller must ensure b is not zero.
///
/// # Safety
///
/// Calling this function with `b == 0` is undefined behavior.
unsafe fn divide(a: Int, b: Int): Int {
a / b // Undefined if b == 0
}
fn main() {
let result = unsafe {
divide(10, 2)
}
println!("10 / 2 = \(result)")
}
The # Safety section in documentation comments is the standard way to document unsafe function preconditions.
Safe Abstractions Over Unsafe Code
Often you'll want to provide a safe interface to unsafe operations. This is the key to using unsafe code effectively:
fn main() {
var v = vec![1, 2, 3, 4, 5]
// This is safe because splitAtMut checks the index before doing unsafe operations
let (left, right) = v.splitAtMut(2)
println!("Left: \(left:?)") // [1, 2]
println!("Right: \(right:?)") // [3, 4, 5]
}
// This is what splitAtMut might look like internally:
fn splitAtMut<T>(v: &mut Vec<T>, mid: Int): (&mut Vec<T>, &mut Vec<T>) {
// Safe because we check the index
if mid > v.len() {
panic!("Index out of bounds")
}
unsafe {
let ptr = v.asMutPtr()
let left = std.slice.fromRawPartsMut(ptr, mid)
let right = std.slice.fromRawPartsMut(ptr.offset(mid as IntSize), v.len() - mid)
(&mut *left, &mut *right)
}
}
The principle here is: safe boundary around unsafe code. Do all the validation and safety checks in the safe wrapper, leaving the dangerous operations in unsafe blocks.
Using extern for FFI
When calling C functions from Oxide, you use extern to declare foreign functions:
extern "C" {
// Declare C functions
fn strlen(s: *const UInt8): UInt
fn malloc(size: UInt): *mut UInt8
fn free(ptr: *mut UInt8)
}
fn main() {
unsafe {
let ptr = malloc(1024)
free(ptr)
}
}
You can also expose Oxide functions to C:
#[no_mangle]
extern "C" fn oxideAdd(a: Int, b: Int): Int {
a + b
}
Mutable Statics
You can declare global mutable variables using var at module scope:
var COUNTER: Int = 0
fn incrementCounter() {
unsafe {
COUNTER += 1
}
}
fn main() {
incrementCounter()
unsafe {
println!("Counter: \(COUNTER)")
}
}
Accessing mutable statics is unsafe because:
- Multiple threads could access and modify the value simultaneously
- The compiler can't enforce the usual borrowing rules across the program
For safe multi-threaded access to shared state, use std.sync.Mutex or std.sync.atomic.
Unsafe Traits
Sometimes a trait has requirements that can't be checked by the compiler. You mark such traits as unsafe:
unsafe trait UnsafeMarkerTrait {
fn importantInvariant()
}
// Implementing an unsafe trait requires unsafe
unsafe extension SomeType: UnsafeMarkerTrait {
fn importantInvariant() {
// Must uphold the invariant
}
}
A real example from the standard library is Send and Sync:
// These are marker traits - they have no methods
unsafe trait Send {}
unsafe trait Sync {}
// Only types that are safe to send between threads implement Send
// The compiler implements this automatically for most types
When to Use Unsafe
Use unsafe code when:
- You must for the task - Calling C functions, low-level system programming
- It's worth the risk - The performance gain or expressive power justifies the safety trade-off
- You can isolate it - Keep unsafe code in small, well-documented modules
- You can verify it - You can convince yourself (and reviewers) it's actually safe
Don't use unsafe when:
- Safe alternatives exist - The standard library usually provides safe versions
- You're not sure it's safe - If you can't prove it's safe, it probably isn't
- It makes code much more complex - The safety trade-off should be worth it
Guidelines for Safe Unsafe Code
When you do write unsafe code, follow these principles:
Document the Safety Contract
/// Performs an operation that requires careful pointer manipulation.
///
/// # Safety
///
/// The caller must ensure:
/// - `ptr` is a valid pointer to at least `len` elements of type T
/// - `len` is the actual number of elements `ptr` points to
/// - `ptr` is properly aligned for type T
/// - The memory pointed to by `ptr` is not accessed elsewhere while this function runs
unsafe fn dangerousOperation<T>(ptr: *const T, len: Int) {
// Implementation
}
Validate Before Acting
unsafe fn validateAndOperate(slice: &[UInt8], index: Int) {
// Safe checks first
if index >= slice.len() {
panic!("Index out of bounds")
}
// Only then do unsafe operations
unsafe {
let ptr = slice.asPtr().offset(index as IntSize)
// ...
}
}
Keep Unsafe Blocks Small
// Good: unsafe is localized
fn findZero(data: &[UInt8]): Int? {
for (i, &byte) in data.iter().enumerate() {
if byte == 0 {
return Some(i)
}
}
null
}
// Avoid: large unsafe blocks where they're not needed
unsafe fn findZeroBad(data: &[UInt8]): Int? {
for (i, &byte) in data.iter().enumerate() {
if byte == 0 {
return Some(i)
}
}
null
}
Common Unsafe Patterns
Pattern: Working with Raw Pointers
fn processBuffer(buf: &mut [UInt8]) {
unsafe {
let ptr = buf.asMutPtr()
// Operate on the pointer
for i in 0..<buf.len() {
*ptr.offset(i as IntSize) = (*ptr.offset(i as IntSize)).wrappingAdd(1)
}
}
}
Pattern: Calling C Functions
extern "C" {
fn systemCall(command: *const UInt8): Int
}
fn runCommand(cmd: String): Int {
cmd.asBytes().withCStr { cPtr ->
unsafe { systemCall(cPtr) }
}
}
Pattern: Casting Between Types
fn castPtrToInt(ptr: *const UInt8): Int {
unsafe {
ptr as Int
}
}
Testing Unsafe Code
Unsafe code deserves extra testing:
#[test]
fn testUnsafeOperation() {
var x = 5
let ptr = &mut x as *mut Int
unsafe {
*ptr = 10
}
assertEq!(x, 10)
}
#[test]
fn testRawPointerOffset() {
var array = [1, 2, 3, 4, 5]
let ptr = array.asMutPtr()
unsafe {
assertEq!(*ptr, 1)
assertEq!(*ptr.offset(1), 2)
assertEq!(*ptr.offset(4), 5)
}
}
Summary
Unsafe code in Oxide:
- Exists for a reason - Sometimes you need it for performance or interoperability
- Requires careful thought - Document your safety requirements clearly
- Should be isolated - Keep it in small, well-tested modules
- Isn't the default - Most Oxide code is safe, and that's a feature
- Doesn't bypass the type system - Unsafe code still gets type-checked
Remember: unsafe doesn't mean "do whatever you want." It means "the compiler can't verify this is safe, so you must verify it yourself." Take that responsibility seriously, and unsafe code can be a powerful tool in your Oxide toolkit.