-
Notifications
You must be signed in to change notification settings - Fork 3
Zig data structures in JavaScript
Zig is a low-level, bare-metal programming language. Data types are representated in a way that
essentially reflects how the CPU sees them. For instance, a variable of the type u32
(32-bit
integer) is just 4 bytes residing somewhere in the computer's memory. If it holds the value
0x01020304
, those 4 bytes would be 0x04 0x03 0x02 0x01
in an Intel machine
(little-endian laoutout). They
would be 0x01 0x02 0x3 0x4
in a machine with a machine with a big-endian PowerPC processor.
Composite types like struct and array are likewise just a sequence of bytes.
struct { x: f32, y: f32 }
has 8 bytes, with x
occupying the first 4 and y
occupying the
remainder. [12]f32
, meanwhile, consumes 48 bytes of memory.
In JavaScript, Zig data types are represented by objects with hidden fields. One of these fields is
Symbol(memory)
, which holds a
DataView.
You can see these objects' internals if you dump them into the console:
pub const Uint32 = u32;
pub const Point = struct { x: f32, y: f32 };
pub const Float32Array = [12]f32;
import { Float32Array, Point, Uint32 } from './data-type-example-1.zig';
console.log(new Uint32(0x01ff));
console.log(new Point({ x: 0, y: 0 }));
console.log(new Float32Array([ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 ]));
u32 {
[Symbol(memory)]: DataView {
byteLength: 4,
byteOffset: 0,
buffer: ArrayBuffer { [Uint8Contents]: <ff 01 00 00>, byteLength: 4 }
}
}
data-type-example-1.Point {
[Symbol(memory)]: DataView {
byteLength: 8,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00>,
byteLength: 8
}
}
}
[12]f32 {
[Symbol(memory)]: DataView {
byteLength: 48,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 80 3f 00 00 00 40 00 00 40 40 00 00 80 40 00 00 a0 40 00 00 c0 40 00 00 e0 40 00 00 00 41 00 00 10 41 00 00 20 41 00 00 30 41 00 00 40 41>,
byteLength: 48
}
}
}
Getters and setters provide access to the underlying bytes:
import { Point } from './data-type-example-1.zig';
const descriptors = Object.getOwnPropertyDescriptors(Point.prototype);
for (const [ name, desc ] of Object.entries(descriptors)) {
if (desc.get) {
console.log({ name, desc });
}
}
{
name: '$',
desc: {
get: [Function: getSelf],
set: [Function: initializer],
enumerable: false,
configurable: true
}
}
{
name: 'dataView',
desc: {
get: [Function: get] { special: true },
set: [Function: set] { special: true },
enumerable: false,
configurable: true
}
}
{
name: 'base64',
desc: {
get: [Function: get] { special: true },
set: [Function: set] { special: true },
enumerable: false,
configurable: true
}
}
{
name: 'x',
desc: {
get: [Function: getValue],
set: [Function: setValue] { required: true },
enumerable: true,
configurable: true
}
}
{
name: 'y',
desc: {
get: [Function: getValue],
set: [Function: setValue] { required: true },
enumerable: true,
configurable: true
}
}
Objects representing composite data types containing other composite types have the additional
hidden property Symbol(slots)
, use to hold the child objects representing the inner type:
pub const Point = struct { x: f32, y: f32 };
pub const Line = struct { p1: Point, p2: Point };
import { Line } from './data-type-example-2.zig';
const line = new Line({
p1: { x: 0, y: 0 },
p2: { x: 1, y: 1 },
});
console.log(line);
console.log(line.p1);
console.log(line.p2);
data-type-example-2.Line {
[Symbol(slots)]: {
'0': data-type-example-2.Point { [Symbol(memory)]: [DataView] },
'1': data-type-example-2.Point { [Symbol(memory)]: [DataView] }
},
[Symbol(memory)]: DataView {
byteLength: 16,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 80 3f 00 00 80 3f>,
byteLength: 16
}
}
}
data-type-example-2.Point {
[Symbol(memory)]: DataView {
byteLength: 8,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 80 3f 00 00 80 3f>,
byteLength: 16
}
}
}
data-type-example-2.Point {
[Symbol(memory)]: DataView {
byteLength: 8,
byteOffset: 8,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 80 3f 00 00 80 3f>,
byteLength: 16
}
}
}
Child objects are created on-demand as needed. In the following example, the slots of the slice object is empty initially:
pub const U8Pixels = []@Vector(4, u8);
import { U8Pixels } from './data-type-example-3.zig';
const rawData = new Uint8Array(800 * 600 * 4);
const pixels = new U8Pixels(rawData);
const pixelSlice = pixels['*'];
console.log(pixelSlice);
pixelSlice[3] = [ 255, 255, 255, 255 ];
console.log(pixelSlice);
[_]@Vector(4, u8) {
[Symbol(memory)]: DataView {
byteLength: 1920000,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 1919900 more bytes>,
byteLength: 1920000
}
},
[Symbol(length)]: 480000,
[Symbol(slots)]: {}
}
[_]@Vector(4, u8) {
[Symbol(memory)]: DataView {
byteLength: 1920000,
byteOffset: 0,
buffer: ArrayBuffer {
[Uint8Contents]: <00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ... 1919900 more bytes>,
byteLength: 1920000
}
},
[Symbol(length)]: 480000,
[Symbol(slots)]: { '3': @Vector(4, u8) { [Symbol(memory)]: [DataView] } }
}
When we change the third pixel, the vector object representing gets auto-vivificated into existence. It's the only one we touched, so it's the only one kept in the slots. The slice object's creation has not led to half a million child objects being created unnecessarily.
Notice how the slice object also has the hidden field Symbol(length)
.