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 = ∫
<?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 = ∫
<?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
)