Skip to content

Native Binary Data Support for Host Functions #38

@simongdavies

Description

@simongdavies

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 Uint8ArrayVec<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:

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions