Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
214: Sharing Memory Between Rust and JavaScript
When you first start plumbing Rust into a web app via WebAssembly, your instinct is usually to treat the boundary like a standard API. You pass a slice of data into a Rust function, Rust processes it, and then you return a Vec<u8> or a String back to JavaScript. It feels clean, it's type-safe, and it just works. But once you start dealing with anything larger than a few kilobytes—say, an image buffer for a real-time filter—you'll notice a massive performance hit that has nothing to do with your actual algorithm.
The overhead of the "Copy-Everything" approach
The problem is that Rust and JavaScript don't actually share the same heap. When you return a Vec<u8> from a wasm-bindgen function, Rust isn't just handing JS a pointer to the memory. Instead, wasm-bindgen allocates new memory on the JavaScript side and copies every single byte from the Wasm linear memory into that new JS array. If you're processing a 4K image, you're copying roughly 33 megabytes of data every single frame. You've essentially turned your high-performance Rust engine into a bottleneck because you're spending all your CPU cycles moving memory around rather than calculating pixels.
// The naive way: High overhead for large data
#[wasm_bindgen]
pub fn apply_grayscale(pixels: &[u8]) -> Vec<u8> {
let mut output = pixels.to_vec();
for chunk in output.chunks_mut(4) {
let gray = (chunk[0] as u32 + chunk[1] as u32 + chunk[2] as u32) / 3;
chunk[0] = gray as u8;
chunk[1] = gray as u8;
chunk[2] = gray as u8;
}
output // This triggers a full copy back to JS
}
Directly viewing the Wasm heap
The better way is to stop thinking about "returning" data and start thinking about "exposing" it. Rust's memory is just one big contiguous array (the WebAssembly.Memory object). If Rust allocates a buffer and keeps it alive, JavaScript can read that memory directly without copying a single byte. We do this by returning a pointer (the memory offset) and the length of the data to JavaScript, then wrapping that slice of the Wasm heap in a JS TypedArray.
I usually do this by creating a static buffer or a long-lived struct that holds the data. By returning the pointer to the start of the buffer, JavaScript can create a Uint8Array that acts as a "window" into the Rust memory. It's incredibly fast because the JS array is just a view; it's not a copy.
// The professional way: Zero-copy memory sharing
#[wasm_bindgen]
pub struct ImageProcessor {
buffer: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
pub fn new(size: usize) -> Self {
Self { buffer: vec![0; size] }
}
pub fn get_ptr(&self) -> *const u8 {
self.buffer.as_ptr()
}
pub fn process(&mut self, input: &[u8]) {
// Perform calculations directly into self.buffer
// ...
}
}
On the JavaScript side, you'd use it like this: const view = new Uint8Array(wasm.memory.buffer, processor.get_ptr(), size);. Now, any change Rust makes to that buffer is instantly visible in JS.
The "Memory Growth" trap
There is a catch here, and it's one that has bitten me more times than I'd like to admit. WebAssembly memory can grow. If Rust decides it needs more space and calls memory.grow(), the underlying ArrayBuffer in JavaScript is detached and invalidated. Your Uint8Array view suddenly becomes a dead object, and trying to access it will throw an error.
If your buffer size is constant—like a fixed-resolution canvas—this isn't an issue. But if you're dynamically resizing your data, you cannot cache the JS view. You must re-create the Uint8Array every time you call a Rust function that might trigger an allocation. It sounds like a chore, but creating a TypedArray view is an incredibly cheap operation compared to copying megabytes of data. Always prioritize the fresh view over the cached one if there's any chance the Rust heap has shifted.
📋 Practical Task
Exercise: Implementing a Zero-Copy RGB-to-Grayscale Buffer
You are provided with a Rust library that currently implements a grayscale filter by returning a Vec<u8>, causing severe lag in a web-based image editor. Your task is to refactor the implementation to use a shared memory pattern.
Requirements:
- Modify the Rust code to implement a
BufferManagerstruct that allocates aVec<u8>once. - Implement a method
ptr()that returns the raw pointer to the buffer and a methodlen()that returns its size. - Implement the
grayscalemethod to modify the internal buffer in-place rather than returning a new vector. - In the provided JavaScript snippet, replace the current
const result = wasm.apply_grayscale(data)logic with aUint8Arrayview that points directly to theBufferManager's memory. - Ensure the JavaScript code handles the "Memory Growth" risk by recreating the view immediately before accessing the processed pixels.
There are no comments for now.