Skip to Content
Course content

212: wasm-bindgen for JS Interop

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you know that WebAssembly is great for performance, but it's essentially a sandbox. It doesn't "know" about the DOM, the window, or your JavaScript variables. That's where wasm-bindgen comes in. It's the glue that lets you pass strings, objects, and functions back and forth between the two worlds.

How do I actually make a Rust function callable from JavaScript?

The short answer is the #[wasm_bindgen] attribute. When you tag a public function with this, the tool generates a JavaScript wrapper that handles all the messy memory management for you. Instead of dealing with raw pointers and linear memory, you just call a regular JS function.

Let's say we're building a game and we want Rust to handle the "Combat Math" because it's computationally heavy. Here is how you'd expose a function to calculate damage:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn calculate_damage(base_attack: f64, defense: f64, crit_multiplier: f64) -> f64 {
    let damage = base_attack - defense;
    if damage < 0.0 {
        return 0.0;
    }
    
    // Imagine more complex math here
    damage * crit_multiplier
}

Once you compile this with wasm-pack, you can just import it in your JS file like any other module: import { calculate_damage } from './pkg/my_game_engine';. I've found that keeping your "business logic" in Rust and your "UI logic" in JS is the sweet spot for most Wasm projects.

Can Rust call JavaScript functions, or is it a one-way street?

It's definitely a two-way street. You can "import" JavaScript functions into Rust. You do this by using an extern "C" block decorated with #[wasm_bindgen]. This tells Rust: "Trust me, this function will exist in the JS environment when the code actually runs."

A common use case is logging or triggering a UI alert. If you want to call a custom JS function called showNotification from your Rust logic, it looks like this:

#[wasm_bindgen]
extern "C" {
    // This tells Rust that JS has a function with this signature
    fn showNotification(message: &str);
}

#[wasm_bindgen]
pub fn level_up(current_level: i32) {
    let new_level = current_level + 1;
    // Now we call the JS function directly!
    showNotification(&format!("Congratulations! You reached level {}", new_level));
}

One thing to watch out for: if you call a function in the extern block that doesn't actually exist in your JS environment, the app will crash at runtime. There's no compile-time check for JS functions because Rust has no way of knowing what your JS file looks like.

How do I move complex data, like structs, between the two?

Passing a single number is easy, but passing a full object is where people usually get stuck. You can't just send a Rust struct to JS because the memory layouts are completely different. However, wasm-bindgen allows you to export structs as JS classes.

Check out this example of a Player profile. By marking the struct and its impl block with #[wasm_bindgen], Rust creates a JS class that wraps the Wasm memory pointer.

#[wasm_bindgen]
pub struct Player {
    name: String,
    health: i32,
}

#[wasm_bindgen]
impl Player {
    #[wasm_bindgen(constructor)]
    pub fn new(name: String, health: i32) -> Player {
        Player { name, health }
    }

    pub fn take_damage(&mut self, amount: i32) {
        self.health -= amount;
    }

    pub fn get_health(&self) -> i32 {
        self.health
    }
}

In JavaScript, this looks incredibly natural: const hero = new Player("Aragorn", 100); hero.take_damage(20); console.log(hero.get_health());. Just keep in mind that the Player object in JS is actually a pointer to memory inside the Wasm linear heap. If you're doing this with thousands of objects, be mindful of memory leaks—you'll need to call hero.free() in JS if you want to manually drop the Rust object before the JS garbage collector gets around to it.




📋 Practical Task

Exercise: Build a Rust-powered Password Strength Validator

Your goal is to create a small interop bridge that validates password strength in Rust and reports the result back to JavaScript.

  • The Rust Side: Create a function check_password_strength that takes a String. It should return an integer: 0 for "Weak" (less than 8 characters), 1 for "Medium" (8+ characters but no numbers), and 2 for "Strong" (8+ characters including at least one digit).
  • The JS Side: Create a JavaScript function that calls your Rust validator. Based on the returned number (0, 1, or 2), it should call a JS alert() saying "Weak", "Medium", or "Strong".
  • The Glue: Ensure you use the #[wasm_bindgen] attribute on the Rust function so it is exported correctly.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.