Skip to content

Pointer

Chung Leong edited this page Jul 21, 2024 · 7 revisions

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,
    pointer: *StructA,
};

pub var a: StructA = .{ .number1 = 1, .number2 = 2 };
pub var b: StructB = .{
    .child = .{ .number1 = -1, .number2 = -2 },
    .pointer = &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);

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.pointer.number1 = 123;
module.b.child.print();
module.b.pointer.print();
module.a.print();
pointer-example-1.StructA{ .number1 = -123, .number2 = -456 }
pointer-example-1.StructA{ .number1 = 123, .number2 = 456 }
pointer-example-1.StructA{ .number1 = 1, .number2 = 2 }

Notice how a has been modified through the pointer.

Auto-vivification

Assignment to a pointer changes its target:

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

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

While the assignment to child altered the struct, the assignment to pointer actually changed the pointer's target to a new instance of StructA, created automatically by Zigar when it detected that the object given isn't an instance of StructA. It's equivalentt to doing the following:

module.b.pointer = new StructA({ number1: 123, number2: 456 });

Explicitly dereferencing

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

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

module.b.pointer['*'] = { number1: 123, number2: 456 };
module.a.print();

The above code is equivalent to the following Zig code:

b.pointer.* = .{ .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:

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

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

You can see once again here how assignment to a pointer changes its target (int was not set to 42).

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

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

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

Pointer arguments

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-3.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 { setI8 } from './pointer-example-4.zig';

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

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

import { findMeaning } from './pointer-example-4.zig';

const array = new Int8Array(1);
setI8(array, 42);
console.log(array);
Int8Array(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, ArrayBuffer,
[]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)).

If you pass an incompatible array, auto-vivification would occur. A object with its own memory gets created and its content filled with values from the given array. It is then passed to the function, gets modified, and is tossed out immediately. As that's unlikely the desired behavior, Zigar will issue a warning when that happens:

import { setI8 } from './pointer-example-4.zig';

const array = new Uint8Array(5);
setI8(array, 42);
console.log(array);
Implicitly creating an Int8Array from an Uint8Array
Uint8Array(5) [ 0, 0, 0, 0, 0 ]

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-5.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 ]
Clone this wiki locally