Pointers
A raw pointer holds the address of a value. *T is a read-only pointer: the value it points at can be read through it. *var T is a writable pointer: the value can also be written. A pointer may be null, may be stored in a field, returned, offset and compared, and may outlive what it points at — the compiler checks only what the pointer type allows, never whether the address is still good. That is the price of the things only a pointer can do: foreign functions, manual allocation, stored links between values, and walking memory by address. Because the compiler cannot check what a pointer addresses, every operation that reaches through one is written inside an unsafe block.
pointer-type = "*" type
| "*" "var" type
address-of = "@" place
dereference = "*" expression
unsafe-block = "unsafe" block
var score = 10;
let writer = @score; // *var int: score is a var
let reader: *int = @score; // a read-only view of the same int
// Safety: both pointers address score, which is live for the whole block.
unsafe {
*writer += 5; // score is now 15
let current = *reader; // 15
}
For safe, checked access to a value someone else owns, use a reference instead; a pointer is the deliberate step outside those checks.
Taking an address
The prefix @ takes the address of a place: a variable, a field, an element, or the target of another pointer. @x.field and @a[i] address one part of a value. Taking an address is safe; using it is not. The pointer's writability comes from the place:
| Place | @place has type |
|---|---|
a var binding, or a part of one | *var T |
a let binding, or a part of one | *T |
an element of a var T[..] view | *var T |
an element of a T[..] view | *T |
a field or element reached through a *var T | *var T |
error: cannot assign '*int' to '*var int': '@limit' yields a read-only '*T'; declare 'limit' with 'var' for a '*var T'
& never takes an address in Rux: &x is rejected with '&' does not take an address; write '@' to take the address of a value. A pointer is made only from an address, so a plain value where a pointer is expected is the error cannot assign 'int' to '*int'.
Taking the writable address of a variable declared without a value counts as initialising it, because the address exists to be filled through: var info: SystemInfo; GetSystemInfo(@info); leaves info usable. See Initialisation.
Reading and writing through a pointer
The prefix * dereferences a pointer: *p is the value it points at, a place that can be read, and written when p is a *var T:
unsafe func Store(target: *var int, value: int) {
*target = value;
}
error: cannot modify data through read-only pointer '*int'
The pointee type, not the variable it points at, decides what is allowed: a *int cannot write even when it points at a var.
A write through a pointer initialises the storage it addresses and destroys nothing. The pointer cannot tell whether that storage holds a value yet, so *p = value, *p = move value, p[i] = value and a field write through a *var T never run a destructor for what was there before. Code that replaces a value through a pointer destroys or moves out the old one first:
struct Noisy {
id: int;
}
extend Noisy {
func ~Noisy(&var self) {
PrintLine("destroy {}", self.id);
}
}
func Main() -> int {
var slot = Noisy { id: 1 };
slot = Noisy { id: 2 }; // assignment: destroys 1
let p = @slot;
// Safety: p addresses slot, which is live.
unsafe {
*p = Noisy { id: 3 }; // through a pointer: 2 is never destroyed
}
return 0; // destroys 3
}
destroy 1
destroy 3
Moving a value out through a pointer is rejected, because the pointer does not own it: let mine = move *p; fails with cannot move '*p' out of borrowed pointer storage.
Fields, elements and methods
. reaches through a pointer to a struct, and [i] through a pointer to an element; there is no arrow operator. Reads and writes go straight to the pointed-at value, inside unsafe:
struct Point {
x: int;
y: int;
}
func Main() -> int {
var point = Point { x: 1, y: 2 };
let p = @point;
// Safety: p addresses point, which is live.
let sum = unsafe {
p.x = 7; // point.x is now 7
p.x + p.y
};
return 0;
}
A method whose receiver is a reference is not called through a raw pointer, because a pointer proves nothing about the address it holds. Dereference first, unsafe { (*p).Shift(3) }, to vouch for it explicitly:
error: cannot call method 'Shift', whose receiver is '&var Point', through raw pointer '*var Point'
note: a raw pointer does not prove a valid, non-null borrow, so it is not borrowed implicitly
help: dereference it where it is known to be valid: '(*p).Shift(...)'
A method that works on addresses declares a raw receiver, *self or *var self, and is called on the pointer itself; a field it reads through self needs unsafe like any other.
Pointers nest: **T is a pointer to a pointer, and each * removes one level, so with pp: **var int, **pp = 1 writes the int.
Conversions
| From | To | How | Needs unsafe |
|---|---|---|---|
*var T | *T | implicitly | no |
*T | *opaque | implicitly; *var T likewise to *var opaque | no |
*opaque | *T | as *T; likewise as *var T | yes |
*T | *U | as *U | yes |
*T | *var U | never, for any U but opaque | — |
| a pointer | uint | as uint — the address as a number | no |
uint | a pointer | as *T | yes |
Losing write access is always allowed; gaining it is not: cannot assign '*int' to '*var int'. A cast with as is the explicit, unchecked way to retype an address — see Casts — but it cannot add write access either, whatever pointee it names, so @x as *var int and @x as *var uint64 on a let x are refused. Untyped memory is the one way through: writable memory reached through an *opaque, such as a fresh allocation, is retyped with as *var T, and a *T may become a *var opaque for a C function that takes a void * it only reads. A cast that gives an address a type it did not have needs an unsafe context; one that only forgets what a pointer says is safe:
error: cannot cast '*int' to '*var uint64', which would give a read-only pointer write access
error: casting '*opaque' to '*var int' requires 'unsafe'
null
null is the pointer that points at nothing. It converts to every pointer type, and it is the only value a pointer has that is not an address. Compare with == and != — which are safe — before dereferencing a pointer that might be null:
struct Node {
value: int;
next: *Node;
}
func Main() -> int {
var tail = Node { value: 3, next: null };
var head = Node { value: 1, next: @tail };
var cursor: *Node = @head;
while cursor != null {
// Safety: cursor is not null and addresses a live Node of this list.
unsafe {
PrintLine("{}", cursor.value);
cursor = cursor.next;
}
}
return 0;
}
Reading or writing through null is not checked: it compiles, and crashes the program when it runs.
null is a pointer value, not absence. It is not a value of an optional type, and where an optional of a pointer is expected it is a present null pointer:
| Type | Meaning |
|---|---|
(*T)? | an optional pointer: none, or a pointer |
*T? | a pointer to an optional, *(T?) |
let present: (*int)? = null; // present, holding a null pointer
let absent: (*int)? = none; // absent
*opaque
*opaque and *var opaque point at bytes of no particular type, as C's void * does. Allocation functions return them, and foreign functions take and return them. Any typed pointer converts to one implicitly; to use the memory, cast it back to a typed pointer:
import Memory::{ Alloc, Free };
func Main() -> int {
let block = Alloc(4 * sizeof(int));
if block == null {
return 1;
}
// Safety: block was allocated above and is freed once, when Main returns.
defer unsafe { Free(block); }
// Safety: block holds room for four ints.
unsafe {
let values = block as *var int;
values[0] = 1;
}
return 0;
}
Arithmetic on an *opaque moves by single bytes.
Comparison
Two pointers compare with == and !=, which compare addresses, and with <, <=, > and >=, which order them — useful when walking a buffer up to an end pointer. Comparing pointers is safe. See Pointer arithmetic.
What a pointer does not have
A pointer has no length: p.length is an error. It cannot be iterated with for, and slicing it needs an explicit end. To treat raw memory as a sequence, build a view of it with a count, p[..n], and iterate that, for value in p[..n]:
error: cannot iterate over pointer '*int32'
help: iterate a slice of the memory it points at, as in 'for value in p[..count]'
A pointer is 8 bytes on every supported target, whatever it points at.
Unsafe code
The compiler checks references, bounds, ownership and borrows, but it cannot check what a raw pointer addresses, what a foreign function does, or what an unsafe func expects of its caller. The operations that depend on such facts are allowed only in an unsafe context — an unsafe { … } block or the body of an unsafe func — so every place a program's soundness rests on a promise can be found by searching for unsafe.
| Needs an unsafe context | Safe |
|---|---|
*p, *p = v, p[i], a range p[a..b], a field p.x through *T or *var T, and @p.x or @p[i] | taking an address with @ |
p + n, n + p, p - n, p += n, p -= n | copying, comparing and null-testing pointers |
a cast that gives an address a new type: an integer to a pointer, *opaque to *T, *A to *B | a cast that only forgets: *var T to *T, or to *opaque |
calling an extern function, an asm function or an unsafe func, or naming one as a value | converting a pointer to an integer |
reading or writing an extern variable | @ of an extern variable |
making an interface handle from a pointer, p as Shape | borrowing a value as an interface view, &Shape |
Each such operation outside an unsafe context is an error that names it:
error: dereferencing a raw pointer requires 'unsafe'
note: the compiler cannot prove that a raw pointer addresses a live value of its type
help: wrap it in 'unsafe { ... }' and say why it holds in a '// Safety:' comment above it
The others read indexing a raw pointer requires 'unsafe', accessing field 'next' through a raw pointer requires 'unsafe', pointer arithmetic requires 'unsafe', casting '*opaque' to '*var Node' requires 'unsafe', call to unsafe function 'Copy' requires 'unsafe', call to extern function 'write' requires 'unsafe' and making interface value 'Shape' from pointer '*var Square' requires 'unsafe'.
An unsafe block only grants permission; nothing about the program's behaviour changes. In value position it yields its last expression written without a ;, so a single read is let first = unsafe { *p };. At the start of a statement it is the statement, ends at its } and yields nothing, so a value it ends with is an error (this 'unsafe' block yields a value of type 'int' that is not used). Its locals are destroyed at its end, return, break and continue leave it as they leave any block, and defer unsafe { … } defers it. A block that holds no operation needing it, or one inside another unsafe context, is the warning unnecessary 'unsafe' block.
unsafe func declares a function whose caller carries an obligation the compiler cannot check, such as Memory::Copy, whose ranges must be valid for the length it is given. A call to it needs an unsafe context, and its body is one. The keyword stands directly before func, as in pub unsafe func, also for a method. An extern or asm function is always unsafe to call, so unsafe on one is an error, and a destructor, an operator and an interface requirement or its implementation cannot be unsafe, because nothing marks where they are called: destructor '~Holder' cannot be 'unsafe', because nothing marks where it is called. A function whose safety does not depend on its caller stays safe and keeps its raw operations in unsafe blocks inside its body.
rux lint asks for the reason: its rule unsafe.comment warns about an unsafe block unless the comment directly above the line holding it has a line starting Safety:, and about an unsafe func unless one does above its declaration — usually a /// Safety: line of its documentation, saying what a caller must uphold. The comment says why the operation is sound at that place, as the examples on this page do.
See also
- Pointer arithmetic — moving a pointer by whole elements
- Pointer slicing — a bounded view of the memory a pointer addresses
- References — checked, non-null access to a value
- Foreign functions — how pointers cross into C
- Layout — the size and alignment of what a pointer addresses
- Learn: Pointer, Out parameter, Raw memory, Optional pointer