Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Getting started

wasmgc provides garbage-collected structs and arrays for Rust on WebAssembly. The engine manages the objects’ memory.

Setup

Create a Rust library and add wasmgc:

cargo new --lib counter
cd counter
cargo add wasmgc

Install the Wasm target and the postprocessing tool:

rustup target add wasm32-unknown-unknown
cargo install wasmgc --features cli

Configure Cargo.toml to produce a Wasm module:

[lib]
crate-type = ["cdylib"]

A Rust example

Replace src/lib.rs with:

use wasmgc::{Gc, wasmgc};

#[wasmgc]
struct Counter {
    value: i32,
}

#[unsafe(no_mangle)]
pub extern "C" fn _start() {
    let counter = Gc::new(Counter { value: 1 });
    counter.set_value(2);
    assert_eq!(counter.value(), 2);
}

#[wasmgc] generates the struct’s field accessors. Gc::new creates an object on the engine’s GC heap; value() reads its field and set_value() updates it. _start is the entry point invoked by wasmtime run.

Build and run

Install Wasmtime to run the example. Compile to Wasm, then postprocess the output to replace generated bindings with WasmGC instructions:

cargo build --target wasm32-unknown-unknown --release
wasmgc target/wasm32-unknown-unknown/release/counter.wasm -o counter.gc.wasm
wasmtime run counter.gc.wasm

The program exits successfully when the assertion passes.

Continue with Using Rust for structs and arrays, or Host interoperability for loading a module and exposing APIs to a host.

Using Rust

These examples run inside a Wasm module after postprocessing.

Structs

#[wasmgc] defines a managed struct. Gc::new consumes its initial field values and creates an object on the engine’s GC heap.

use wasmgc::{Gc, GcArray, wasmgc};

#[wasmgc]
struct Counter {
    value: i32,
}

let counter = Gc::new(Counter { value: 1 });
let shared = counter.clone();

shared.set_value(2);
assert_eq!(counter.value(), 2);
assert!(Gc::ref_eq(&counter, &shared));

The macro generates value() and set_value() with the field’s Rust visibility. Cloning creates another root to the same object; both roots observe the same changes. There is no Rust &Counter into the managed object’s storage.

Reference fields

Use Gc<T> for a reference field, or Option<Gc<T>> when it may be null. Reference setters borrow their arguments, and getters return owned roots.

#[wasmgc]
struct Holder {
    counter: Option<Gc<Counter>>,
}

let counter = Gc::new(Counter { value: 3 });
let holder = Gc::new(Holder { counter: None });
holder.set_counter(Some(&counter));

drop(counter);
let retained = holder.counter().unwrap();
assert_eq!(retained.value(), 3);

holder.set_counter(None);
assert_eq!(retained.value(), 3);

The holder keeps its field reachable after the original root is dropped. The getter’s returned root then keeps the counter alive after the field is cleared. The same reference types can form recursive and mutually referencing structs.

Arrays

GcArray<T> is an alias for Gc<Array<T>>. Its length is fixed at construction.

let numbers = GcArray::from_slice([1_i32, 2, 3]);
numbers.set(1, 9);
assert_eq!(numbers.get(1), Some(9));
assert_eq!(numbers.get(3), None);

let zeros = GcArray::<i32>::new_default(4);
assert_eq!(zeros.len(), 4);
assert_eq!(zeros.get(0), Some(0));

Arrays can also hold references. Copying references preserves object identity; it does not copy the objects themselves.

let counter = Gc::new(Counter { value: 5 });
let counters = GcArray::from_slice([counter.clone()]);
counters.set(0, &counter);

drop(counter);
assert_eq!(counters.get(0).unwrap().value(), 5);

from_slice copies elements from a Rust slice, array, or vector into GC storage. It does not transfer the Rust allocation into the GC heap. Array setters panic on an out-of-bounds index.

Reachability

A Gc owns a root. Dropping it releases that root; the object remains alive while another root, host reference, or reachable GC object retains it. The engine can collect unreachable cycles.

Managed objects do not run Rust destructors. Their Rust struct supplies construction values and must not implement Drop. Ordinary Rust allocations, such as Vec and String, remain in linear memory and cannot be stored as GC fields directly.

Host interoperability

After postprocessing, host functions exchange actual Wasm references. JavaScript receives GC objects and host values directly, without numeric handles.

Loading a module

In a Rust library project, put the following examples in src/lib.rs and configure Cargo.toml:

[lib]
crate-type = ["cdylib"]

Build the library for wasm32-unknown-unknown and postprocess it:

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
wasmgc target/wasm32-unknown-unknown/release/your_crate.wasm -o app.wasm

Replace your_crate.wasm with the library’s output filename. Serve app.wasm alongside your page and load it with:

const bytes = await (await fetch("./app.wasm")).arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
const wasm = instance.exports;

Struct exports

Choose which operations to expose to the host:

use wasmgc::{Gc, wasmgc};

#[wasmgc(constructor)]
pub struct Counter {
    #[getter]
    #[setter]
    value: i32,
}

JavaScript can construct and update a counter:

const counter = wasm["Counter.new"](1);
wasm["Counter.set_value"](counter, 2);
console.log(wasm["Counter.get_value"](counter)); // 2

constructor exports Counter.new, with parameters in field order. Field #[getter] and #[setter] attributes independently export the corresponding operations. These options only control host exports: Rust always gets its field methods, even when the attributes are omitted.

Function exports

Apply #[wasmgc] to a free function to export it under its Rust name:

#[wasmgc]
pub fn counter(value: i32) -> Gc<Counter> {
    Gc::new(Counter { value })
}

#[wasmgc]
pub fn read(counter: &Gc<Counter>) -> i32 {
    counter.value()
}
const value = wasm.counter(3);
console.log(wasm.read(value)); // 3

The functions remain callable from Rust. Parameters may be owned values or shared borrows; return values must be owned or (). Supported values include scalars, Gc<T>, GcArray<T>, Option<Gc<T>>, and ExternRef. Functions must be synchronous and non-generic. Export names do not include Rust module paths.

Host imports and ExternRef

ExternRef keeps a host value alive. Declare host operations in a #[wasmgc] extern "wasmgc" block; imports use the env module and their Rust function names.

use wasmgc::ExternRef;

#[wasmgc]
extern "wasmgc" {
    /// # Safety
    ///
    /// The host value must have an add(i32) method returning an i32 normally.
    fn counter_add(counter: &ExternRef, delta: i32) -> i32;
}

/// # Safety
///
/// `counter` must satisfy the `counter_add` host contract.
#[wasmgc]
pub unsafe fn increment(counter: ExternRef) -> i32 {
    // SAFETY: The host supplies a counter satisfying counter_add's contract.
    unsafe { counter_add(&counter, 1) }
}

Supply the import when instantiating the processed module:

const { instance } = await WebAssembly.instantiate(bytes, {
    env: { counter_add: (counter, delta) => counter.add(delta) },
});
const counter = { value: 0, add(delta) { return this.value += delta; } };
console.log(instance.exports.increment(counter)); // 1

Imports are unsafe to call by default. Host implementations must satisfy their declared contract and return normally; unwinding through the generated bridges is unsupported. An import may use safe fn when its host implementation is safe for every argument allowed by its Rust types.

ExternRef can also be stored in GC fields and arrays. Cloning or storing a reference retains the same host value; it does not copy the host object.

Examples

Persistent tree

Open the browser example.

Edit a tree and compare the previous and current versions. Rust copies only the path to the edited node; the rest of the tree is shared between versions. Matching object labels and highlighted nodes show that sharing.

Only these two versions are retained. Each edit releases the oldest version; the page uses weak references to show when nodes no longer referenced by either version are actually collected by the browser.

See the source and local run commands.

Shared palette

Open the browser example.

Three GC color objects are shared by 6,144 cells. Editing a color updates every cell that references the same object. The browser and terminal versions use the same Rust implementation.

Run the terminal version from a repository checkout:

node examples/palette/run.mjs

Breakout

A terminal game with a ball, paddle, and bricks stored in GC objects and arrays. Move the paddle with the arrow keys or A/D.

node examples/breakout/run.mjs

See the source. Both terminal commands build and postprocess the Wasm module automatically.