WebAssembly (WASM) has been around for a few years now. It’s a binary format that runs in the browser, allowing programmers to write code in Rust (or Java, Go, C/C++), compile it into WebAssembly and then execute the code in the browser.
I’ve been wanting to build something in WebAssembly for a while and just recently it hit me that I have a thing: mktoc! mktoc is a table of contents generator written in Rust, it comes as a Binary and a Library and can be compiled into WebAssembly with zero effort!
Quick (Technical) Overview
- The Rust code gets compiled for the target
wasm32-unknown-unknown - wasm-pack is used to compile and bundle the generated WASM code
- Some JavaScript and a static HTML page is used to build the Front-end
- The web app is deployed on GitHub pages
Compiling to WASM
Compiling to WASM is surprisingly straight forward. Rust has a target for WASM, wasm32-unknown-unknown
, and a nice build tool for generating the “glue code” around the WASM binary: wasm-pack
wasm-pack build --release --target web
This command builds the code as an optimised release with web as the target platform – other targets are bundler, nodejs, web, no-modules
– bundler
being the default here.
A few things more are required to get the Rust code to compile to WASM:
[lib]
must be set tocrate-type = ["cdylib"]
wasm-bindgen
must be added as a dependency
So the Cargo.toml
looks something like this:
[...]
[lib]
crate-type = ["cdylib"]
[dependencies]
mktoc = { path = "../" }
wasm-bindgen = "0.2.87"
The mktoc dependency is loaded relatively because I included the WASM code as a sub-directory in the mktoc repository.
With the dependencies and crate-type in place it’s finally time for some Rust code!
use mktoc;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn make_toc(content: &str) -> String {
mktoc::add_toc(
content.to_string(),
mktoc::generate_toc(content.to_string(), mktoc::Config::default()),
)
}
#[wasm_bindgen]
pub fn make_toc_only(content: &str) -> String {
mktoc::generate_toc(content.to_string(), mktoc::Config::default())
}
Here we create two functions, make_toc
and make_toc_only
, which are attributed with #[wasm_bindgen]
, which tells the compiler to make these functions available through WebAssembly.
And that’s it. The Rust code in mktoc doesn’t need to be updated at all!
For compiling, wasm-pack
is used:
wasm-pack build --release --target web
The files will be written to the pkg
directory by default and can be loaded via JavaScript.
<script type="module">
import init, { make_toc, make_toc_only } from "./pkg/webtoc.js";
init().then(() => {
// wasm code is loaded
// make_toc is the function defined in Rust
make_toc(`
# Sample
<!-- BEGIN mktoc -->
<!-- END mktoc -->
This is an example document.
## Sample heading 2
\`\`\`rust
pub fn hello(name: &str) -> String {
format!("Hello, {}!", name)
}
\`\`\``);
});
</script>
A full working example can also be found on CodePen https://codepen.io/kevingimbel/pen/GRwGrQZ – the code uses a packaged and released version from NPM at https://npmjs.com/webtoc via unpkg.
Using the WASM code in a Web App
The same code is available as a “web app” at https://kevingimbel.github.io/webtoc/ – you can paste or load your own Markdown files. When “uploading” a file there’s no need to actually transmit it to a browser: It’s accessible via JavaScript without being processed by a backend!
markdown_file.addEventListener("change", readFile);
function readFile() {
const [file] = markdown_file.files;
const reader = new FileReader();
let dot_split = file.name.split(".");
let file_ext = dot_split[dot_split.length - 1].toLowerCase()
if (file_ext !== "md" && file_ext !== "markdown") {
triggerError("Not a markdown file. Please add .md or .markdown file");
// reset the file input
markdown_file.value = "";
return;
}
reader.addEventListener("load", () => {
markdown_input.value = "";
// pass the file to make_toc
generate_toc(reader.result);
});
if (file) {
reader.readAsText(file);
}
}
Here’s how it’s done:
- Line 1:
markdown_file
identifier is the Input element. - Line 2:
readFile
will be called on “change”: The change event is triggered when a file has been selected via the input - Line 3:
markdown_file.files
contains an array of selected files – we only care about one, so we load it withconst [file]
- Line 6-7:
dot_split
andfile_ext
are used to get the file extension from the file name – we won’t process files which aren’t markdown. - Line 9-14: I’ve decided that markdown files must end in
.md
or.markdown
, if they do not an error is thrown - Line 16-20: adds a load event on the reader we created. This triggers once the file has been loaded by the browser – this happens BEFORE any form is submitted! Here we reset the input value (= remove the file from the element), then pass the file content to the
generate_toc
function - Line 22-24:Here the file is read – this will trigger the
load
event defined in line 16-20.
This setup is incredibly fast. A fairly large Markdown file is processed in milliseconds!
Takeaways
- Compiling Rust to WASM is surprisingly easy!
Without adjustments I was able to bring my Rust library to the web (and NPM!) - WASM enables Rust code to be used “Full Stack”
The mktoc code can be used to preview a Markdown file in the browser, and write it to disk via a backend - Rust is awesome
Not a takeaway, but I love Rust!
Code
All code used in this blog post is available on Github:
Further reading
- https://rustwasm.github.io/docs/wasm-pack/
- https://webassembly.org
- https://developer.mozilla.org/en-US/docs/WebAssembly
Join the discussion on Mastodon 🐘 or comment below 👇