UNPKG

arraybuffer-struct

Version:

Map ArrayBuffer to C-like struct with full type support.

410 lines (339 loc) 15.4 kB
> ⚠️ Disclaimer: > The English here might be pretty rough. > This README was created with AI assistance + Google Translate, > so please forgive any awkward phrasing! > Also, if you're a beginner struggling with GitHub/NPM, you're not alone this stuff can be a real headache. 😅 # arraybuffer-struct A JavaScript library for working with memory buffers as struct types. # Usage `i8`: int8 1 byte, range: -128 to 127 `u8`: uint8 1 byte, range: 0 to 255 `u8c`: uint8 clamped 1 byte, range: 0 to 255, clamps values outside the range to 0 or 255 `i16`: int16 2 bytes, range: -32768 to 32767 `u16`: uint16 2 bytes, range: 0 to 65535 `i32`: int32 4 bytes, range: -2147483648 to 2147483647 `u32`: uint32 4 bytes, range: 0 to 4294967295 `i64`: int64 8 bytes, range: -9223372036854775808 to 9223372036854775807 `u64`: uint64 8 bytes, range: 0 to 18446744073709551615 `f16`: float16 2 bytes, range: 6.103515625e-05 to 65504 `f32`: float32 4 bytes, range: 1.175494351e-38 to 3.402823466e+38 `f64`: float64 8 bytes, range: 2.2250738585072014e-308 to 1.7976931348623157e+308 `bool`: 1 byte, true or false `utf8`: variable-length string `struct`: nested object with its own fields and types **Always use little endian reading and writing.** ```javascript import Struct from 'arraybuffer-struct'; const point = new Struct({ x: {value: 10, type: 'i32'}, y: {value: 20, type: 'i32'} }); point.data.x; // 10 point.data.y; // 20 // Not initialized const noInit = new Struct({ x: {value: null, type: 'i32'}, y: {value: undefined, type: 'i32'} }); noInit.data.x; // 0 noInit.data.y; // 0 // all types const allTypes = new Struct({ i8: {value: 1, type: 'i8'}, u8: {value: 2, type: 'u8'}, i16: {value: 3, type: 'i16'}, u16: {value: 4, type: 'u16'}, i32: {value: 5, type: 'i32'}, u32: {value: 6, type: 'u32'}, i64: {value: 7n, type: 'i64'}, u64: {value: 9n, type: 'u64'}, f16: {value: 10, type: 'f16'}, f32: {value: 11, type: 'f32'}, f64: {value: 12, type: 'f64'}, bool: {value: true, type: 'bool'}, utf8: {value: 'A', type: 'utf8'}, // only one character // Array types f64Arr1: {value: [1, 2, 3], type: 'f64[3]'}, // Floar64Array [1, 2, 3] f64Arr2: {value: new Float64Array([4, 5, 6]), type: 'f64[3]'}, // Floar64Array [4, 5, 6] boolArr: {value: [true, false, true], type: 'bool[3]'}, // Array [true, false, true] string: {value: 'abcde', type: 'utf8[100]'}, // string 'abcde' // Multidimensional Array i32Mat2x2_1: {value: [[1, 2], [3, 4]], type: 'i32[2][2]'}, i32Mat2x2_2: {value: new Int32Array([1, 2, 3, 4]), type: 'i32[2][2]'}, i32Mat2x2_3: {value: [1, 2, 3, 4], type: 'i32[2][2]'}, i32Mat4x4: { value: [ [1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, 16] ], type: 'i32[4][4]' }, // struct struct: { value: { x: {value: 1, type: 'i32'}, y: {value: 2, type: 'i32'}, z: {value: 3, type: 'i32'} }, type: 'struct' }, moreStructs: { value: { a: { value: { b: { value: 1, type: 'i32' } }, type: 'struct' } }, type: 'struct' } }); ``` ## 🔧 Type Availability by Platform `f16`: Chrome 135+ or Node.js 24+, See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Float16Array#browser_compatibility `i64` & `u64`: Chrome 67+ or Node.js 10.4+, See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt64Array#browser_compatibility # options ## align & layoutOpt ```javascript const obj = { x: {value: 1, type: 'i8'}, y: {value: 2, type: 'i64'} }; const struct1 = new Struct(obj, {align: true, layoutOpt: false}); struct1.view.buffer.byteLength; // 16 const struct2 = new Struct(obj, {align: false, layoutOpt: false}); struct2.view.buffer.byteLength; // 9 const struct3 = new Struct(obj, {align: true, layoutOpt: true}); struct3.view.buffer.byteLength; // 9 ``` Explanation: When `align: true` and `layoutOpt: false`, the structure follows C-like natural alignment: - `i8` (1 byte) is placed at offset 0 - `i64` (8 bytes) requires 8-byte alignment, so 7 bytes of padding are inserted - Total size becomes: 1 (`i8`) + 7 (padding) + 8 (`i64`) = 16 bytes When `align: false`, no padding is applied regardless of type alignment. - The structure becomes tightly packed: 1 (`i8`) + 8 (`i64`) = 9 bytes - However, this layout is incompatible with C/wasm and cannot safely use TypedArray slicing for certain fields. When `layoutOpt: true`, member reordering is enabled (e.g., large fields come first) to minimize padding. - With both `align: true` and `layoutOpt: true`, the struct is aligned but members are reordered to reduce size. - In this case, `i64` comes first, followed by `i8`, producing a total size of 9 bytes without breaking alignment. Summary: `align: true` ensures ABI compatibility with C/C++/WebAssembly memory layout TypedArray slicing works reliably only when proper alignment is preserved ⚠️ Disabling alignment may reduce size but breaks compatibility and causes potential slicing errors ⚠️ Enabling `layoutOpt` reorders fields, so offsets may not match original field order this is not ideal if field layout must match exactly (e.g., in wasm interop) If you want to use array on non-aligned structures, please set `{useTypedArray: false}`, which will use js array to read and write ## shared ```javascript const shared = new Struct({ counter: {value: 0, type: 'i32'}, lock: {value: 0, type: 'i32'} }, {shared: true}); shared.view.buffer; // SharedArrayBuffer ``` ## utf8FixedSize only initialize the first element of a `string[]` with a fixed size. ```javascript const obj = { strArr: {value: ['abc', 'def'], type: 'utf8[2][100]'} }; // Treat each element of string[] as a separate column const struct1 = new Struct(obj, {utf8FixedSize: true}); struct1.data.strArr; // ["abc", "def"] // Concatenate all elements of string[] into a string and encode them together const struct2 = new Struct(obj, {utf8FixedSize: false}); struct2.data.strArr; // ["abcdef", ""] ``` ## useTypedArray Enable or disable returning native TypedArray views for fixed-size array fields. When useTypedArray is `true`, array fields will be represented as real TypedArray instances, allowing fast bulk operations and compatibility with APIs that expect typed arrays. When `false`, the array is returned as a normal JavaScript array with each element implemented via getter/setter for fine-grained control. ```javascript const obj = { arr: {value: [1, 2, 3, 4, 5], type: 'i32[5]'} }; const struct1 = new Struct(obj, {useTypedArray: true}); struct1.data.arr; // Int32Array [1, 2, 3, 4, 5] const struct2 = new Struct(obj, {useTypedArray: false}); struct2.data.arr; // Array [1, 2, 3, 4, 5] (all elements are getter/setter) ``` ## buffer & byteOffset Manually specify an external `ArrayBuffer` as the backing store and set a custom `byteOffset`. Note: - An error will be thrown if the remaining space from the given `byteOffset` is insufficient to fit the entire Struct. - If you want to use arrays on `byteOffset` that are not multiples of 2, 4, or 8 without possible `RangeError`, set `{useTypedArray: false}`, which will use js arrays for safe reading and writing. ```javascript const buffer = new ArrayBuffer(1024); const struct = new Struct({ x: {value: 1, type: 'i32'}, y: {value: 2, type: 'i32'} }, { buffer: buffer, byteOffset: 128 }); struct.view.buffer === buffer; // true struct.view.byteOffset; // 128 ``` # Serializable by structuredClone / postMessage `Struct` instances are designed to be safely serialized using `structuredClone` or transferred via `postMessage` (e.g. to a Web Worker or Node.js worker thread). When a `Struct` object is cloned or transferred: Its internal layout metadata (field types, offsets, etc.) is preserved. The underlying buffer (`ArrayBuffer` or `SharedArrayBuffer`) is retained and re-linked. You can reconstruct a working `Struct` instance simply by passing the cloned object into new `Struct(...)`. ```javascript // In main thread: const struct = new Struct({...}, {shared: true}); worker.postMessage(struct); // structuredClone happens automatically // In worker thread: parentPort.on("message", data => { const clone = new Struct(data); // Rebuilds the same structure clone.data.someField = 42; // Modifies the same SharedArrayBuffer }); ``` If a `SharedArrayBuffer` is used, both the original and cloned `Struct` instances will operate on the same shared memory, enabling real-time synchronization between threads. This makes inter-thread communication and memory-mapped data models extremely simple and efficient. # SharedArrayBuffer & Worker Please make sure you have enabled `COOP`/`COEP`, otherwise `SharedArrayBuffer` will not be available. main.js: ```javascript import Struct from "arraybuffer-struct"; import { Worker } from "worker_threads"; const shared = new Struct({ counter: {value: [0], type: 'i32[1]'} }, { shared: true }); const worker = new Worker("./worker.js"); worker.postMessage({shared: shared}); worker.on("message", () => { console.log(shared.data.counter[0]); }); ``` worker.js: ```javascript import Struct from "arraybuffer-struct"; import { parentPort } from "worker_threads"; parentPort.on("message", ({shared}) => { const shared = new Struct(shared); for (let i = 0; i < 1000000; i++) { Atomics.add(shared.data.counter, 0, 1); } parentPort.postMessage("done"); }); ``` # WebAssembly Struct ⇄ JavaScript Struct Object This library bridges the gap between low-level memory structures in `WebAssembly` (C/C++) and high-level structured objects in `JavaScript`. It allows developers to define struct layouts in `JavaScript` that exactly match native memory layouts, enabling direct memory mapping via `ArrayBuffer` or `SharedArrayBuffer`. Key use cases include: - Interfacing with `WebAssembly` modules that return raw pointers to structs. - Interpreting memory buffers from native code (e.g., WASM, C/C++) as structured JS objects. - Creating compact, binary-efficient data representations for workers or network transfers. - Supporting precise control over memory alignment and layout optimization. By syncing memory layout between JS and WASM, this tool enables efficient and predictable memory access especially useful when working with shared memory, `TypedArray` slicing, or low-level binary protocols. struct.c: ```c #include <stdint.h> typedef struct { int a; float b; char c[10]; long long d; unsigned char e; } Data; Data data = {1, 2.3f, "hello", 5ll, 255U}; uintptr_t get() { return (uintptr_t)&data; } // emcc -o struct.wasm struct.c --no-entry -s STANDALONE_WASM=1 -s EXPORTED_FUNCTIONS="['_get']" ``` main.js: ```javascript import Struct from "arraybuffer-struct"; import * as fs from "fs"; const wasmBuffer = fs.readFileSync("./test.wasm"); const wasmModule = await WebAssembly.instantiate(wasmBuffer); const {instance: {exports: {get, memory}}} = wasmModule; const struct = new Struct({ // Not initialized values a: {value: null, type: 'i32'}, b: {value: null, type: 'f32'}, c: {value: null, type: 'utf8[10]'}, d: {value: null, type: 'i64'}, e: {value: null, type: 'u8'} }, { align: true, layoutOpt: false, buffer: memory.buffer, byteOffset: get() }); const {data} = struct; console.log([data.a, data.b, data.c, data.d, data.e]); // [1, 2.299999952316284, "hello", 5n, 255] ``` # ✅ Module Format Support This library supports multiple module systems for maximum compatibility: UMD (for direct use in browsers via `<script>` tag or CDN) CommonJS (for Node.js require) ESM (for modern import in both Node.js and browser environments) # ⚠️ TypeScript Type Inference Quirk Due to limitations in TypeScript's inference system particularly around deeply nested generic arguments the compiler might fail to infer the correct types if optional parameters are omitted. This can result in unexpected fallback to any. Consider the following example: ```javascript const a = new Struct({ a: { value: Array.from({ length: 10 }, () => ({ value: { x: { value: 1, type: 'i32' }, y: { value: 2, type: 'i32' } }, type: 'struct' })), type: 'struct' } }); a.data.a; // TypeScript infers: any[] ``` Despite being well-structured, a.data.a becomes any[]. However, simply supplying the optional options parameter even an empty object resolves the issue: Solution 1: Pass {} as the options parameter ```javascript const b = new Struct({ a: { value: Array.from({ length: 10 }, () => ({ value: { x: { value: 1, type: 'i32' }, y: { value: 2, type: 'i32' } }, type: 'struct' })), type: 'struct' } }, {}); b.data.a; // TypeScript infers: { x: number; y: number }[] ``` Solution 2: Use @satisfies with type annotation ```javascript /** * @typedef {import('arraybuffer-struct').StructInputData} StructInputData */ /** @satisfies {StructInputData} */ const obj = { a: { value: Array.from({ length: 10 }, () => ({ value: { x: { value: 1, type: 'i32' }, y: { value: 2, type: 'i32' } }, type: 'struct' })), type: 'struct' } }; const c = new Struct(obj, {}); c.data.a; // TypeScript infers: { x: number; y: number }[] ``` 🧠 Explanation The reason behind this behavior is that TypeScript prioritizes inference from the second generic parameter when both are involved. When the second argument (options) is omitted, it can cause the first argument (T) to lose inference context, defaulting to any. This is a known edge case in TypeScript's inference mechanics. # ⚠ Expected Lifespan Notice: Potential Future Retirement 🧭 This package is designed to provide a flexible workaround for memory structure typing (`struct`) in JavaScript, which currently lacks native support for such features. However, there is an official proposal underway: https://github.com/tc39/proposal-structs If this proposal is successfully adopted and implemented across environments, native `Struct` support will eventually offer a cleaner, faster, and more integrated solution. Therefore: 📌 This project is **not intended as a long-term stable solution**, but rather as a **transitional utility**. 📅 If the proposal is accepted and broadly implemented, this package may be **officially retired** or repurposed as a compatibility layer. 💬 Personally, I'm very excited about this proposal and would love to see it land soon feel free to visit the repo and give it a star to help bring more attention to it! 😄