Docs/Advanced

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:

Valk
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 ptr argument, such as mem.free
  • Turning a ptr into 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:

Valk
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:

Valk
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:

Valk
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:

Valk
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.