Pointers

Pointers in Spawn are similar to the references described earlier. Their behavior is as close as possible to pointers in C, which means that they are unsafe by default, but are still efficient and can be used to work with external libraries.

Unlike references, pointers can be null. The nil keyword is used to represent a null pointer. Although null pointers are usually described as unsafe, using nil does not require an unsafe block, since the presence of a null pointer itself is not dangerous, only to dereference of a null pointer, which requires an unsafe block, is dangerous.

Create a pointer

To create a pointer, as for references, the & and &mut operators are used, which are explicitly cast to the pointer type:

fn main() { number := 10 ptr := &number as *i32 println(ptr) // 0x7ffedc601a28 }

In this example, we create a pointer to a variable number of type i32 by taking a reference to it using the & operator and explicitly casting it to the pointer type *i32. This is the only way to create pointers in Spawn.

nil pointers do not have an explicit type, so when using nil as a value when creating a variable, you must explicitly specify the type:

fn main() { ptr := nil as *i32 println(ptr) // 0x0 }

Dereference

As with references, the * operator is used to access the value pointed to by a pointer. However, the compiler cannot be sure that the pointer is valid, so pointer dereference is an unsafe operation and must be done in an unsafe block:

fn main() { number := 10 ptr := &number as *i32 unsafe { println(*ptr) // 10 } }

Modify the value by pointer

As with references, the * operator is used to change the value of a pointer:

fn main() { number := 10 ptr := &number as *i32 unsafe { *ptr = 20 } println(number) // 20 }

However, there is no automatic dereferencing for pointers, and changing the value of a pointer is an unsafe operation and must be done in an unsafe block.

Pointer arithmetic

As in C, pointers can be modified using arithmetic operations. To understand what pointer arithmetic is, let's look at the following example:

import mem fn main() { ptr := mem.alloc(mem.size_of[i32]() * 3) as *i32 unsafe { *ptr = 10 *(ptr + 1) = 20 println(*(ptr + 1)) // 20 } }

In this small example, we allocate memory for three values of type i32 using the alloc function from the mem module, which returns a reference to the allocated memory, since references don't support arithmetic, we explicitly cast it to a pointer type *i32 .

Now let's look at what an allocated memory block looks like:

___________________
|  1  |  2  |  3  |
|_____|_____|_____|
   ^
   ptr

The ptr variable stores the address of the first element. In the first line of the unsafe block we write the value 10 to this address:

___________________
|  1  |  2  |  3  |
|_   _|_   _|_   _|
|  10 |     |     |
|_____|_____|_____|
   ^
   ptr

In the second line we write the value, but at an address equal to the address of the first element plus the size of one element. Note that +1 means shift by one element, not one byte.

___________________
|  1  |  2  |  3  |
|_   _|_   _|_   _|
|  10 |  20 |     |
|_____|_____|_____|
   ^     ^ ptr + 1
   ptr

And finally on the third line we get the value at ptr + 1, which is 20.

Be careful, if you try to write a value to address ptr + 200, the program will likely crash with a segmentation fault because you are trying to write a value to memory that has not been allocated.

The entry *(ptr + X) can be written in the more readable form ptr[X]:

import mem fn main() { ptr := mem.alloc(mem.size_of[i32]() * 3) as *i32 unsafe { ptr[0] = 10 ptr[1] = 20 println(ptr[1]) // 20 } }

Pointer arithmetic is defined only for array-like pointers, which are shown above, and for pointers to structure elements; otherwise the behavior is undefined.