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