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
216: Building a Wasm Plugin System
One of the most powerful patterns I've used in production systems is the Wasm plugin architecture. It gives you the holy grail of extensibility: users can write plugins in any language that targets WebAssembly, but they run in a sandboxed environment where they can't accidentally (or intentionally) wipe your server's hard drive. For this lesson, we're going to build a simple text-processing engine. The host will pass a string to a plugin, and the plugin will "transform" it—in our case, we'll build a plugin that censors specific words.
Defining the Guest Plugin
I always start with the guest—the code that will actually be compiled to wasm32-unknown-unknown. Because Wasm doesn't have a built-in way to pass complex types like String or Vec across the boundary, we have to stick to primitive types. We'll use pointers and lengths, essentially treating Wasm memory as a big byte array.
// guest/src/lib.rs
#[no_mangle]
pub extern "C" fn censor_text(ptr: *mut u8, len: usize) {
let slice = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
let text = std::str::from_utf8_mut(slice).unwrap();
// Simple logic: replace "secret" with "XXXXXX"
// Note: In a real app, we'd handle length changes carefully.
// For this example, we'll assume in-place replacement of equal length.
if let Some(pos) = text.find("secret") {
let bytes = unsafe { text.get_unchecked_mut(pos..pos+6) };
bytes.copy_from_slice(b"XXXXXX");
}
}
Setting Up the Host Runtime
On the host side, I'm using wasmtime. It's the industry standard for a reason. The host's job is to load the .wasm file, instantiate it, and provide the memory that the guest will operate on. I like to keep the host logic lean; it should just be a coordinator that hands data to the plugin and gets it back.
// host/src/main.rs
use wasmtime::*;
fn main() -> Result<()> {
let engine = Engine::default();
let module = Module::from_file(&engine, "plugin.wasm")?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let censor_fn = instance.get_typed_func::<(&mut [u8], usize), ()>(&mut store, "censor_text")?;
let mut data = "This is a secret message".as_bytes().to_vec();
let memory = instance.get_memory(&mut store, "memory")
.ok_or(Error::msg("No memory found"))?;
// This is where I usually trip up...
censor_fn.call(&mut store, (&mut data, data.len()))?;
Ok(())
}
The Memory Wall (And How I Hit It)
When I first ran the code above, it crashed immediately. I realized I made a classic Wasm mistake: I tried to pass a Rust slice &mut [u8] from the host directly into the guest function. But &mut [u8] is a host-side pointer. The Wasm guest has its own isolated linear memory; it has no idea what a host-side pointer is. It can only see addresses within its own wasmtime::Memory.
To fix this, I have to actually write the data into the guest's memory first, then pass the offset of that memory to the function. It's a bit more boilerplate, but it's the only way to ensure the guest can actually touch the bytes.
Bridging the Gap with Memory Offsets
Here is the corrected host logic. Instead of passing the slice, I allocate space in the guest's memory, copy my string there, and pass the starting index.
// Corrected host logic
let mut data = "This is a secret message".as_bytes().to_vec();
let memory = instance.get_memory(&mut store, "memory")
.ok_or(Error::msg("No memory found"))?;
// 1. Find where the guest wants the data (or allocate space)
// For simplicity, we'll write to the start of the Wasm memory
memory.write(&mut store, 0, &data)?;
// 2. Pass the offset (0) and the length
let censor_fn = instance.get_typed_func::<(u32, usize), ()>(&mut store, "censor_text")?;
censor_fn.call(&mut store, (0, data.len()))?;
// 3. Read the modified data back out of Wasm memory
let mut result = vec![0u8; data.len()];
memory.read(&store, 0, &mut result)?;
println!("Result: {}", String::from_utf8_lossy(&result));
I also had to update the guest function signature to fn censor_text(ptr: u32, len: usize) and cast that u32 back to a pointer inside the guest. It's less "Rusty," but when you're dealing with FFI and Wasm, you're essentially writing C-style interfaces. Once that's done, the host writes "secret," the guest changes it to "XXXXXX," and the host reads it back. Sandboxed, fast, and decoupled.
📋 Practical Task
Implement a "Uppercase" Wasm Plugin
Your task is to expand the existing system. Create a new guest plugin that implements a function called transform_to_uppercase. This plugin should take a pointer and a length, and modify the string in-place to be entirely uppercase.
- Modify the guest code to iterate through the byte slice and convert each character to uppercase.
- Update the host to load this new plugin and verify that a string like "hello rust" becomes "HELLO RUST".
- Ensure you handle the memory transfer correctly by writing to the guest memory and reading the result back, avoiding the pointer mistake discussed in the lesson.
There are no comments for now.