Pointer - chung-leong/zigar GitHub Wiki

A pointer is a variable that points to other variables. It holds a memory address. It also holds a length if it's a slice pointer.

Auto-deferenecing

Zigar auto-deferences a pointer when you perform a property lookup:

const std = @import("std");

pub const StructA = struct {
    number1: i32,
    number2: i32,

    pub fn print(self: StructA) void {
        std.debug.print("{any}\n", .{self});
    }
};

pub const StructB = struct {
    child: StructA,
    ptr: *StructA,
};

pub var a: StructA = .{ .number1 = 1, .number2 = 2 };
pub var b: StructB = .{
    .child = .{ .number1 = -1, .number2 = -2 },
    .ptr = &a,
};
import module from './pointer-example-1.zig';

console.log(module.b.child.number1, module.b.child.number2);
console.log(module.b.pointer.number1, module.b.pointer.number2);
-1 -2
1 2

In the example above, child is a struct in StructB itself while pointer points to a struct sitting outside. The manner of access is the same for both.

Assignment works the same way:

import module from './pointer-example-1.zig';

module.b.child.number1 = -123;
module.b.ptr.number1 = 123;
module.b.child.print();
module.b.ptr.print();
module.a.print();
pointer-example-1.StructA{ .number1 = -123, .number2 = -2 }
pointer-example-1.StructA{ .number1 = 123, .number2 = 2 }
pointer-example-1.StructA{ .number1 = 123, .number2 = 2 }

Notice how a has been modified through the pointer.

Certain operations that use Symbol.toPrimitive() trigger auto-defererencing of primitive pointers:

var int: i32 = 123;
pub var int_ptr = ∫
import module from './pointer-example-2.zig';

console.log(`${module.int_ptr}`);
console.log(Number(module.int_ptr));
console.log(module.int_ptr == 123);
console.log(module.int_ptr + 1);
123
123
true
124

Explicit dereferencing

In order to modify the target of a pointer as a whole, you need to explicitly deference the pointer:

const std = @import("std");

pub const StructA = struct {
    number1: i32,
    number2: i32,

    pub fn print(self: StructA) void {
        std.debug.print("{any}\n", .{self});
    }
};

pub var a: StructA = .{ .number1 = 1, .number2 = 2 };
pub var ptr: *StructA = &a;

var int: i32 = 123;
pub var int_ptr = ∫
import module from './pointer-example-3.zig';

module.ptr['*'] = { number1: 123, number2: 456 };
module.a.print();
pointer-example-3.StructA{ .number1 = 123, .number2 = 456 }

The above code is equivalent to the following Zig code:

b.ptr.* = .{ .number1 = 123, .number2 = 456 };
a.print();

In both cases we're accessing '*`. JavaScript doesn't allow asterisk as a name so we need to use the bracket operator.

Explicity dereferencing is also required when the pointer target is a primitive like integers:

import module from './pointer-example-3.zig';

console.log(module.int_ptr['*']);
module.int_ptr['*'] = 555;
console.log(module.int_ptr['*']);
123
555

Again, this is essentially the same syntax required for the same operation in Zig:

std.debug.print("{d}\n", .{int_ptr.*});
int_ptr.* = 555;
std.debug.print("{d}\n", .{int_ptr.*});

Auto-vivication

Zig pointers cannot point to regular JavaScript values like number, string, or object. Zig is a low-level language that where everything is just bytes. Only objects backed by an ArrayBuffer are valid point targets.

When you assign a regular JavaScript value to a Zig pointer, a process called "auto-vivication" occurs. Zigar automatically makes a valid pointer target come into being. Consider the following:

const std = @import("std");

pub const I32 = i32;

pub fn set(int_ptr: *i32, value: i32) void {
    std.debug.print("before = {d}, after = {d}\n", .{ int_ptr.*, value });
    int_ptr.* = value;
}
import { set } from './pointer-example-4.zig'

var a = 1234;
set(a, 5678);
console.log(a);
before = 1234, after = 5678
1234

int_ptr of set() cannot point at a. What actually happens here is that a new I32 object comes into being automatically and is initialialized using the value of a. The bytes in this new object are what int_ptr points at. These bytes are modified by set() but are immediately discarded (and will eventually be reclaimed by the JavaScript garbage collector). a itself is not changed.

The code above is equivalent to the following:

import { set, I32 } from './pointer-example-4.zig'

var a = 1234;
set(new I32(a), 5678);
console.log(a);

Generally, you would use pointers to send data to the Zig side. You shouldn't use them to capture side-effects. Work done on the Zig side should be sent to JavaScript through a function's return value or through a Promise.

The following example demonstrates how to provide a structure containing pointers to a function. The structure in question is a simplified directory tree:

const std = @import("std");

pub const File = struct {
    name: []const u8,
    data: []const u8,
};
pub const Directory = struct {
    name: []const u8,
    entries: []const DirectoryEntry,
};
pub const DirectoryEntry = union(enum) {
    file: *const File,
    dir: *const Directory,
};

fn indent(depth: u32) void {
    for (0..depth) |_| {
        std.debug.print("  ", .{});
    }
}

fn printFile(file: *const File, depth: u32) void {
    indent(depth);
    std.debug.print("{s} ({d})\n", .{ file.name, file.data.len });
}

fn printDirectory(dir: *const Directory, depth: u32) void {
    indent(depth);
    std.debug.print("{s}/\n", .{dir.name});
    for (dir.entries) |entry| {
        switch (entry) {
            .file => |f| printFile(f, depth + 1),
            .dir => |d| printDirectory(d, depth + 1),
        }
    }
}

pub fn printDirectoryTree(dir: *const Directory) void {
    printDirectory(dir, 0);
}
import { printDirectoryTree } from './pointer-example-5.zig';

const catImgData = new ArrayBuffer(8000);
const dogImgData = new ArrayBuffer(16000);

printDirectoryTree({
    name: 'root',
    entries: [
        { file: { name: 'README', data: 'Hello world' } },
        {
            dir: {
                name: 'images',
                entries: [
                    { file: { name: 'cat.jpg', data: catImgData } },
                    { file: { name: 'dog.jpg', data: dogImgData } },
                ]
            }
        },
        {
            dir: {
                name: 'src',
                entries: [
                    { file: { name: 'index.js', data: 'while (true) alert("You suck!")' } },
                    { dir: { name: 'empty', entries: [] } },
                ]
            }
        }
    ]
});
root/
  README (11)
  images/
    cat.jpg (8000)
    lobster.jpg (16000)
  src/
    index.js (31)
    empty/

As you can see in the JavaScript code above, you don't need to worry about creating the pointer targets at all. Zigar handles this for you. First it autovivificate a Directory struct expected by printDirectoryTree, then it autovivificates a slice of DirectoryEntry with three items. These items are in term autovivificated, first a File struct, then two Directory structs. For each of these a slice of u8 is autovivificated using the name given.

Basically, you can treat a pointer to a struct (or any type) as though it's a struct. Just supply the correct initializers.

Auto-casting

In the previous section's example, both a string and an ArrayBuffer were used as data for a File struct:

                    { file: { name: 'index.js', data: 'while (true) alert("You suck!")' } },
const catImgData = new ArrayBuffer(8000);
/* ... */
                    { file: { name: 'cat.jpg', data: catImgData } },

In the first case, auto-vification was trigged. In the second case, something else happened instead: auto-casting. The bytes in catImgData were interpreted as a slice of u8. No copying occurred. The []const data pointer ended up pointing directly to catImgData. Had the function made changes through this pointer, they would show up in catImgData.

Let us look at a different example where we have a non-const pointer argument:

pub fn setI8(array: []i8, value: i8) void {
    for (array) |*element_ptr| {
        element_ptr.* = value;
    }
}
import { setU8 } from './pointer-example-6.zig';

const buffer = new ArrayBuffer(5);
setU8(buffer, 8);
console.log(buffer);
ArrayBuffer { [Uint8Contents]: <08 08 08 08 08>, byteLength: 5 }

As you can see, the function modifies the buffer. A []u8 pointer also accepts a typed array:

import { setU8 } from './pointer-example-6.zig';

const array = new Uint8Array(1);
setU8(array, 42);
console.log(array);
Uint8Array(5) [ 42, 42, 42, 42, 42 ]

The chart below shows which pointer type is compatible with which JavaScript objects:

Zig pointer type JavaScript object types
[]u8 Uint8Array, Uint8ClampedArray, DataView, ArrayBuffer
[]i8 Int8Array, DataView
[]u16 Unt16Array, DataView
[]i16 Int16Array, DataView
[]u32 Uint32Array, DataView
[]i32 Int32Array, DataView
[]u64 BigUint64Array, DataView
[]i64 BigInt64Array, DataView
[]f32 Float32Array, DataView
[]f64 Float64Array, DataView

These mappings are also applicable to single pointers (e.g. *i32) and slice pointers to arrays and vectors (e.g. [][4]i32, []@Vector(4, f32)).

Explicit casting

Pointers to structs require explicit casting:

const std = @import("std");

pub const Point = extern struct { x: f64, y: f64 };
pub const Points = []const Point;

pub fn printPoint(point: *const Point) void {
    std.debug.print("({d}, {d})\n", .{ point.x, point.y });
}

pub fn printPoints(points: Points) void {
    for (points) |*p| {
        printPoint(p);
    }
}
import { Point, Points, printPoint, printPoints } from './pointer-example-7.zig';

const array = new Float64Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]);
printPoints(Points(array.buffer));
const view = new DataView(array.buffer, 16, 16);
printPoint(Point(view));
(1, 2)
(3, 4)
(5, 6)
(7, 8)
(9, 10)
(3, 4)

Resizing pointer target

You can change the length of a slice pointer:

var numbers = [_]u32{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
pub const ptr: []u32 = &numbers;
import { ptr } from './pointer-example-6.zig';

console.log('Before:', [ ...ptr ]);
ptr.length = 5;
console.log('After:', [ ...ptr ]);
ptr.length = 10;
console.log('Restored:', [ ...ptr ]);
Before: [
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]
After: [ 0, 1, 2, 3, 4 ]
Restored: [
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]

Changing the length of a pointer changes its target:

import { ptr } from './pointer-example-6.zig';

const before = ptr['*'];
ptr.length = 5;
const after = ptr['*'];
ptr.length = 10;
const restored = ptr['*'];

console.log(`before === after: `, before === after);
console.log('before === restored:', before === restored);
before === after: false
before === restored: true

You cannot expand a slice pointer to beyond its target's original length:

import { ptr } from './pointer-example-6.zig';

try {
    ptr.length = 11;
} catch (err) {
    console.log(err.message);
}
Length of slice can be 10 or less, received 11

Many-item pointers

Unlike slice pointers ([]T), many-item pointers ([*]T) do not have explicit lengths. Zigar deals with the situation by assigning an initial length of one. To access the complete list you need to manually set the correct length:

var numbers = [_]u32{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
pub var ptr: [*]u32 = &numbers;

// export a function so Zigar would link the module
pub fn dummy() void {}
import module from './many-item-pointer-example-1.zig';

console.log([ ...module.ptr ]);
module.ptr.length = 10;
console.log([ ...module.ptr ]);
[ 0 ]
[
  0, 1, 2, 3, 4,
  5, 6, 7, 8, 9
]

It's possible to access memory outside the actual range:

import module from './many-item-pointer-example-1.zig';

console.log([ ...module.ptr ]);
module.ptr.length = 12;
console.log([ ...module.ptr ]);
module.ptr.length = 10_000_000;
console.log([ ...module.ptr ]);
[ 0 ]
[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 4278845440 ]
Segmentation fault (core dumped)

When a many-item pointer has a sentinel value, Zigar uses it to determine the initial length:

var numbers = [_]u32{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
pub var ptr: [*:5]u32 = @ptrCast(&numbers);

// export a function so Zigar would link the module
pub fn dummy() void {}
import module from './many-item-pointer-example-2.zig';

console.log([ ...module.ptr ]);
[ 0, 1, 2, 3, 4, 5 ]

C pointers

C pointers behave like many-item pointers except that they can point at a single item and null:

const std = @import("std");

const Point = extern struct {
    x: f64,
    y: f64,
};

pub fn print(ptr: [*c]Point) callconv(.C) void {
    if (ptr != null) {
        std.debug.print("{any}\n", .{ptr.*});
    } else {
        std.debug.print("{any}\n", .{ptr});
    }
}
import { print } from './c-pointer-example-1.zig';

print({ x: 123, y: 456 });
print([ { x: 123, y: 456 }, { x: 200, y: 300 } ]);
print(null);
c-pointer-example-1.Point{ .x = 1.23e2, .y = 4.56e2 }
c-pointer-example-1.Point{ .x = 1.23e2, .y = 4.56e2 }
c-pointer-example-1.Point@0

The following code does not work as you would expect:

const std = @import("std");

pub fn print(ptr: [*c]u32) void {
    std.debug.print("{any}\n", .{ptr.*});
}
import { print } from './c-pointer-example-2.zig';

print(123);
0

This is because Zigar interprets a number given to a pointer constructor as a request to create a slice of that length:

pub const CPtrU32 = [*c]u32;
import { CPtrU32 } from './c-pointer-example-3.zig';

const slice = new CPtrU32(5);
console.log([ ...slice ]);
[ 0, 0, 0, 0, 0 ]

Since you would never pass a single int or float by pointer, this quirk is just something to keep in mind.

Pointer to anyopaque

*anyopaque (or void* in C) behaves like [*]u8:

pub const PtrVoid = *anyopaque;
import { PtrVoid } from './anyopaque-pointer-example-1.zig';

const buffer = new PtrVoid(5);

console.log([ ...buffer ]);
console.log(buffer.typedArray);

The constructor of *anyopaque will accept a string as argument:

const c = @cImport(
    @cInclude("stdio.h"),
);

pub const PtrVoid = *anyopaque;

pub const fopen = c.fopen;
pub const fclose = c.fclose;
pub const fwrite = c.fwrite;
import { fopen, fclose, fwrite, PtrVoid } from './anyopaque-pointer-example-2.zig';

const f = fopen('anyopaque-pointer-example-2-out.txt', 'w');
const buffer = new PtrVoid('Cześć! Jak się masz?\n');
fwrite(buffer, buffer.length, 1, f);
fclose(f);

*anyopaque can point to any Zig data object and any JavaScript object backed by an ArrayBuffer:

const std = @import("std");

pub const Point = struct {
    x: u32,
    y: u32,
};
pub const Points = []Point;

pub fn memset(ptr: *anyopaque, byte_count: usize, value: u8) void {
    const bytes: [*]u8 = @ptrCast(ptr);
    for (0..byte_count) |index| {
        bytes[index] = value;
    }
}
import { __zigar, Points, Point, memset } from './anyopaque-pointer-example-3.zig';
const { sizeOf } = __zigar;

const point = new Point({ x: 0, y: 0 });
memset(point, sizeOf(Point), 0xFF);
console.log(point.valueOf());

const points = new Points(8);
memset(points, points.length * sizeOf(Point), 0xFF);
console.log(points.valueOf());

const ta = new Uint32Array(4);
memset(ta, ta.length * 4, 0xFF);
console.log(ta);
{ x: 4294967295, y: 4294967295 }
[
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 },
  { x: 4294967295, y: 4294967295 }
]
Uint32Array(4) [ 4294967295, 4294967295, 4294967295, 4294967295 ]