Cone Language Reference

Raw pointers: *T

On this page

A reference comes with guarantees: it points at a live value, through a permission, for no longer than its lifetime allows. A raw pointer comes with none of them. It may be null, it may step through memory by arithmetic, and nothing checks where it points. That is what memory from C looks like, and what a data structure the compiler cannot check is built on. This page covers making pointers, null, arithmetic, comparing, and reading and writing through them, so you can work with raw memory knowing which promises have become yours. Here is a C string copied by hand:

mod strcopy;

import stdio use *;
import libc;

fn main() {
  let text &Array[u8] = "pointers";

  // Room for the bytes and the zero that ends a C string, from C's allocator
  let block = malloc(text.len + 1usize);
  if block == null {
    panic("out of memory");
  }

  // Copy byte by byte, stepping two pointers along
  var src = text as *u8;            // the address of text[0]
  var dst = block;
  var left = text.len;
  while left > 0usize {
    *dst++ = *src++;
    left -= 1usize;
  }
  *dst = 0u8;                       // the zero C looks for

  // Indexing a pointer is not bounds-checked
  print <- "first and last: ", block[0], " ", block[7], "\n";

  // The caller promises the zero, so the bytes can be read as a C string
  let copy = cstr.fromPtr(block);
  print <- "copied ", copy.len(), " bytes: ", copy.view(), "\n";

  libc.free(block);                 // C's memory is the program's to give back
}

It prints:

first and last: 112 115
copied 8 bytes: pointers

Try this: print block[8], the zero. block[20] would compile just as happily, and read whatever lies past the end of the block.

Every line that reads or writes through a pointer here is one the compiler cannot check, so in Cone's design it belongs inside trust: dereferencing, indexing, arithmetic, and casting a pointer to a reference. trust is not built yet, and until it is, these compile without it. Using pointers safely is the programmer's job alone.

Declaring and making a pointer#

A pointer's type is written with * where a reference has &, followed by the value's type and nothing else: no region, lifetime or permission.

let ptr1 *i32;   // a pointer to an integer

A pointer usually gets its value from a reference, or from an external function, as malloc's is above. A reference coerces to a pointer to the same type, or can be cast with as, which makes a copy of the address:

let ptr2 *i32 = &n;           // coerced
let ptr3 = ref1 as *i32;       // cast

A slice is an address and a count, so it is not a pointer, and none is made of it implicitly. as takes the address of its first element, the count dropped, for a C function that takes a buffer and a length, as text as *u8 does above (A slice is no pointer). A C string is not a pointer either but a cstr, which promises the zero that ends it; a C string that may be null stays a *u8, and cstr.fromPtr makes a cstr of one whose zero the caller vouches for.

The other way, a pointer may be cast to a reference, ptr as &i32. That is how a library hands out memory it manages as a safe reference, and it is a promise that the memory holds a live i32 for as long as the reference is used, so in Cone's design the cast needs trust, ref = trust {ptr1 as &i32}; (not built yet: today the cast is written without it).

A pointer does not belong to a region. Memory the program asked for explicitly, from malloc say, is the program's to free, as the example's last line does.

null#

The null pointer is written null. It takes the pointer type it is wanted as, so it may initialize or be assigned to a pointer, be passed for a pointer parameter or returned as one, fill a pointer field, or be compared with a pointer:

var ptr3 *i32 = null;
if ptr3 == null { ptr3 = ref1 as *i32; }

Only a raw pointer may be null: a reference never is, and an Option's absence is None. So null is an error anywhere it is wanted as something else, and anywhere nothing says which pointer type it is:

let p = null;          // **ERROR** 1237: Nothing here says which raw pointer type 'null' is. Give it
                       //   one where it is wanted, as in 'let p *u8 = null'.
let p *u8 = null;      // fine, and so is: null as *u8

A null pointer is false as a condition and every other pointer true, so if !p asks whether p is null. It is zero: 0usize as *i32 is the same pointer.

null is a constant, so it may be a global's initial value or a field's or parameter's default. So may a constant reinterpreted as a pointer with as, a fixed address a device or a C library documents. A struct's pointer fields can then start out set, and a new value need not mention them:

struct Buffer {
  pub base *u8 = null;
  pub guard *u8 = 4096usize as *u8;
  pub len usize = 0usize;
}

let b = new Buffer();     // b.base == null is true; b.guard as usize is 4096

A field's default must be a constant, of the kinds Structs lists (a literal, a named constant, a constant expression such as Red | Green, a variant of an enum that holds no value such as next Option[i32] = None, or a tuple or array of them). Two more kinds are for pointers: null, and a constant reinterpreted to a number or pointer type with as. A struct of such constants is one too; a variant that holds a value is not.

Pointer arithmetic#

A reference stands in for the value it points at. A pointer does not: operations on a pointer act on the pointer itself. Arithmetic moves it forward or back across elements of its type with ++, --, +, -, += or -=, before or after (*dst++ = *src++ above reads, writes, then moves both):

++p;                       // p, a var variable, points to the next element
let later = first + 4usize;

The step is the size of the type pointed at, not one byte: incrementing a *i32 moves it 4 bytes on, since a 32-bit integer is 4 bytes, and any number added to it is in effect multiplied by 4.

Comparing pointers#

Pointer comparisons compare the pointers, not what they point at (unlike references, whose == compares the values they refer to). Two pointers of the same type can be compared: == is true when both point to the same place, and === and !==, which ask that question of references, mean the same as == and != on pointers. <, > and the rest say which address is the greater, which is how to tell whether a pointer has run past the end of a block.

Reading and writing through a pointer#

As with references, * reaches the value a pointer points at, and [] treats the pointer as the start of an array, ptr1[2] being the third element on. Unlike a slice's, a pointer's index is not bounds-checked, which can save time where the index is known to be safe, and does harm where it is not. Both belong inside trust in Cone's design (not built yet):

trust {                    // Not built yet: today, write the line without trust
  ptr1[2] = ptr2[1];
}

Storing through a pointer, *ptr = value, treats the place as already holding a value of its type. Where that type is an owning reference, the reference the place held is released before the new one is stored; a value whose type has a final is not finalized. Reading a value of a type that moves with *ptr marks the variable ptr as moved. Memory that holds no value yet, or a value being moved out for good, is what the intrinsics mem.writeRaw and mem.readRaw are for.

You can now work with raw memory and know which promises you have taken on. Next is the mechanism that makes taking them on visible: trust.