rokdocs
v0.1

1.1. Pointer family - Ptr / ConstPtr / ManyPtr / CPtr

The rok::bare::ptr module defines five kinds of raw pointer. The design follows Zig's arity-encoded model: the number of pointees (arity) and mutability are encoded in the type itself, ruling out silent coercion entirely.

1.1.1. The 5 pointer kinds

The table below shows how the five kinds divide.

TypeMeaningC equivalentNullable
Ptr<T>mutable, exactly 1 TT*✗ (use Ptr<T> | none)
ConstPtr<T>const, exactly 1 Tconst T*✗
ManyPtr<T>mutable, N contiguous Ts (length unknown)T* (array)✗
Slice<T>mutable, N contiguous Ts (length included)(T*, len)✗ (separate niche)
CPtr<T>C-compatible nullable + 1T* (nullable)✓ (automatic)

Ptr, ConstPtr, ManyPtr and CPtr are #[repr(transparent)]: across extern "C" they are C pointers.

Of these, Slice<T> is a safe view defined in 2.9 collections; this module only defines the path for extracting its underlying ptr.

Send / Sync

The raw pointers Ptr<T> and CPtr<T> are !Send and !Sync (2.13.9). To pass a type containing a pointer between threads, its author must vouch for it with unsafe impl Send.

Automatic niche

Ptr<T>, ConstPtr<T> and ManyPtr<T> can use the null pointer value as a niche, so adding none requires no extra tag. This is an optimization, not a guarantee: an implementation may represent Ptr<T> | none as an ordinary tagged sum type.

sizeof::<Ptr<i32>>()         //→ 8 (64-bit target)
sizeof::<Ptr<i32> | none>() //→ 8 (implementation using the niche - not guaranteed)

CPtr<T>, on the other hand, is itself nullable, so it needs no separate sentinel; its none representation is simply CPtr::null().

The niche mechanism is covered in 2.2 non-zero.

1.1.2. Ptr<T> - strict provenance API

The public API of the most basic type, Ptr<T>, is:

struct Ptr<T> { /* opaque - (address, provenance) pair */ }

impl<T> Ptr<T> {
    // construction
    fn dangling() -> Ptr<T>;           // valid alignment, invalid address (unique)
    fn null() -> Ptr<T> | none;         // always none

    // provenance-preserving (strict provenance, safety first)
    fn addr(self) -> usize;             // address only (provenance lost)
    fn with_addr(self, addr: usize) -> Ptr<T>;
                                        // provenance kept, address changed
    fn map_addr(self, f: fn(usize) -> usize) -> Ptr<T>;

    // arithmetic (provenance preserving)
    fn offset(self, count: isize) -> Ptr<T>;       // count * sizeof(T) byte offset
    fn add(self, count: usize) -> Ptr<T>;
    fn sub(self, count: usize) -> Ptr<T>;
    fn byte_offset(self, count: isize) -> Ptr<T>;  // offset in bytes

    fn distance(self, origin: Ptr<T>) -> isize;     // self.addr() - origin.addr()
    fn is_aligned(self) -> bool;
    fn align_offset(self, align: usize) -> usize;

    // dereference (UB if invalid)
    unsafe fn read(self) -> T;
    unsafe fn write(self, value: T);
    unsafe fn read_volatile(self) -> T;
    unsafe fn write_volatile(self, value: T);
}
// module functions of rok::bare::ptr
fn addr_of<T>(place: T) -> Ptr<T>;
fn many_addr_of<T>(place: T) -> ManyPtr<T>;
fn expose_provenance<T>(p: Ptr<T>) -> usize;
unsafe fn from_exposed_provenance<T>(addr: usize) -> Ptr<T>;

addr_of / many_addr_of take a place expression: a local variable or a static (an extern "C" static included), or a field or element of one. A pointer to a local is valid while that variable is in scope; a pointer to a static for the whole run (statics live in data sections; a lazily initialized static is initialized before its address is taken; GC references held by statics are roots). Writing through a pointer to a non-mut static is undefined behavior. A variable whose address is taken stays a GC root for that scope, and roots pin, so the referent does not move. In a coroutine body the pointer is valid only until the next suspension point. Using such a pointer after a suspension point is a compile error.

Strict provenance - separated escape hatch

Conversion usize → Ptr<T> is rejected by default, because dereferencing a ptr whose provenance was lost is UB. The normal path and the escape hatch are therefore clearly separated.

// recommended - provenance preserved
let p: Ptr<i32> = ...;
let q = p.with_addr(p.addr() + 4);     // 4 bytes further - provenance unchanged

// discouraged - fresh provenance (only for FFI / hardware registers)
use rok::bare::ptr::{expose_provenance, from_exposed_provenance};

let exposed: usize = expose_provenance(p);   // "exposes" p's provenance
let r: Ptr<i32> = unsafe { from_exposed_provenance(exposed) };
                                              // _assumes_ the new ptr has one of
                                              // the exposed provenances

The authoritative starting point of this design is Ralf Jung's "Pointers Are Complicated", and it is isomorphic to rok::core::ptr::with_exposed_provenance of Rust 1.84, stabilized in December 2024.

Example - pointer arithmetic

The short example below shows arithmetic in use.

let arr: [i32; 5] = [10, 20, 30, 40, 50];
let p: Ptr<i32> = rok::bare::ptr::addr_of(arr[0]);

let p3 = p.add(3);                    // ptr to arr[3]
unsafe {
    let v = p3.read();                //→ 40
}

// byte arithmetic (struct field offset)
struct Header { magic: u32; size: u64; }
let h: Ptr<Header> = ...;
let size_ptr: Ptr<u64> = h.byte_offset(4).cast::<u64>();
                                      // location of Header.size
                                      // (alignment check needed - see 1.2 cast)

1.1.3. ConstPtr<T> - read-only

The read-only variant lacks the write family.

struct ConstPtr<T> { /* opaque */ }

impl<T> ConstPtr<T> {
    fn dangling() -> ConstPtr<T>;

    fn addr(self) -> usize;
    fn with_addr(self, addr: usize) -> ConstPtr<T>;
    fn offset(self, count: isize) -> ConstPtr<T>;
    fn add(self, count: usize) -> ConstPtr<T>;
    fn sub(self, count: usize) -> ConstPtr<T>;

    unsafe fn read(self) -> T;
    unsafe fn read_volatile(self) -> T;
    // no write methods - read-only
}

The safety of conversion to and from Ptr<T> depends on direction. Adding const is free; removing it must go through the named cast rok::bare::const_cast (a safe call; writing through the result is unsafe).

let m: Ptr<i32> = ...;
let c: ConstPtr<i32> = m.as_const();        // Ptr → ConstPtr (safe)

let c: ConstPtr<i32> = ...;
let m: Ptr<i32> = rok::bare::const_cast(c);    // ConstPtr → Ptr (safe call; writes through m are unsafe - 1.2 cast)

Removing const therefore happens only through an explicit path; silent conversion is not accepted.

1.1.4. ManyPtr<T> - N contiguous, length unknown

Next is the type that points to several elements without length information.

struct ManyPtr<T> { /* opaque */ }

impl<T> ManyPtr<T> {
    fn addr(self) -> usize;
    fn with_addr(self, addr: usize) -> ManyPtr<T>;

    fn offset(self, count: isize) -> ManyPtr<T>;
    fn add(self, count: usize) -> ManyPtr<T>;

    unsafe fn read(self, idx: usize) -> T;
    unsafe fn write(self, idx: usize, value: T);

    // no length information - the caller guarantees the index
    unsafe fn slice(self, len: usize) -> Slice<T>;        // attaches a length to make a Slice
}

It is used to accept C's T* (array); since there is no length information, avoiding OOB is the caller's responsibility. slice over memory outside the GC heap must not hold elements with GC references; that is undefined behavior.

extern "C" {
    fn read_buffer(out: ManyPtr<u8>, len: usize) -> usize;
}

let buf: [u8; 1024] = [0; 1024];
let read_count = unsafe {
    read_buffer(rok::bare::ptr::many_addr_of(buf[0]), 1024)
};

1.1.5. CPtr<T> - C-compatible nullable

The nullable variant tied directly to the C ABI is:

struct CPtr<T> { /* opaque - nullable */ }

impl<T> CPtr<T> {
    fn null() -> CPtr<T>;
    fn from_ptr(p: Ptr<T>) -> CPtr<T>;       // non-null
    fn from_const(p: ConstPtr<T>) -> CPtr<T>;

    fn is_null(self) -> bool;
    fn as_ptr(self) -> Ptr<T> | none;        // null → none
    fn as_const(self) -> ConstPtr<T> | none;

    fn addr(self) -> usize;                  // null = 0
}

It fits at boundaries where C functions exchange nullable T*. Typical use:

extern "C" {
    fn malloc(size: usize) -> CPtr<u8>;       // null on failure
    fn free(ptr: CPtr<u8>);
}

let buf: CPtr<u8> = unsafe { malloc(1024) };
match buf.as_ptr() {
    p: Ptr<u8> => use(p),
    none       => panic("malloc failed"),
}
unsafe { free(buf) };

Ptr<T> | none is ABI-compatible thanks to the automatic niche, but CPtr alone is not enough where an explicit sentinel such as -1 is needed. In that case the standard approach is to model it separately with #[niche(-1)] on a newtype.

1.1.6. Conversion matrix

The ways between these five types are summarized in a single matrix.

from \ toPtr<T>ConstPtr<T>ManyPtr<T>CPtr<T>Slice<T>usize
Ptr<T>-.as_const() safe.as_many() safeCPtr::from_ptr()rok::bare::ptr::slice(p, len).addr() provenance loss
ConstPtr<T>rok::bare::const_cast-.as_many()CPtr::from_const()rok::bare::ptr::slice.addr()
ManyPtr<T>.as_one() safe.as_const()-CPtr::from_ptr(...).slice(len) unsafe.addr()
CPtr<T>.as_ptr()? .as_const()?manual-manual.addr()
Slice<T>(raw extraction from a safe view).as_ptr() (ConstPtr).as_many_ptr()manual-.as_ptr().addr()
usizefrom_exposed_provenance ⚠from_exposed_provenance ⚠manualCPtr::from_addrmanual-

⚠ marks an escape hatch that creates fresh provenance.

1.1.7. Comparison with other languages

Comparing with other languages that have tackled the same problem makes rok's position clear.

AspectCRustZigrok
Single ptrT**const T / *mut T*T / *const TPtr<T> / ConstPtr<T>
Many ptrT* (ambiguous)*const T (ambiguous)[*]TManyPtr<T>
length-awarenone&[T][]TSlice<T>
nullable(every ptr)Option<*const T>?*TCPtr<T> or Ptr<T> | none
arity separationnonepartial (slice separate)✓ (*T / [*]T separate)✓ (Zig model)
strict provenancenone✓ (1.84+)none✓ (Rust model)
as cast rejectedallowedallowed (warning only)explicit @ptrCastfully rejected

As the table shows, rok's three weightiest decisions are: first, adopting Zig-style arity separation to divide the five kinds clearly; second, adopting the Rust 2024 model of strict provenance; and third, enforcing explicit casts so that as cannot be used on pointers at all. Together these three decisions avoid most of the pointer pitfalls of both C and Rust.

Edit this page on GitHub