Zig data structures in JavaScript - chung-leong/zigar GitHub Wiki

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).