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 ]