Skip to content

Pointer

Chung Leong edited this page Apr 15, 2024 · 4 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 point targets at all. Zigar handle 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)

Many-item and C pointers

Data structures pointed to by C pointers and many-item pointers lacking sentinel values are not accessible in JavaScript. Zigar simply has no way of determining the correct memory range. For this reason these pointers are treated as zero-length slices:

var numbers = [_]u32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 88 };
pub var ptr1: [*]u32 = &numbers;
pub var ptr2: [*c]u32 = &numbers;
pub var ptr3: [*:88]u32 = @ptrCast(&numbers);
import module from './pointer-example-6.zig';

console.log(module.ptr1.length);
console.log(module.ptr2.length);
console.log(module.ptr3.length);
0
0
10