Skip to content

rejth/wasm-transfer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wasm-transfer

wasm-transfer is a Rust and WebAssembly study project about moving complex structured data from Rust into JavaScript efficiently.

Motivation

Passing small values across the WASM boundary is straightforward. Passing many complex records is different: each boundary crossing, allocation, string conversion, object construction, and copy starts to matter.

This repository explores that boundary with a realistic nested structure:

  • Order is exported to JavaScript with wasm-bindgen.
  • Order owns a private Vec<Product>.
  • Product contains a String, f32, u32, and bool.
  • JavaScript can read products either as native JS objects or through binary views.

The goal is to understand when a simple JS object API is good enough and when a binary representation is worth the extra wrapper code.

Prerequisites

Install Rust

Install Rust using rustup (the official installer):

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

After installation, restart your terminal or run:

source $HOME/.cargo/env

Verify installation:

rustc --version
cargo --version

Install wasm-pack

This project uses wasm-pack to compile the Rust crate to WebAssembly and generate the JavaScript bindings in pkg/.

Install with the official installer:

curl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh

Or with Cargo:

cargo install wasm-pack

Verify installation:

wasm-pack --version

Project Layout

wasm-transfer/
├── src/                  # Rust WASM crate
│   ├── product.rs
│   ├── order.rs
│   └── serialize/        # one module per transfer strategy
├── js/
│   ├── types/            # shared TypeScript types
│   ├── views/            # binary product decoders
│   ├── scripts/          # demo and benchmark
│   └── tests/            # Node integration tests
├── tests/web.rs          # wasm32 browser tests
└── pkg/                  # wasm-pack output (generated)

Transfer Methods

Each strategy crosses the WASM boundary differently. The diagram below shows the path from Rust to what JavaScript receives.

flowchart TB
  subgraph rust ["Rust (Order)"]
    O["Vec<Product>"]
  end

  subgraph js_objects ["JavaScript objects"]
    J1["Array of plain objects"]
  end

  subgraph js_binary ["JavaScript binary"]
    J2["Uint8Array + custom view"]
    J3["[ptr, len] + raw view"]
    J4["Uint8Array + msgpackr decode"]
    J5["Uint8Array (Rust archive)"]
  end

  O -->|"gloo-utils serde"| J1
  O -->|"serde_wasm_bindgen"| J1
  O -->|"encoded raw bytes"| J2
  O -->|"raw struct memory"| J3
  O -->|"MessagePack"| J4
  O -->|"rkyv"| J5
Loading

At a glance

Ratings are relative within this project (not universal benchmarks). Simple = little JS glue code. Fast = large vectors. Portable = meaningful to JavaScript without Rust layout knowledge.

Method Output Simple Fast Portable
gloo-utils serde JS objects ●●●●● ●●○○○ ●●●●●
serde_wasm_bindgen JS objects ●●●●○ ●●●○○ ●●●●●
Encoded raw bytes Uint8Array + view ●●○○○ ●●●●○ ●●○○○
Raw struct memory [ptr, len] + view ●○○○○ ●●●●● ●○○○○
MessagePack Uint8Array + decode ●●●○○ ●●●●○ ●●●●○
rkyv Uint8Array ●○○○○ ●●●●● ○○○○○

JavaScript object APIs

Best when dataset size is small or developer ergonomics matter more than throughput.

gloo-utils serdeserialize_items_with_serdegetItemsJson()

  • Returns a normal JS array of { sku, price, quantity, in_stock }.
  • Tradeoff: serializes through JSON internally; simple API, expensive for large data.

serde_wasm_bindgenserialize_items_with_serde_wasm_bindgengetItemsJs()

  • Returns native JS values without a JSON string step.
  • Tradeoff: still builds many JS objects field-by-field for large vectors.

Binary handoff APIs

Best when moving thousands of records and you want fewer allocations at the boundary.

Encoded raw bytesserialize_items_with_bytesgetItemsBinary() + ProductsView

  • Compact Uint8Array with a documented layout; strings still point into WASM memory.
  • Tradeoff: custom layout and lifetime rules on the JS side.

Raw struct memoryserialize_items_with_raw_bytesgetItemsBinaryRaw() + ProductsViewRaw

  • Hands off [pointer, length] directly into the Rust Vec backing store.
  • Tradeoff: fastest path, but unsafe if Rust memory layout or lifetimes change.

MessagePackserialize_items_with_rmp_serdegetItemsBinaryMessagePack() + msgpackr

  • Portable binary blob; decode on the JS side with msgpackr.
  • Tradeoff: extra decode step, but good balance of speed and interoperability.

rkyvserialize_items_with_rkyvUint8Array

  • Zero-copy archive bytes optimized for Rust consumption.
  • Tradeoff: not a practical JS interchange format; bytes are not meaningful to JavaScript alone.

Usage

Build the Node-targeted WASM package:

npm run build

Run the demo:

npm run demo

Run the benchmark:

npm run demo:benchmark

Minimal TypeScript example:

import { unpack } from "msgpackr";
import { Order } from "./pkg/wasm_transfer.js";
import { ProductsView } from "./js/views/products.js";

const order = new Order(1, "Ada");
order.addProduct("SKU001", 29.99, 2, true);
order.addProduct("SKU002", 10.0, 1, false);

const jsItems = order.getItemsJs();
const msgpackItems = unpack(order.getItemsBinaryMessagePack());
const binaryItems = new ProductsView(order.getItemsBinary());

console.log(jsItems[0].sku);
console.log(msgpackItems[0][0]);
console.log(binaryItems.get(0).sku);

order.free();

Conclusion

For small and medium datasets, returning native JavaScript objects through serde_wasm_bindgen or gloo-utils is the easiest and most maintainable approach.

For large vectors, binary transfer is the important optimization. A custom raw-byte view gives maximum control and can be very fast, but it couples JavaScript to Rust memory layout and lifetime rules. MessagePack is the best practical compromise in this project: it keeps the WASM-to-JS transfer compact and fast while preserving a portable, JavaScript-decodable format.

rkyv is useful to demonstrate zero-copy archive ideas on the Rust side, but it is not a good Rust-to-JavaScript interchange format here because the bytes are not directly meaningful to JavaScript.

About

Exploring data transfer strategies between Rust and JavaScript in WebAssembly

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors