WebAssembly from Scratch / Part One
If you have not come across WebAssembly (a.k.a. “WASM”) before, you may be wondering what exactly is it and why would anyone bother spending any time on it? In the nutshell, WASM is a way of running native code (think C/C++ kind of binary code) within confines of the web browser without sacrificing the app security or stability. The idea behind WASM is that we should be able to grab any programming language, build a library and then load it as a part of the web app as if it was just another module.
The idea is not new. In the past, there were quite a few attempts to bring native code into the browser – remember ActiveX for one? WebAssembly builds upon the previous attempts by providing critical pieces that were missing before:
- It prescribes the way Assembly should be compiled and loaded into the DOM
- It is de-facto living standard (albeit still rather fluid) supported by most modern browsers
This is all good but the question remains, why should you bother?
Why this guide?
The goal of this guide is simple: by the end of this part, you should be able to put together a real-life development environment that could be used in production development. We will go through the toolchains and plugins needed to set things up, and if everything goes well, then by the end of this part you should have a working “Hello world!” WebAssembly loaded in your browser.
In the following parts, we will dig deeper into the interop, loading and making calls to our assembly in different ways, optimization flags and basic troubleshooting
What are we going to build?
In this guide, we are building a Windows-based dev environment using the following stack:
- Visual Studio Code (VSCode) for code editing and running various plugins
- Rust language for coding the actual WebAssembly
- Node.js for the “host” (browser) app
- Webpack for bundling it all together. We will also use Webpack dev server for testing and debugging
Mac-based environment works in exactly the same way, the only difference is the way the individual components are installed
Rust is a relatively new language that has been specifically designed for projects that need to be lightweight and run on limited hardware resources (think embedding). Rust is a great candidate for WebAssembly because it compiles into a small lean binary that does not need an expensive runtime. Also, its clever language design eliminates many common C-style errors and makes the code much more robust.
Rust is not the only language that could be used in WebAssembly – there are several dozen existing ports and shims (if curious, check out this list) but Rust is one of the best candidates at this point.
Installing the tools
Step 1: VSCode + Git
First things first, before we dive into the WebAssembly-specific things, we need to install the tools. First, we will need to download and install these:
Git version is not important so if you already have Git installed then you can skip this one. We will need Git at the later steps because some of the Node modules are built from the source, which requires Git.
If you are using full version of the Visual Studio – your configuration steps will be slightly different than the one described in this guide.
Step 2: Installing Build Tools for VS
This is a required step in order to make sure we can build the required components. Assuming you have Visual Studio 2019 installed, follow this link to download the Build Tools. Scroll down to the section titled “Tools for Visual Studio 2019” and download the installer for additional VS payloads (see the following screenshot)
Once you run the installer, you should see a list of available payloads. At the minimum, we need to make sure the “C++ build tools” section is selected (see the following screenshot). You may also choose to select “C++ CLang tools” item on the right (optional). Once you are happy with your selections, click “Install”. Now is a good time to get coffee or catch on the latest Netflix episodes – the installation will take a while.
Step 3: Rust and Cargo
Once the VS payload is installed, we are ready to move onto the next step – installing Rust. On Windows, you will need to download “rust-init” installer from here. Rust-init is a terminal-based install script that takes care of everything including modifying the PATH etc. It installs Rust compiler and Cargo, Rust package management tool (if you are coming from Node development, Cargo is much like npm). Heads up: installing Rust takes a few minutes. More coffee time!
Once rust-init finished, close the command window and open a new one (type “cmd” in the search and hit Enter). Type “rustc –version” and hit Enter. If you see something like the following screenshot – everything is going well. To make sure, type “cargo” and hit Enter. You should see a list of cargo command options.
If you see an error message saying that “rustc is not recognized as an internal or external command”, then it is likely due to path-related problems and you may need to edit your path manually – Rust download page tells you where the tools are installed
Step 4: Adding Rust support to Visual Studio
Visual Studio Marketplace has quite a few Rust extensions. At the time of this writing, the “official” RLS plugin appears to be the most stable and complete of the bunch. At the very minimum, we will need the Rust compiler plugin; a few other plugins will make your life easier when dealing with WebAssemblies. Open the VS Extension palette and grab the following plugins:
- rust-lang (RLS) – required
- “Better TOML” – optional, recommended. TOML is the language that describes Rust compiler options etc.
- “WebAssembly Toolkit” – optional. Install this one if you need to inspect assemblies and view the binary/textual representation of the compiled code
In case you wondered, here is what my extension palette looks like:
Step 5: Node.js
We are almost done. The last piece we need to install is Node.js, which we will use for building the host application skeleton (this will be our “test harness” for running the assembly). Also, we need Node to run Webpack bundler. So go to the Node download page and grab the “current” build. NOTE: if you are running Windows 10, you must use Node version 13 or later. Node 12 installer has issues with some Windows 10 editions.
That’s it. We are done installing and ready to play!
Creating our first WebAssembly
Now that we have all the tool, let’s create our very first assembly. Open up the Command prompt and navigate to the folder where you would like to create our new project (I keep mine in the “Developer” folder of the home directory). Now run the following command:
npm init rust-webpack first-wasm
Here “first-wasm” is the name of our new project – you are welcome to pick any name you like. What happens behind the scene is npm cloning this “template” repository and initializing package.json, making the project ready for the first build. Once finished, you should see something like this:
Now open Visual Studio and the new project folder to the workspace – in my case, I will be adding “C:\Users\Alex\Developer\first-wasm”. After I added the project to the workspace, my Visual Studio looks like this:
Let’s take a brief tour of the new project:
- “src” is the Rust source code – not particularly useful for now, this your typical “Hello world!” thing. All it does is calling the “host” console.log() function and printing “Hello world!” to the browser console. NOTE: when you open a Rust code file for the very first time, the VS Rust extension will prompt you to install RLS (Rust Language Server). Say “Yes” and let it install RLS and all the parts
- “target” is a temporary compilation folder. We don’t need to touch it.
- “tests” is self-explanatory. Unit tests live there
Now let’s try building the assembly. Hit “Shift+Ctrl+B” to “Run Build Task”. Visual Studio should show a menu of three auto-recognized tasks, the top one is “npm build”. Select that one and hit Enter. Now you will be asked whether you want to scan the task output. For this first exercise, it is perfectly fine to continue without scanning.
The first build will take longer than normal because the build will update its dependencies and one of them is “wasm-pack“, which is Webpack counterpart for the WebAssembly. This is why we had to install the C++ build tools in the earlier steps!
After a while, the build should finish and your VS terminal window should look like this:
Fantastic! A brief look at the folder structure reveals that we now have two additional folders:
- “pkg” – intermediary build folder created by wasm-pack tool
- “dist” – is our finished application
Now just a few finishing touches and we are ready to fly. First, open Cargo.toml file and make the following changes:
- First, add the “[lib]” section exactly as show below (lines 11-12). This will instruct Rust to build a dynamic library
- Second, uncomment line 16 to enable “wee_alloc” allocator. This step is optional
Now we are almost ready. To make sure our app can run on all browsers, we need to add a polyfill for TextEncoder/TextDecoder classes that are not supported in Microsoft Edge browser. Go to this Github repository and download “lib/encoding-indexes.js” and “lib/encoding.js“. Save the files to the “static” folder of our project. Now edit the “static/index.html” file and add these two to the head, like so:
Now let’s run the project. Hit “Terminal > Run Task > npm start”, a new build will kick in. The first time will be slow, the subsequent builds are usually blazing fast. Once the build is over, you should have a browser window open to “localhost:8080” – if not, open a new one and point to that URL.
Now is the moment of truth – open the browser Console and see what’s in there. In my browser, it shows this:
Now that we have successfully configured the environment, we have all the tools needed to start developing a more useful code. In the next part we will take a closer look at the interop, will play with the “glue” loader and see if we can optimize our assembly to run faster.
Let me know what you think!
Did you have any problem following this guide? Do you know a better or easier way of getting the job done? What will you build next? Let me know!