Unsafe code
Raw pointers, manual memory, and casts.
On this page
Valk checks memory access by default. For C libraries and low-level code it also has raw pointers and manual memory, which the compiler cannot check. Operations that can break memory safety only compile inside an @unsafe scope.
Unsafe scopes#
Put @unsafe on its own line; it allows unsafe operations from there to the end of the scope, including nested scopes:
fn first(data: *[u8]) u8 {
@unsafe
return data[0]
}
fn main() {
let bytes = Array[u8]{ 7, 8, 9 }
println(first(bytes.items())) // 7
}
What needs @unsafe:
- Indexing an unbounded pointer such as
*[u8] - Calling a function that takes a
ptrargument, such asmem.free - Turning a
ptrinto a typed pointer*T - Calling a function marked
@unsafe
valk build --ignore-unsafe allows these in the whole build.
Pointer types#
| Type | Meaning |
|---|---|
ptr |
An untyped address |
*T |
Points at one T; fields are read and written through it |
*[T] |
Points at a run of T with no known length |
*[T x N] |
Points at exactly N items of T |
?ptr, ?*T |
May be null |
@ref(x) gives the address of a variable as *T, the way &x gives a borrow &T. A *T converts to ptr and *[T] on its own; a ptr converts to uint.
Read and write memory#
@ptrv(p, T) reads or writes a T at an address, and @ptrv(p, T, i) the i-th T after it. p.@offset(n) moves an address by n bytes:
use valk.mem
fn main() {
@unsafe
let buffer = mem.alloc(16)
defer mem.free(buffer)
@ptrv(buffer, u32, 0) = 7
@ptrv(buffer, u32, 1) = 9
println(@ptrv(buffer, u32, 1)) // 9
println(@ptrv(buffer.@offset(4), u32)) // 9
}
Manual memory#
Memory from valk.mem is not garbage collected; release it with mem.free:
use valk.mem
struct Point {
x: i32
y: i32
}
fn main() {
@unsafe
let point = mem.new[Point](.{ x: 1, y: 2 })
defer mem.free(point)
point.x = 5
println(point.x + point.y) // 7
}
| Function | Does |
|---|---|
mem.alloc(size) |
Allocate size bytes, uninitialized |
mem.calloc(size) |
Allocate size bytes set to zero |
mem.new[T](value) |
Allocate one T holding value, as a *T |
mem.resize(p, size, new_size) |
Move to a new allocation of new_size bytes |
mem.free(p) |
Release memory from any of the above |
mem.copy_bytes(from, to, n) |
Copy n bytes; ranges must not overlap |
mem.move_bytes(from, to, n) |
Copy n bytes; ranges may overlap |
mem.clear_bytes(p, n) |
Set n bytes to zero |
mem.equals_bytes(a, b, n) |
Compare n bytes |
Don't store GC-managed objects in manual memory: the garbage collector does not look there.
Casts#
.@cast(T) reinterprets a value as another type without converting it:
fn main() {
let n: u32 = 0x01020304
let bytes = @ref(n).@cast(*[u8 x 4])
println(bytes[0]) // 4 on little-endian CPUs
let f: f64 = 1.0
println(@ptrv(@ref(f), u64)) // 4607182418800017408
}
Use .to(T) for ordinary number conversions; .@cast is for pointers and bit patterns. An integer becomes an address only through .@cast(ptr).
Unsafe functions#
Mark a function @unsafe when callers must uphold something the compiler can't check. It can then only be called inside an unsafe scope:
fn first_byte(text: String) u8 @unsafe {
return text.data_cstring.get(0)
}
fn main() {
@unsafe
println(first_byte("hi")) // 104
}
String.data_cstring is one such function: the C string it returns is only valid while the string is alive.
