-
Notifications
You must be signed in to change notification settings - Fork 3
Pointer
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.
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.
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 });
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
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.
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 ]
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)
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
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 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.
*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 ]