Introduction: Why Rust and WebAssembly at an Advanced Level
\nWebAssembly (Wasm) and Rust are a combination that allows you to move computationally heavy tasks from JavaScript into high-performance native code running in the browser. At a basic level, you may have already encountered compiling simple functions. However, to create real products — game engines, image processors, parsers, or cryptographic libraries — a deep understanding of memory management, passing complex data structures, and interacting with the external environment is required.
\nIn this article, we will explore advanced techniques for working with WebAssembly in Rust: from manual memory management to creating hybrid applications where Rust and JavaScript work as a single unit. The material is aimed at developers already familiar with the basics of Rust and Wasm.
\n\n1. Memory Management: Linear Memory and Allocators
\nUnlike JavaScript, WebAssembly uses linear memory — a flat array of bytes. Rust code compiled to Wasm works with this memory directly. For efficient work, it is necessary to understand how to allocate and free memory manually.
\n\n1.1. Manual Memory Allocation with wasm_bindgen
\nThe wasm-bindgen library provides convenient abstractions, but at an advanced level, direct memory access is often required. Consider the following example:
use wasm_bindgen::prelude::*;\nuse std::mem;\nuse std::slice;\n\n#[wasm_bindgen]\npub fn allocate_buffer(size: usize) -> *mut u8 {\n let mut buf = Vec::with_capacity(size);\n let ptr = buf.as_mut_ptr();\n mem::forget(buf); // prevent vector from being dropped\n ptr\n}\n\n#[wasm_bindgen]\npub fn deallocate_buffer(ptr: *mut u8, size: usize) {\n unsafe {\n let _ = Vec::from_raw_parts(ptr, 0, size);\n }\n}\n\n#[wasm_bindgen]\npub fn fill_buffer(ptr: *mut u8, value: u8, len: usize) {\n let slice = unsafe { slice::from_raw_parts_mut(ptr, len) };\n for byte in slice.iter_mut() {\n *byte = value;\n }\n}\nHere we allocate memory via Vec, "forget" about it (so Rust does not drop the buffer), and pass the pointer to JavaScript. The deallocate_buffer function restores the vector and correctly frees the memory.
1.2. Using a Custom Allocator
\nFor complex projects, it is worth replacing the standard allocator with a more predictable one. For example, wee_alloc is a small allocator optimized for Wasm:
// in Cargo.toml\n// [dependencies]\n// wee_alloc = "0.4"\n\n#[global_allocator]\nstatic ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;\nThis will reduce the size of the final .wasm file and speed up memory allocation in the browser.
\n\n2. Passing Complex Data Structures Between Rust and JavaScript
\nAt an advanced level, you are not limited to numbers and strings. You need to pass arrays, structures, and even closures.
\n\n2.1. Structures with #[wasm_bindgen]
\nYou can export entire Rust structures as JavaScript classes:
\n#[wasm_bindgen]\npub struct Matrix {\n rows: usize,\n cols: usize,\n data: Vec<f64>,\n}\n\n#[wasm_bindgen]\nimpl Matrix {\n #[wasm_bindgen(constructor)]\n pub fn new(rows: usize, cols: usize) -> Matrix {\n Matrix {\n rows,\n cols,\n data: vec![0.0; rows * cols],\n }\n }\n\n pub fn get(&self, row: usize, col: usize) -> f64 {\n self.data[row * self.cols + col]\n }\n\n pub fn set(&mut self, row: usize, col: usize, val: f64) {\n self.data[row * self.cols + col] = val;\n }\n\n pub fn multiply(&self, other: &Matrix) -> Result<Matrix, String> {\n if self.cols != other.rows {\n return Err("Incompatible dimensions".to_string());\n }\n // ... matrix multiplication implementation\n let result = Matrix::new(self.rows, other.cols);\n Ok(result)\n