← Back to Lessons Lesson 21 of 28
Intermediate data-structures
Wasm Memory Model
What is Linear Memory?
WebAssembly has a single, contiguous block of memory called linear memory. Think of it as a giant Vec<u8> that both Rust and JavaScript can read and write to.
Address: 0x0000 0x0100 0x0200 ... 0xFFFF
┌─────────┬─────────┬─────────┬──────┐
Memory: │ stack │ heap │ data │ ... │
│ data │ allocs │ section │ │
└─────────┴─────────┴─────────┴──────┘
└────────── 1 page = 64KB ───────────┘Key properties:
- Byte-addressable — every byte has an index (0, 1, 2, ...)
- Pages — memory grows in 64KB pages (1 page = 65,536 bytes)
- Shared — both Rust and JS can access the same bytes
- Bounds-checked — accessing out-of-bounds memory traps (doesn't crash the browser)
How Rust Uses Linear Memory
When you compile Rust to Wasm, the compiler lays out memory like this:
┌──────────────┬──────────────┬──────────────┬──────────────┐
│ Static data │ Stack │ Heap │ Free space │
│ (strings, │ (local vars, │ (Vec, String,│ (grows →) │
│ constants) │ call frames)│ Box allocs) │ │
└──────────────┴──────────────┴──────────────┴──────────────┘
0x0000 0xFFFF...Accessing Memory from JavaScript
JavaScript sees Wasm memory as an ArrayBuffer:
// After init(), wasm.memory is available
const memory = wasm.memory;
// Create typed views into the buffer
const bytes = new Uint8Array(memory.buffer);
const u32s = new Uint32Array(memory.buffer);
const f64s = new Float64Array(memory.buffer);
// Read a byte at address 1024
const value = bytes[1024];
// Write a float at address 2048 (byte offset)
f64s[2048 / 8] = 3.14; // Float64Array index = byte offset / 8Passing Data Between Rust and JS
Approach 1: Copy via wasm-bindgen (simple, safe)
#[wasm_bindgen]
pub fn process(data: &[u8]) -> Vec<u8> {
// data is copied from JS to Wasm memory
let mut result = data.to_vec();
// ... modify result ...
result // copied from Wasm back to JS
}Approach 2: Pointer + length (zero-copy, advanced)
#[wasm_bindgen]
pub struct Buffer {
data: Vec<u8>,
}
#[wasm_bindgen]
impl Buffer {
#[wasm_bindgen(constructor)]
pub fn new(size: usize) -> Self {
Self { data: vec![0; size] }
}
pub fn ptr(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn len(&self) -> usize {
self.data.len()
}
}const buf = new Buffer(1024);
const ptr = buf.ptr();
const len = buf.len();
// Create a view directly into Wasm memory — zero copy!
const view = new Uint8Array(wasm.memory.buffer, ptr, len);
// Modify in place — Rust sees the changes
view[0] = 42;Memory Growth
Wasm memory starts small and grows on demand:
// Rust triggers growth automatically when Vec/String needs more space
let mut big_vec = Vec::with_capacity(1_000_000); // may grow memory// JS can check and grow manually
console.log(wasm.memory.buffer.byteLength); // current size
wasm.memory.grow(10); // grow by 10 pages (640KB)
// IMPORTANT: After grow(), all ArrayBuffer views are invalidated!
// Re-create views after growing:
const freshView = new Uint8Array(wasm.memory.buffer);Common Pitfalls
| Pitfall | Why | Fix |
|---|---|---|
| Stale ArrayBuffer views after memory growth | memory.grow() detaches old buffer |
Re-create views after any operation that might grow |
| Alignment errors | Float64Array needs 8-byte aligned offset | Use ptr as usize % 8 == 0 check |
| Memory leaks | Rust allocations not freed | Drop structs properly, avoid forget() overuse |
| Reading freed memory | Use-after-free via stale JS reference | Copy data out before freeing Rust structs |
Memory Size Budget
| Content | Typical size |
|---|---|
| Small Wasm module | 50-200KB |
| Wasm + web-sys bindings | 200KB-1MB |
| Default linear memory | 1 page (64KB) |
| Large application | 10-50MB |
| Browser limit | ~2-4GB |
Try It
Click Run to see linear memory concepts in action — creating memory, writing/reading at offsets, growing pages, and storing typed values.
Try It
Chapter Quiz
Pass all questions to complete this lesson