Practical lessons from porting range-set-blaze to WASM
Do you want your Rust code to run everywhere — from large servers to web pages, robots, and even watches? In this second of three articles, I’ll show you how to use WebAssembly (WASM) to run your Rust code directly in the user’s browser.
With this technique, you can provide CPU-intensive, dynamic web pages from a — perhaps free — static web server. As a bonus, a user’s data never leaves their machine, avoiding privacy issues. For example, I offer a tool to search race results for friends, running club members, and teammates. To see the tool, go to its web page, and click “match”.
Aside: To learn more about matching names, see Use Bayes’ Theorem to Find Distinctive Names in a List in Towards Data Science.
Running Rust in the browser presents challenges. Your code doesn’t have access to a full operating system like Linux, Windows, or macOS. You have no direct access to files or networks. You have only limited access to time and random numbers. We’ll explore workarounds and solutions.
Porting code to WASM in the browser requires several steps and choices, and navigating these can be time-consuming. Missing a step can lead to failure. We’ll reduce this complication by offering nine rules, which we’ll explore in detail:
- Confirm that your existing app works with WASM WASI and create a simple JavaScript web page.
- Install the wasm32-unknown-unknown target, wasm-pack, wasm-bindgen-cli, and Chrome for Testing & Chromedriver.
- Make your project cdylib (and rlib), add wasm-bindgen dependencies, and test.
- Learn what types wasm-bindgen supports.
- Change functions to use supported types. Change files to generic BufRead.
- Adapt tests, skipping those that don’t apply.
- Change to JavaScript-friendly dependencies, if necessary. Run tests.
- Connect your web page to your functions.
- Add wasm-pack to your CI (continuous integration) tests.
Aside: These articles are based on a three-hour workshop that I presented at RustConf24 in Montreal. Thanks to the participants of that workshop. A special thanks, also, to the volunteers from the Seattle Rust Meetup who helped test this material. These articles replace an article I wrote last year with updated information.
As with the first article in this series, before we look at the rules one by one, let’s define our terms.
- Native: Your home OS (Linux, Windows, macOS)
- Standard library (std): Provides Rust’s core functionality — Vec, String, file input/output, networking, time.
- WASM: WebAssembly (WASM) is a binary instruction format that runs in most browsers (and beyond).
- WASI: WebAssembly System Interface (WASI) allows outside-the-browser WASM to access file I/O, networking (not yet), and time handling.
- no_std: Instructs a Rust program not to use the full standard library, making it suitable for small, embedded devices or highly resource-constrained environments.
- alloc: Provides heap memory allocation capabilities (Vec, String, etc.) in no_std environments, essential for dynamically managing memory.
Based on my experience with range-set-blaze, a data structure project, here are the decisions I recommend, described one at a time. To avoid wishy-washiness, I’ll express them as rules.
Rule 1: Confirm that your existing app works with WASM WASI and create a simple JavaScript web page.
Getting your Rust code to run in the browser will be easier if you meet two prerequisites:
- Get your Rust code running in WASM WASI.
- Get some JavaScript to run in the browser.
For the first prerequisite, see Nine Rules for Running Rust on WASM WASI in Towards Data Science. That article — the first article in this series — details how to move your code from your native operating system to WASM WASI. With that move, you will be halfway to running on WASM in the Browser.
Confirm your code runs on WASM WASI via your tests:
rustup target add wasm32-wasip1
cargo install wasmtime-cli
cargo test --target wasm32-wasip1
For the second prerequisite, show that you can create some JavaScript code and run it in a browser. I suggest adding this index.html file to the top level of your project:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Line Counter</title>
</head>
<body>
<h1>Line Counter</h1>
<input type="file" id="fileInput" />
<p id="lineCount">Lines in file: </p>
<script>
const output = document.getElementById('lineCount');
document.getElementById('fileInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) { output.innerHTML = ''; return } // No file selected
const reader = new FileReader();
// When the file is fully read
reader.onload = async (e) => {
const content = e.target.result;
const lines = content.split(/rn|n/).length;
output.textContent = `Lines in file: ${lines}`;
};
// Now start to read the file as text
reader.readAsText(file);
});
</script>
</body>
</html>
Now, serve this page to your browser. You can serve web pages via an editor extension. I use Live Preview for VS Code. Alternatively, you can install and use a standalone web server, such as Simple Html Server:
cargo install simple-http-server
simple-http-server --ip 127.0.0.1 --port 3000 --index
# then open browser to http://127.0.0.1:3000
You should now see a web page on which you can select a file. The JavaScript on the page counts the lines in the file.
Let’s go over the key parts of the JavaScript because later we will change it to call Rust.
Aside: Must you learn JavaScript to use Rust in the browser? Yes and no. Yes, you’ll need to create at least some simple JavaScript code. No, you may not need to “learn” JavaScript. I’ve found ChatGPT good enough to generate the simple JavaScript that I need.
- See what file the user chose. If none, just return:
const file = event.target.files[0];
if (!file) { output.innerHTML = ''; return } // No file selected
- Create a new FileReader object, do some setup, and then read the file as text:
const reader = new FileReader();
// ... some setup ...
// Now start to read the file as text
reader.readAsText(file);
- Here is the setup. It says: wait until the file is fully read, read its contents as a string, split the string into lines, and display the number of lines.
// When the file is fully read
reader.onload = async (e) => {
const content = e.target.result;
const lines = content.split(/rn|n/).length;
output.textContent = `Lines in file: ${lines}`;
};
With the prerequisites fulfilled, we turn next to installing the needed WASM-in-the-Browser tools.
Rule 2: Install the wasm32-unknown-unknown target, wasm-pack, wasm-bindgen-cli, and Chrome for Testing & Chromedriver.
We start with something easy, installing these three tools:
rustup target add wasm32-unknown-unknown
cargo install wasm-pack --force
cargo install wasm-bindgen-cli --force
The first line installs a new target, wasm32-unknown-unknown. This target compiles Rust to WebAssembly without any assumptions about the environment the code will run in. The lack of assumptions makes it suitable to run in browsers. (For more on targets, see the previous article’s Rule #2.)
The next two lines install wasm-pack and wasm-bindgen-cli, command-line utilities. The first builds, packages, and publishes into a form suitable for use by a web page. The second makes testing easier. We use –force to ensure the utilities are up-to-date and mutually compatible.
Now, we get to the annoying part, installing Chrome for Testing & Chromedriver. Chrome for Testing is an automatable version of the Chrome browser. Chromedriver is a separate program that can take your Rust tests cases and run them inside Chrome for Testing.
Why is installing them annoying? First, the process is somewhat complex. Second, the version of Chrome for Testing must match the version of Chromedriver. Third, installing Chrome for Testing will conflict with your current installation of regular Chrome.
With that background, here are my suggestions. Start by installing the two programs into a dedicated subfolder of your home directory.
- Linux and WSL (Windows Subsystem for Linux):
cd ~
mkdir -p ~/.chrome-for-testing
cd .chrome-for-testing/
wget https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/linux64/chrome-linux64.zip
wget https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/linux64/chromedriver-linux64.zip
unzip chrome-linux64.zip
unzip chromedriver-linux64.zip
- Windows (PowerShell):
New-Item -Path $HOME -Name ".chrome-for-testing" -ItemType "Directory"
Set-Location -Path $HOME.chrome-for-testing
bitsadmin /transfer "ChromeDownload" https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/win64/chrome-win64.zip $HOME.chrome-for-testingchrome-win64.zip
bitsadmin /transfer "ChromeDriverDownload" https://storage.googleapis.com/chrome-for-testing-public/129.0.6668.70/win64/chromedriver-win64.zip $HOME.chrome-for-testingchromedriver-win64.zip
Expand-Archive -Path "$HOME.chrome-for-testingchrome-win64.zip" -DestinationPath "$HOME.chrome-for-testing"
Expand-Archive -Path "$HOME.chrome-for-testingchromedriver-win64.zip" -DestinationPath "$HOME.chrome-for-testing"
Aside: I’m sorry but I haven’t tested any Mac instructions. Please see the Chrome for Testing web page and then try to adapt the Linux method. If you let me know what works, I’ll update this section.
This installs version 129.0.6668.70, the stable version as of 9/30/2024. If you wish, check the Chrome for Testing Availability page for newer stable versions.
Next, we need to add these programs to our PATH. We can add them temporarily, meaning only for the current terminal session:
- Linux and WSL (just for this session):
export PATH=~/.chrome-for-testing/chrome-linux64:~/.chrome-for-testing/chromedriver-linux64:$PATH
- Windows (just for this session):
# PowerShell
$env:PATH = "$HOME.chrome-for-testingchrome-win64;$HOME.chrome-for-testingchromedriver-win64;$PATH"
# or, CMD
set PATH=%USERPROFILE%.chrome-for-testingchrome-win64;%USERPROFILE%.chrome-for-testingchromedriver-win64;%PATH%
Alternatively, we can add them to our PATH permanently for all future terminal sessions. Understand that this may interfere with access to your regular version of Chrome.
Linux and WSL (then restart your terminal):
echo 'export PATH=~/.chrome-for-testing/chrome-linux64:~/.chrome-for-testing/chromedriver-linux64:$PATH' >> ~/.bashrc
Windows (PowerShell, then restart your terminal):
[System.Environment]::SetEnvironmentVariable("Path", "$HOME.chrome-for-testingchrome-win64;$HOME.chrome-for-testingchromedriver-win64;" + $env:PATH, [System.EnvironmentVariableTarget]::User)
Once installed, you can verify the installation with:
chromedriver --version
Aside: Can you skip installing and using Chrome for Testing and Chromedriver? Yes and no. If you skip them, you’ll still be able to create WASM from your Rust. Moreover, you’ll be able to call that WASM from JavaScript in a web page.
However, your project — like all good code — should already contain tests. If you skip Chrome for Testing, you will not be able to run WASM-in-the-Browser test cases. Moreover, WASM in the Browser violates Rust’s “If it compiles, it works” principle. Specifically, if you use an unsupported feature, like file access, compiling to WASM won’t catch the error. Only test cases can catch such errors. This makes running test cases critically important.
Now that we have the tools to run tests in the browser, let’s try (and almost certainly fail) to run those tests.
Rule 3: Make your project cdylib (and rlib), add wasm-bindgen dependencies, and test.
The wasm-bindgen package is a set of automatically generated bindings between Rust and JavaScript. It lets JavaScript call Rust.
To prepare your code for WASM in the Browser, you’ll make your project a library project. Additionally, you’ll add and use wasm-bindgen dependencies. Follow these steps:
- If your project is executable, change it to a library project by renaming src/main.rs to src/lib.rs. Also, comment out your main function.
- Make your project create both a static library (the default) and a dynamic library (needed by WASM). Specifically, edit Cargo.toml to include:
[lib]
crate-type = ["cdylib", "rlib"]
- Add wasm-bindgen dependencies:
cargo add wasm-bindgen
cargo add wasm-bindgen-test --dev
- Create or update .cargo/config.toml (not to be confused with Cargo.toml) to include:
[target.wasm32-unknown-unknown]
runner = "wasm-bindgen-test-runner"
Next, what functions do you wish to be visible to JavaScript? Mark those functions with #[wasm_bindgen] and make them pub (public). At the top of the functions’ files, add use wasm_bindgen::prelude::*;.
Aside: For now, your functions may fail to compile. We’ll address this issue in subsequent rules.
What about tests? Everywhere you have a #[test] add a #[wasm_bindgen_test]. Where needed for tests, add this use statement and a configuration statement:
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
If you like, you can try the preceding steps on a small, sample project. Install the sample project from GitHub:
# cd to the top of a work directory
git clone --branch native_version --single-branch https://github.com/CarlKCarlK/rustconf24-good-turing.git good-turing
cd good-turing
cargo test
cargo run pg100.txt
Here we see all these changes on the small, sample project’s lib.rs:
// --- May fail to compile for now. ---
use wasm_bindgen::prelude::*;
// ...
#[wasm_bindgen]
pub fn good_turing(file_name: &str) -> Result<(u32, u32), io::Error> {
let reader = BufReader::new(File::open(file_name)?);
// ...
}
// fn main() {
// ...
// }
#[cfg(test)]
mod tests {
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
// ...
#[test]
#[wasm_bindgen_test]
fn test_process_file() {
let (prediction, actual) = good_turing("./pg100.txt").unwrap();
// ...
}
}
With these changes made, we’re ready to test (and likely fail):
cargo test --target wasm32-unknown-unknown
On this sample, the compiler complains that WASM in the Browser doesn’t like to return tuple types, here, (u32, u32). It also complains that it doesn’t like to return a Result with io::Error. To fix these problems, we’ll need to understand which types WASM in the Browser supports. That’s the topic of Rule 4.
What will happen after we fix the type problems and can run the test? The test will still fail, but now with a runtime error. WASM in the Browser doesn’t support reading from files. The sample test, however, tries to read from a file. In Rule 5, we’ll discuss workarounds for both type limitations and file-access restrictions.
Rule 4: Learn what types wasm-bindgen supports.
Rust functions that JavaScript can see must have input and output types that wasm-bindgen supports. Use of unsupported types causes compiler errors. For example, passing in a u32 is fine. Passing in a tuple of (u32, 32) is not.
More generally, we can sort Rust types into three categories: “Yep!”, “Nope!”, and “Avoid”.
Yep!
This is the category for Rust types that JavaScript (via wasm-bindgen) understands well.
We’ll start with Rust’s simple copy types:
Two items surprised me here. First, 64-bit integers require extra work on the JavaScript side. Specifically, they require the use of JavaScript’s BigInt class. Second, JavaScript does not support 128-bit integers. The 128-bit integers are “Nopes”.
Turning now to String-related and vector-related types:
These super useful types use heap-allocated memory. Because Rust and JavaScript manage memory differently, each language makes its own copy of the data. I thought I might avoid this allocation by passing a &mut [u8] (mutable slice of bytes) from JavaScript to Rust. That didn’t work. Instead of zero copies or one, it copied twice.
Next, in Rust we love our Option and Result types. I’m happy to report that they are “Yeps”.
A Rust Some(3) becomes a JavaScript 3, and a Rust None becomes a JavaScript null. In other words, wasm-bindgen converts Rust’s type-safe null handling to JavaScript’s old-fashioned approach. In both cases, null/None is handled idiomatically within each language.
Rust Result behaves similarly to Option. A Rust Ok(3) becomes a JavaScript 3, and a Rust Err(“Some error message”) becomes a JavaScript exception that can be caught with try/catch. Note that the value inside the Rust Err is restricted to types that implement the Into<JsValue> trait. Using String generally works well.
Finally, let’s look at struct, enum, and JSValue, our last set of “Yeps”:
Excitingly, JavaScript can construct and call methods on your Rust structs. To enable this, you need to mark the struct and any JavaScript-accessible methods with #[wasm_bindgen].
For example, suppose you want to avoid passing a giant string from JavaScript to Rust. You could define a Rust struct that processes a series of strings incrementally. JavaScript could construct the struct, feed it chunks from a file, and then ask for the result.
JavaScript’s handling of Rust enums is less exciting. It can only handle enums without associated data (C-like enums) and treats their values as integers.
In the middle of the excitement spectrum, you can pass opaque JavaScript values to Rust as JsValue. Rust can then dynamically inspect the value to determine its subtype or—if applicable—call its methods.
That ends the “Yeps”. Time to look at the “Nopes”.
Nope!
This is the category for Rust types that JavaScript (via wasm-bindgen) doesn’t handle.
Not being able to pass, for example, &u8 by reference is fine because you can just use u8, which is likely more efficient anyway.
Not being able to return a string slice (&str) or a regular slice (&[u8]) is somewhat annoying. To avoid lifetime issues, you must instead return an owned type like String or Vec<u8>.
You can’t accept a mutable String reference (&mut String). However, you can accept a String by value, mutate it, and then return the modified String.
How do we workaround the “Nopes”? In place of fixed-length arrays, tuples, and 128-bit integers, use vectors (Vec<T>) or structs.
Rust has sets and maps. JavaScript has sets and maps. The wasm-bindgen library, however, will not automatically convert between them. So, how can you pass, for example, a HashSet from Rust to JavaScript? Wrap it in your own Rust struct and define needed methods. Then, mark the struct and those methods with #[wasm-bindgen].
And now our third category.
Avoid
This is the category for Rust types that JavaScript (via wasm-bindgen) allows but that you shouldn’t use.
Avoid using usize and isize because most people will assume they are 64-bit integers, but in WebAssembly (WASM), they are 32-bit integers. Instead, use u32, i32, u64, or i64.
In Rust, char is a special u32 that can contain only valid Unicode scalar values. JavaScript, in contrast, treats a char as a string. It checks for Unicode validity but does not enforce that the string has a length of one. If you need to pass a char from JavaScript into Rust, it’s better to use the String type and then check the length on the Rust side.
Rule 5: Change functions to use supported types. Change files to generic BufRead.
With our knowledge of wasm-bindgen supported types, we can fixup the functions we wish to make available to JavaScript. We left Rule 3’s example with a function like this:
#[wasm_bindgen]
pub fn good_turing(file_name: &str) -> Result<(u32, u32), io::Error> {
let reader = BufReader::new(File::open(file_name)?);
// ...
}
We, now, change the function by removing #[wasm_bindgen] pub. We also change the function to read from a generic reader rather than a file name. Using BufRead allows for more flexibility, enabling the function to accept different types of input streams, such as in-memory data or files.
fn good_turing<R: BufRead>(reader: R) -> Result<(u32, u32), io::Error> {
// delete: let reader = BufReader::new(File::open(file_name)?);
// ...
}
JavaScript can’t see this function, so we create a wrapper function that calls it. For example:
#[wasm_bindgen]
pub fn good_turing_byte_slice(data: &[u8]) -> Result<Vec<u32>, String> {
let reader = BufReader::new(data);
match good_turing(reader) {
Ok((prediction, actual)) => Ok(vec![prediction, actual]),
Err(e) => Err(format!("Error processing data: {e}")),
}
}
This wrapper function takes as input a byte slice (&[u8]), something JavaScript can pass. The function turns the byte slice into a reader and calls the inner good_turing. The inner function returns a Result<(u32, u32), io::Error>. The wrapper function translates this result into Result<Vec<u32>, String>, a type that JavaScript will accept.
In general, I’m only willing to make minor changes to functions that will run both natively and in WASM in the Browser. For example, here I’m willing to change the function to work on a generic reader rather than a file name. When JavaScript compatibility requires major, non-idiomatic changes, I create a wrapper function.
In the example, after making these changes, the main code now compiles. The original test, however, does not yet compile. Fixing tests is the topic of Rule 6.
Rule 6: Adapt tests, skipping those that don’t apply.
Rule 3 advocated marking every regular test (#[test]) to also be a WASM-in-the-Browser test (#[wasm_bindgen_test]). However, not all tests from native Rust can be run in a WebAssembly environment, due to WASM’s limitations in accessing system resources like files.
In our example, Rule 3 gives us test code that does not compile:
#[cfg(test)]
mod tests {
use super::*;
use wasm_bindgen_test::wasm_bindgen_test;
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[test]
#[wasm_bindgen_test]
fn test_process_file() {
let (prediction, actual) = good_turing("./pg100.txt").unwrap();
assert_eq!(prediction, 10223);
assert_eq!(actual, 7967);
}
}
This test code fails because our updated good_turing function expects a generic reader rather than a file name. We can fix the test by creating a reader from the sample file:
use std::fs::File;
#[test]
fn test_process_file() {
let reader = BufReader::new(File::open("pg100.txt").unwrap());
let (prediction, actual) = good_turing(reader).unwrap();
assert_eq!(prediction, 10223);
assert_eq!(actual, 7967);
}
This is a fine native test. Unfortunately, we can’t run it as a WASM-in-the-Browser test because it uses a file reader — something WASM doesn’t support.
The solution is to create an additional test:
#[test]
#[wasm_bindgen_test]
fn test_good_turing_byte_slice() {
let data = include_bytes!("../pg100.txt");
let result = good_turing_byte_slice(data).unwrap();
assert_eq!(result, vec![10223, 7967]);
}
At compile time, this test uses the macro include_bytes! to turn a file into a WASM-compatible byte slice. The good_turing_byte_slice function turns the byte slice into a reader and calls good_turing. (The include_bytes macro is part of the Rust standard library and, therefore, available to tests.)
Note that the additional test is both a regular test and a WASM-in-the-Browser test. As much as possible, we want our tests to be both.
In my range-set-blaze project, I was able to mark almost all tests as both regular and WASM in the Browser. One exception: a test used a Criterion benchmarking function. Criterion doesn’t run in WASM in the Browser, so I marked that test regular only (#[test]).
With both our main code (Rule 5) and our test code (Rule 6) fixed, can we actually run our tests? Not necessarily, we may need to find JavaScript friendly dependences.
Aside: If you are on Windows and run WASM-in-the-Browser tests, you may see “ERROR tiny_http] Error accepting new client: A blocking operation was interrupted by a call to WSACancelBlockingCall. (os error 10004)” This is not related to your tests. You may ignore it.
Rule 7: Change to JavaScript-friendly dependencies, if necessary. Run tests.
Dependencies
The sample project will now compile. With my range-set-blaze project, however, fixing my code and tests was not enough. I also needed to fix several dependencies. Specifically, I needed to add this to my Cargo.toml:
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies]
getrandom = { version = "0.2", features = ["js"] }
web-time = "1.1.0"
These two dependences enable random numbers and provide an alternative time library. By default, WASM in the Browser has no access to random numbers or time. Both the dependences wrap JavaScript functions making them accessible to and idiomatic for Rust.
Aside: For more information on using cfg expressions in Cargo.toml, see my article: Nine Rust Cargo.toml Wats and Wat Nots: Master Cargo.toml formatting rules and avoid frustration | Towards Data Science (medium.com).
Look for other such JavaScript-wrapping libraries in WebAssembly — Categories — crates.io. Popular crates that I haven’t tried but look interesting include:
- reqwest— features=[“wasm”]— HTTP network access
- plotters — Plotting — includes a demo that controls the HTML canvas object from Rust
- gloo — Toolkit of JavaScript wrappers
Also see Rule 7 in the previous article — about WASM WASI — for more about fixing dependency issues. In the next article in this series — about no_std and embedded — we’ll go deeper into more strategies for fixing dependencies.
Run Tests
With our dependencies fixed, we can finally run our tests, both regular and WASM in the Browser:
cargo test
cargo test --target wasm32-unknown-unknown
Recall that behind the scenes, our call to cargo test –target wasm32-unknown-unknown:
- Looks in .cargo/config.toml and sees wasm-bindgen-test-runner (Rule 3).
- Calls wasm-bindgen-test-runner.
- Uses Chromedriver to run our tests in Chrome for Testing. (Rule 2, be sure Chrome for Testing and Chromedriver are on your path).
With our tests working, we’re now ready to call our Rust code from a web page.
Rule 8: Connect your web page to your functions.
To call your Rust functions from a web page you must first package your Rust library for the web. We installed wasm-pack in Rule 2. Now, we run it:
wasm-pack build --target web
This compiles your project and creates a pkg output directory that JavaScript understands.
Example
In Rule 1, we created an index.html file that didn’t call Rust. Let’s change it now so that it does call Rust. Here is an example of such an index.html followed by a description of the changes of interest.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Good-Turing Estimation</title>
</head>
<body>
<h1>Good-Turing Estimation</h1>
<input type="file" id="fileInput" />
<p id="lineCount"></p>
<script type="module">
import init, { good_turing_byte_slice } from './pkg/good_turing.js'; // These files are generated by `wasm-pack build --target web`
const output = document.getElementById('lineCount');
document.getElementById('fileInput').addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) { output.innerHTML = ''; return } // No file selected
const reader = new FileReader();
// When the file is fully read
reader.onload = async (e) => {
await init(); // Ensure 'good_turing_byte_slice' is ready
// View the memory buffer as a Uint8Array
const u8array = new Uint8Array(e.target.result);
try { // Actually run the WASM
const [prediction, actual] = good_turing_byte_slice(u8array);
output.innerHTML =
`Prediction (words that appear exactly once on even lines): ${prediction.toLocaleString()}<br>` +
`Actual distinct words that appear only on odd lines: ${actual.toLocaleString()}`;
} catch (err) { // Or output an error
output.innerHTML = `Error: ${err}`;
}
};
// Now start to read the file as memory buffer
reader.readAsArrayBuffer(file);
});
</script>
</body>
</html>
Let’s go through the changes of interest.
- The line below imports two functions into JavaScript from the module file pkg/good_turing.js, which we created using wasm-pack. The default function, init, initializes our Rust-generated WebAssembly (WASM) module. The second function, good_turing_byte_slice, is explicitly imported by including its name in curly brackets.
import init, { good_turing_byte_slice } from './pkg/good_turing.js';
- Create a new FileReader object, do some setup, and then read the file as an array of bytes.
const reader = new FileReader();
// ... some setup code ...
// Now start to read the file as bytes.
reader.readAsArrayBuffer(file);
- Here is how we setup code that will run after the file is fully read:
reader.onload = async (e) => {
//...
};
- This line ensures the WASM module is initialized. The first time it’s called, the module is initialized. On subsequent calls, it does nothing because the module is already ready.
await init(); // Ensure 'good_turing_byte_slice' is ready
- Extract the byte array from the read file.
// View the memory buffer as a Uint8Array
const u8array = new Uint8Array(e.target.result);
- Call the Rust-generated WASM function.
const [prediction, actual] = good_turing_byte_slice(u8array);
Aside: Here good_turing_byte_slice is a regular (synchronous) function. If you want, however, you can mark it async on the Rust side and then call it with await on the JavaScript side. If your Rust processing is slow, this can keep your web page more lively.
- Display the result.
output.innerHTML =
`Prediction (words that appear exactly once on even lines): ${prediction.toLocaleString()}<br>` +
`Actual distinct words that appear only on odd lines: ${actual.toLocaleString()}`;
- If there is an error, display the error message.
try { // Actually run the WASM
// ...
} catch (err) { // Or output an error
output.innerHTML = `Error: ${err}`;
}
The final code of the sample project is on GitHub, including a README.md that explains what it is doing. Click this link for a live demo.
range-set-blaze
I ported range-set-blaze to WASM at a user’s request so that they could use it inside their own project. The range-set-blaze project is typically used as a library in other projects. In other words, you normally wouldn’t expect range-set-blaze to be the centerpiece of a web page. Nevertheless, I did make a small demo page. You can browse it or inspect its index.html. The page shows how range-set-blaze can turn a list of integers into a sorted list of disjoint ranges.
Aside: Host Your WASM-in-the-Browser Project on GitHub for Free
1. In your project, create a docs folder.
2. Do wasm-pack build –target web.
3. Copy (don’t just move) index.html and pkg into docs.
4. Delete the .gitignore file in docs/pkg.
5. Check the project into GitHub.
6. Go to the project on GitHub. Then go to “Settings”, “Pages”.
7. Set the branch (in my case main) and the folder to docs. Save.
8. The URL will be based on your account and project names, for example, https://carlkcarlk.github.io/rustconf24-good-turing/
9. To update, repeat steps 2 through 5 (inclusive).
Rule 9: Add wasm-pack to your CI (continuous integration) tests.
Your project is now compiling to WASM in the Browser, passing tests, and showcased on a web page. Are you done? Not quite. Because, as I said in the first article:
If it’s not in CI, it doesn’t exist.
Recall that continuous integration (CI) is a system that can automatically run your tests every time you update your code, ensuring that your code continues to work as expected. In my case, GitHub hosts my project. Here’s the configuration I added to .github/workflows/ci.yml to test my project on WASM in the browser:
test_wasm_unknown_unknown:
name: Test WASM unknown unknown
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Rust
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Install wasm-pack
run: |
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Run WASM tests with Chrome
run: |
rustup target add wasm32-unknown-unknown
wasm-pack test --chrome --headless
By integrating WASM in the Browser into CI, I can confidently add new code to my project. CI will automatically test that all my code continues to support WASM in the browser in the future.
So, there you have it — nine rules for porting your Rust code to WASM in the Browser. Here is what surprised me:
The Bad:
- It’s hard to set up testing for WASM in the Browser. Specifically, Chrome for Testing and Chromedriver are hard to install and manage.
- WASM in the Browser violates Rust’s saying “If it compiles, it works”. If you use an unsupported feature — for example, direct file access — the compiler won’t catch the error. Instead, you will fail at runtime.
- Passing strings and byte vectors creates two copies of your data, one on the JavaScript side and one on the Rust side.
The Good:
- WASM in the Browser is useful and fun.
- You can mark your regular tests to also run in WASM in the Browser. Just mark your tests with both attributes:
#[test]
#[wasm_bindgen_test]
- You can run on WASM in the Browser without needing to port to no_std. Nevertheless, WASM in the Browser is useful as a steppingstone toward running on embedded/no_std.
Stay tuned! In the next article, I’ll show you how to port your Rust code to run in an embedded environment via no_std. This allows your code to run in small devices which I find very cool.
Interested in future articles? Please follow me on Medium. I write about Rust and Python, scientific programming, machine learning, and statistics. I tend to write about one article per month.
Nine Rules for Running Rust in the Browser was originally published in Towards Data Science on Medium, where people are continuing the conversation by highlighting and responding to this story.
Originally appeared here:
Nine Rules for Running Rust in the Browser
Go Here to Read this Fast! Nine Rules for Running Rust in the Browser