-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Problem
Host function calls between guest (QuickJS) and host (Node.js/Rust) serialise all data through JSON strings at every layer of the stack:
- Guest: ctx.json_stringify(args) converts JS values to a JSON string
- VM boundary: FlatBuffers ParameterValue/ReturnValue carry String through shared memory buffers
- NAPI bridge: ThreadsafeFunction payload is (u64, String) — JSON text
- Host JS: JSON.parse(argsJson) to recover values, JSON.stringify(result) to return
This means Uint8Array, ArrayBuffer, and Node.js Buffer values cannot be passed through host functions. JSON.stringify on a Uint8Array produces {"0":1,"1":2,...} — an object with numeric keys, not binary data.
Impact
Any host function that needs to exchange binary data with guest code (filesystem I/O, compression, cryptographic operations, image processing, etc.) is forced to use base64 encoding as a workaround. This has measurable costs:
- +33% payload size — base64 encodes 3 bytes as 4 characters. A 750KB binary payload becomes 1MB of JSON string, which then has to fit through the shared memory buffer.
- Double encoding overhead — the binary data is base64-encoded, then JSON-stringified (adding quotes and escaping), then UTF-8 encoded into the FlatBuffers string, then written to the shared memory buffer. On the return path, the entire chain runs in reverse.
- Effective buffer utilisation drops to ~75% — a 1MB input/output buffer can only carry ~750KB of actual binary data after base64 + JSON framing overhead.
- CPU cost — base64 encode/decode + JSON parse/stringify for every binary payload, both guest-side and host-side.
Current Workaround
The existing as_bytes() utility in hyperlight-js-runtime/src/utils.rs already handles Uint8Array → Vec<u8> conversion for built-in modules (crypto, io), but this path is not available to user-registered host functions — they go through the JSON serialisation chain exclusively.
There is also a TODO comment at the top of that file:
// TODO: implement ArrayBuffer, DataView and other TypedArray's
Proposed Solution
Add a binary data path alongside the existing JSON path for host function arguments and return values. This would need changes at each layer:
- Guest Runtime (hyperlight-js-runtime)
- Add a CallHostJsFunctionBinary host function variant that accepts/returns Vec via VecBytes — or add a Vec parameter to the existing signature
- Detect Uint8Array/ArrayBuffer args/returns and route through the binary path instead of JSON
- NAPI Bridge (js-host-api):
- Handle Buffer/Uint8Array in the TSFN callback alongside the JSON path
- Pass Buffer directly to user callbacks, accept Buffer returns
Environment
hyperlight-js version: (from current main)
Use case: Host functions that do filesystem I/O, ZIP/archive building, image processing, and other binary-heavy operations through host functions.