Type ‣ Pointer ᴾᴴᴾ - chung-leong/zigar GitHub Wiki

JavaScript | PHP


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,
};
<?php

$m = zigar_use(__DIR__ . '/pointer-example-1.zig');

debug_zval_dump($m->b->child->number1, $m->b->child->number2);
debug_zval_dump($m->b->ptr->number1, $m->b->ptr->number2);
int(-1)
int(-2)
int(1)
int(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:

<?php

$m = zigar_use(__DIR__ . '/pointer-example-1.zig');

$m->b->child->number1 = -123;
$m->b->ptr->number1 = 123;
$m->b->child->print();
$m->b->ptr->print();
$m->a->print();
.{ .number1 = -123, .number2 = -2 }
.{ .number1 = 123, .number2 = 2 }
.{ .number1 = 123, .number2 = 2 }

Notice how $m->a has been modified through the pointer.

A primitive pointer gets dereferenced automatically when you place it in a double-quoted string. The same happens when you perform a arithmetic, comparison, or casting operation on it.

var int: i32 = 123;
pub var int_ptr = &int;
<?php

$m = zigar_use(__DIR__ . '/pointer-example-2.zig');

debug_zval_dump($m->int_ptr);
debug_zval_dump("{$m->int_ptr}");
debug_zval_dump($m->int_ptr == 123);
debug_zval_dump($m->int_ptr + 7);
debug_zval_dump($m->int_ptr - $m->int_ptr);
debug_zval_dump((double) $m->int_ptr);
object(*i32)#5 (0) refcount(2){
}
string(3) "123" refcount(1)
bool(true)
int(130)
int(0)
float(123)

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 = &int;
<?php

$m = zigar_use(__DIR__ . '/pointer-example-3.zig');

$m->ptr->{'*'} = [ 'number1' => 123, 'number2' => 456 ];
$m->a->print();
.{ .number1 = 123, .number2 = 456 }

The above code is equivalent to the following Zig code:

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

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

<?php

$m = zigar_use(__DIR__ . '/pointer-example-3.zig');

echo "{$m->int_ptr}\n";
$m->int_ptr->{'*'} = 555;
echo "{$m->int_ptr}\n";
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 PHP values like number, string, or object. Zig is a low-level language that where everything is just bytes. Only Zig objects are valid point targets.

When you assign a regular PHP 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;
}
<?php

$m = zigar_use(__DIR__ . '/pointer-example-4.zig');

$a = 1234;
$m->set($a, 5678);
debug_zval_dump($a);
before = 1234, after = 5678
int(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() then immediately discarded. $a itself is not changed.

The code above is equivalent to the following:

<?php

$m = zigar_use(__DIR__ . '/pointer-example-4.zig');

$a = 1234;
$m->set(new $m->I32($a), 5678);
debug_zval_dump($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 PHP 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);
}
<?php

$m = zigar_use(__DIR__ . '/pointer-example-5.zig');

$cat_img_data = new ArrayBuffer(8000);
$dog_img_data = new ArrayBuffer(16000);

$m->printDirectoryTree([
    'name' => 'root',
    'entries' => [
        [ 'file' => [ 'name' => 'README', 'data' => 'Hello world' ] ],
        [
            'dir' => [
                'name' => 'images',
                'entries' => [
                    [ 'file' => [ 'name' => 'cat.jpg', 'data' => $cat_img_data ] ],
                    [ 'file' => [ 'name' => 'dog.jpg', 'data' => $dog_img_data ] ],
                ]
            ]
        ],
        [ 
            '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)
    dog.jpg (16000)
  src/
    index.js (31)
    empty/

As you can see in the PHP 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' => 'README', 'data' => 'Hello world' ] ],
$cat_img_data = new ArrayBuffer(8000);
/* ... */
                    [ 'file' => [ 'name' => 'cat.jpg', 'data' => $cat_img_data ] ],

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 $cat_img_data. Had the function made changes through this pointer, they would show up in $cat_img_data.

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;
    }
}
<?php

$m = zigar_use(__DIR__ . '/pointer-example-6.zig');

$buffer = new ArrayBuffer(5);
$m->setU8($buffer, 8);
print_r($buffer);
ArrayBuffer Object
(
    [BYTES](/chung-leong/zigar/wiki/BYTES) => Array
        (
            [0] => 8
            [1] => 8
            [2] => 8
            [3] => 8
            [4] => 8
        )

    [byteLength] => 5
    [detached] => 
    [readOnly] => 
)

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

<?php

$m = zigar_use(__DIR__ . '/pointer-example-6.zig');

$array = new Uint8Array(5);
$m->setU8($array, 42);
print_r($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, ArrayBuffer
[]i8 Int8Array
[]u16 Unt16Array,
[]i16 Int16Array,
[]u32 Uint32Array,
[]i32 Int32Array,
[]u64 Uint64Array,
[]i64 Int64Array,
[]f32 Float32Array,
[]f64 Float64Array,
*anyopaquje all of the above

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);
    }
}
<?php

$m = zigar_use(__DIR__ . '/pointer-example-7.zig');

$array = new Float64Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]);
$m->printPoints($m->Points($array->buffer));
$subarray = new Uint8Array($array->buffer, 16, 16);
$m->printPoint($m->Point($subarray));
(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 var ptr: []u32 = &numbers;
<?php

$m = zigar_use(__DIR__ . '/pointer-example-8.zig');

echo "Before: ";
print_r($m->ptr->{'*'});
$m->ptr->__length = 5;
echo "After: ";
print_r($m->ptr->{'*'});
$m->ptr->__length = 10;
echo "Restored: ";
print_r($m->ptr->{'*'});
Before: [_]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
)
After: [_]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
)
Restored: [_]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
)

Changing the length of a pointer changes its target:

<?php

$m = zigar_use(__DIR__ . '/pointer-example-8.zig');

$before = $m->ptr->{'*'};
$m->ptr->__length = 5;
$after = $m->ptr->{'*'};
$m->ptr->__length = 10;
$restored = $m->ptr->{'*'};

echo "before === after: " . ($before === $after ? 'true' : 'false') . "\n";
echo "before === restored: " . ($before === $restored ? 'true' : 'false') . "\n";
before === after: false
before === restored: true

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

<?php

$m = zigar_use(__DIR__ . '/pointer-example-8.zig');

try {
    $m->ptr->__length = 11;
} catch (Exception $e) {
    echo $e->getMessage(), "\n";
}
unable to write field '__length' in pointer '[]u32': out of bound (zig)

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 {}
<?php

$m = zigar_use(__DIR__ . '/many-item-pointer-example-1.zig');

print_r($m->ptr);
$m->ptr->__length = 10;
print_r($m->ptr);
[*]u32 Object
(
    [0] => 0
)
[*]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
)

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

<?php

$m = zigar_use(__DIR__ . '/many-item-pointer-example-1.zig');

print_r($m->ptr);
$m->ptr->__length = 12;
print_r($m->ptr);
$m->ptr->__length = 10_000_000;
print_r($m->ptr);
[*]u32 Object
(
    [0] => 0
)
[*]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
    [5] => 5
    [6] => 6
    [7] => 7
    [8] => 8
    [9] => 9
    [10] => 7
    [11] => 15
)
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 produce a executable file when the example is compiled
// for WebAssembly
pub fn dummy() void {}
<?php

$m = zigar_use(__DIR__ . '/many-item-pointer-example-2.zig');

print_r($m->ptr);
[*]u32 Object
(
    [0] => 0
    [1] => 1
    [2] => 2
    [3] => 3
    [4] => 4
)

A sentinel value of 5 is, of course, highly unlikely. Zero is generally used in real world scenarios.

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});
    }
}
<?php

$m = zigar_use(__DIR__ . '/c-pointer-example-1.zig');

$m->print(x: 123, y: 456);
$m->print([ 
  [ 'x' => 200, 'y' => 300 ],
  [ 'x' => 400, 'y' => 500 ], 
]);
$m->print(null);
.{ .x = 123, .y = 456 }
.{ .x = 200, .y = 300 }
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.*});
}
<?php

$m = zigar_use(__DIR__ . '/c-pointer-example-2.zig');

$m->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;
<?php

$m = zigar_use(__DIR__ . '/c-pointer-example-3.zig');

$slice = new $m->CPtrU32(5);
print_r($slice);
[*c]u32 Object
(
    [0] => 0
    [1] => 0
    [2] => 0
    [3] => 0
    [4] => 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;
<?php

$m = zigar_use(__DIR__ . '/anyopaque-pointer-example-1.zig');

$buffer = new $m->PtrVoid(5);
print_r($buffer->__typed_array);
Uint8Array Object
(
    [0] => 0
    [1] => 0
    [2] => 0
    [3] => 0
    [4] => 0
)

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

const c = @import("c");
pub const fopen = c.fopen;
pub const fclose = c.fclose;
pub const fwrite = c.fwrite;

pub const PtrVoid = *anyopaque;
<?php

$m = zigar_use(__DIR__ . '/anyopaque-pointer-example-2.zig');

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

*anyopaque can point to any Zig data object and any 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;
    }
}
<?php

$m = zigar_use(__DIR__ . '/anyopaque-pointer-example-3.zig');

$zigar = $m->__zigar;

$point = new $m->Point(x: 0, y: 0);
$m->memset($point, $zigar->sizeOf($m->Point), 0xFF);
print_r($point);

$points = new $m->Points(3);
$m->memset($points, count($points) * $zigar->sizeOf($m->Point), 1);
print_r($points);

$ta = new Uint32Array(4);
$m->memset($ta, $ta->byteLength, 0xFF);
print_r($ta);
Point Object
(
    [x] => 4294967295
    [y] => 4294967295
)
[]Point Object
(
    [0] => Point Object
        (
            [x] => 16843009
            [y] => 16843009
        )

    [1] => Point Object
        (
            [x] => 16843009
            [y] => 16843009
        )

    [2] => Point Object
        (
            [x] => 16843009
            [y] => 16843009
        )

)
Uint32Array Object
(
    [0] => 4294967295
    [1] => 4294967295
    [2] => 4294967295
    [3] => 4294967295
)

Types