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:

  1. Interfacing with C code - C libraries use raw pointers extensively
  2. Performance-critical code - Sometimes avoiding the borrow checker's overhead matters
  3. 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:

  1. You must for the task - Calling C functions, low-level system programming
  2. It's worth the risk - The performance gain or expressive power justifies the safety trade-off
  3. You can isolate it - Keep unsafe code in small, well-documented modules
  4. You can verify it - You can convince yourself (and reviewers) it's actually safe

Don't use unsafe when:

  1. Safe alternatives exist - The standard library usually provides safe versions
  2. You're not sure it's safe - If you can't prove it's safe, it probably isn't
  3. 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.