Unchecked Code and Raw Pointers
Gruel's safety guarantees hold everywhere by default. For low-level work — implementing data structures, calling OS interfaces, or writing FFI bindings — you can opt out of those guarantees inside checked blocks.
The name checked reflects that you are taking responsibility for checking the invariants the compiler normally verifies.
Checked Blocks
checked { ... } is a block expression that allows raw-pointer and syscall operations inside it. Its value is the value of the block:
fn main() -> i32 {
let mut value: i32 = 10;
let v: i32 = checked {
let p: MutPtr(i32) = MutPtr(i32)::from(&mut value);
p.write(42);
p.read()
};
@dbg(v); // 42
value
}
Code outside checked blocks is completely unaffected — all of Gruel's safety rules still apply there.
Raw Pointer Types
Two raw pointer types are available, parameterized by the pointee:
Ptr(T) // read-only pointer to T
MutPtr(T) // read-write pointer to T
Getting a Pointer from a Value
Construct a pointer from a reference using the type's from associated function. Combine it with the & and &mut operators from References:
fn main() -> i32 {
let x: i32 = 7;
let mut y: i32 = 0;
let result = checked {
let p: Ptr(i32) = Ptr(i32)::from(&x);
let q: MutPtr(i32) = MutPtr(i32)::from(&mut y);
q.write(p.read() * 6);
q.read()
};
@dbg(result); // 42
result
}
Pointer Methods
Once you have a pointer, the operations are methods on the pointer type:
| Method | Available on | Description |
|---|---|---|
p.read() | Ptr(T), MutPtr(T) | Load the value at p |
p.write(v) | MutPtr(T) | Store v at p |
p.offset(n) | Ptr(T), MutPtr(T) | Advance by n elements (not bytes) |
p.is_null() | Ptr(T), MutPtr(T) | Test whether p is null |
p.to_int() | Ptr(T), MutPtr(T) | Pointer's address as u64 |
Constructors live on the type itself:
| Constructor | Description |
|---|---|
Ptr(T)::null() / MutPtr(T)::null() | Null pointer |
Ptr(T)::from(r) / MutPtr(T)::from(r) | Wrap a Ref(T) / MutRef(T) |
Ptr(T)::from_int(addr) / MutPtr(T)::from_int(addr) | Reinterpret an integer address |
Pointer Arithmetic
p.offset(n) advances by n elements, not bytes — the pointee type determines the stride:
fn main() -> i32 {
let arr = [1, 2, 3, 4, 5];
let third = checked {
let base: Ptr(i32) = Ptr(i32)::from(&arr[0]);
let p = base.offset(2); // points at arr[2]
p.read()
};
third // 3
}
Syscalls
@syscall makes a direct OS system call. The first argument is the syscall number; the rest are arguments (up to six). It returns i64:
fn main() -> i32 {
checked {
// syscall 1 = write(fd, buf, len) on Linux
@syscall(1, 1, 0, 0);
}
0
}
Syscall numbers and ABI conventions are platform-specific. Use @target_os() and @target_arch() to branch between platforms.
Unchecked Functions
Mark a function unchecked to signal that it performs low-level operations. Callers must invoke it from inside a checked block:
unchecked fn dangerous_op() -> i32 { 42 }
fn main() -> i32 {
checked { dangerous_op() }
}
Calling an unchecked function outside a checked block is a compile-time error.
What You Are Responsible For
Inside checked blocks the compiler does not verify:
- That pointers are non-null before dereferencing
- That pointers point to valid, correctly-typed memory
- That there are no aliasing violations (two
MutPtrto the same location) - That pointers don't outlive the value they came from
These are your responsibility. Outside checked blocks, Gruel's normal guarantees are fully in force.
When to Use Checked Blocks
Checked blocks are for implementing low-level primitives — allocators, collections, OS wrappers, FFI. Most application code should never need them. If you find yourself reaching for checked in business logic, consider whether there's a safe abstraction that already does what you need.