Skip to content

Instantly share code, notes, and snippets.

@eliemichel
Last active November 14, 2024 01:34
Show Gist options
  • Save eliemichel/56d8013093b881c39e9b5de72f63e7e5 to your computer and use it in GitHub Desktop.
Save eliemichel/56d8013093b881c39e9b5de72f63e7e5 to your computer and use it in GitHub Desktop.
Javascript view builder to interpret raw ArrayBuffer data as structured data
// Javascript view builder to interpret raw ArrayBuffer data as structured data.
// Copyright 2024 (c) Élie Michel - MIT Licensed
// NB: This is loosly inspired by https://github.com/kainino0x/structured-accessor
const kTypedArrayConstructors = {
i8: Int8Array,
u8: Uint8Array,
i16: Int16Array,
u16: Uint16Array,
i32: Int32Array,
u32: Uint32Array,
f32: Float32Array,
f64: Float64Array,
i64: BigInt64Array,
u64: BigUint64Array,
};
class StructuredAccessorFactory {
// The 'layout' argument is quite low-level in this example, it is expected to be an object that
// gives for each key the type and byte offset. Nested structures are not examplified here but
// are in theory compatible with our approach.
constructor(layout) {
// We build the source code of the this.create() function, so that we can "prepare" this
// function once for all and thus makes the creation of a view as fast as possible.
// The preamble declares only the typed views that we need over the provided array buffer.
const preambleSrc = [];
const readyTypedArrays = new Set();
const ensureTypedArray = (type) => {
if (!readyTypedArrays.has(type)) {
preambleSrc.push(`const data_as_${type} = new ${kTypedArrayConstructors[type].name}(buffer);`);
readyTypedArrays.add(type);
}
}
// The "kernel" is the core source of the pre-interpreted function.
const kernelSrc = [];
kernelSrc.push("let accessor = {};");
for (const [ key, entry ] of Object.entries(layout)) {
const { byteOffset, type } = entry;
ensureTypedArray(type);
const arrayOffset = byteOffset / kTypedArrayConstructors[type].BYTES_PER_ELEMENT;
kernelSrc.push(
`Object.defineProperty(accessor, "${key}", {`,
` enumerable: true,`,
` get() { return data_as_${type}[${arrayOffset}]; },`,
` set(value) { data_as_${type}[${arrayOffset}] = value; },`,
`});`,
);
}
kernelSrc.push("return accessor;")
this.create = Function("buffer", [ ...preambleSrc, ...kernelSrc ].join("\n"));
}
}
// Tests
const ab = new ArrayBuffer(32);
const _0 = new StructuredAccessorFactory({
value: { type: 'i32', byteOffset: 0 },
}).create(ab);
const _1 = new StructuredAccessorFactory([
{ name: '0', type: 'i32', byteOffset: 0 },
{ name: '1', type: 'i32', byteOffset: 4 },
]).create(ab);
const _2 = new StructuredAccessorFactory({
x: { type: 'i8', byteOffset: 0 },
y: { type: 'i32', byteOffset: 4 },
z: { type: 'i8', byteOffset: 8 },
}).create(ab);
console.log('_0 == ' + JSON.stringify(_0));
console.log('_0.value == ' + JSON.stringify(_0.value));
console.log(' setting _0.value');
_0.value = 99;
console.log(' _0.value == ' + JSON.stringify(_0.value));
console.log('arraybuffer = ' + new Int32Array(ab).toString());
console.log('_1[0] == ' + JSON.stringify(_1[0]));
_1[1] = 12;
console.log('_1[1] == ' + JSON.stringify(_1[1]));
console.log('_2.x == ' + JSON.stringify(_2.x));
console.log('_2.y == ' + JSON.stringify(_2.y));
console.log('_2.z == ' + JSON.stringify(_2.z));
console.log('arraybuffer = ' + new Int32Array(ab).toString());
// nodejs perf test
const { performance } = require('perf_hooks');
const factory = new StructuredAccessorFactory({
x: { type: 'i32', byteOffset: 0 },
y: { type: 'i32', byteOffset: 4 },
z: { type: 'i32', byteOffset: 8 },
});
const buffer = new ArrayBuffer(32);
const begin = performance.now();
for (let i = 0 ; i < 100000 ; ++i) {
const accessor = factory.create(buffer);
accessor.x = 42;
accessor.y = -314;
accessor.z = 3615;
//console.assert(accessor.x == 42);
//console.assert(accessor.y == -314);
//console.assert(accessor.z == 3615);
}
const end = performance.now();
console.log(`elapsed: ${end-begin} ms`)
// elapsed: (220 ms w/o assertions)
// Same behavior as the previous script, only this one does not use Function, which leads to lower speed
const kTypedArrayConstructors = {
i8: Int8Array,
u8: Uint8Array,
i16: Int16Array,
u16: Uint16Array,
i32: Int32Array,
u32: Uint32Array,
f32: Float32Array,
f64: Float64Array,
i64: BigInt64Array,
u64: BigUint64Array,
};
class StructuredAccessorFactory {
// The 'layout' argument is quite low-level in this example, it is expected to be an object that
// gives for each key the type and byte offset. Nested structures are not examplified here but
// are in theory compatible with our approach.
constructor(layout) {
this.layout = layout;
}
create(buffer) {
let accessor = {};
for (const [ key, entry ] of Object.entries(this.layout)) {
const { byteOffset, type } = entry;
const typed_data = new kTypedArrayConstructors[type](buffer);
const arrayOffset = byteOffset / kTypedArrayConstructors[type].BYTES_PER_ELEMENT;
Object.defineProperty(accessor, key, {
enumerable: true,
get() { return typed_data[arrayOffset]; },
set(value) { typed_data[arrayOffset] = value; },
});
}
return accessor;
}
}
// Tests
const ab = new ArrayBuffer(32);
const _0 = new StructuredAccessorFactory({
value: { type: 'i32', byteOffset: 0 },
}).create(ab);
const _1 = new StructuredAccessorFactory([
{ name: '0', type: 'i32', byteOffset: 0 },
{ name: '1', type: 'i32', byteOffset: 4 },
]).create(ab);
const _2 = new StructuredAccessorFactory({
x: { type: 'i8', byteOffset: 0 },
y: { type: 'i32', byteOffset: 4 },
z: { type: 'i8', byteOffset: 8 },
}).create(ab);
console.log('_0 == ' + JSON.stringify(_0));
console.log('_0.value == ' + JSON.stringify(_0.value));
console.log(' setting _0.value');
_0.value = 99;
console.log(' _0.value == ' + JSON.stringify(_0.value));
console.log('arraybuffer = ' + new Int32Array(ab).toString());
console.log('_1[0] == ' + JSON.stringify(_1[0]));
_1[1] = 12;
console.log('_1[1] == ' + JSON.stringify(_1[1]));
console.log('_2.x == ' + JSON.stringify(_2.x));
console.log('_2.y == ' + JSON.stringify(_2.y));
console.log('_2.z == ' + JSON.stringify(_2.z));
console.log('arraybuffer = ' + new Int32Array(ab).toString());
// nodejs perf test
const { performance } = require('perf_hooks');
const factory = new StructuredAccessorFactory({
x: { type: 'i32', byteOffset: 0 },
y: { type: 'i32', byteOffset: 4 },
z: { type: 'i32', byteOffset: 8 },
});
const buffer = new ArrayBuffer(32);
const begin = performance.now();
for (let i = 0 ; i < 100000 ; ++i) {
const accessor = factory.create(buffer);
accessor.x = 42;
accessor.y = -314;
accessor.z = 3615;
//console.assert(accessor.x == 42);
//console.assert(accessor.y == -314);
//console.assert(accessor.z == 3615);
}
const end = performance.now();
console.log(`elapsed: ${end-begin} ms`)
// elapsed: (340 ms w/o assertions)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment