Making Tetris in Rust and Wasm (Part 1)

The Tetris Video Framework (Part 2)

Importing and Exporting in Web Assembly


Welcome back readers. So you've survived the first article. Many have. Many... have not. This article assumes a bit of background knowledge. And a bit of intelligence on behalf of the reader. I will not explain what a function is, I will not explain what a variable is. I will not explain what it means to pass two variables as parameters when calling a function. If you do not understand the jargon you must learn like everyone else: using context clues and \ honing your Google-fu. Welcome to Part 2.

1 - A bit of history

So first, what is Web Assembly? Well actually first, what is Assembly?

Assembly is the box of building blocks for code. There are lots of assembly instructions, each instruction does something different, each instruction is totally useless, and with enough of them you can build a castle. They're like the pennies of the US monetary system. And inflation is when you use a scripting language.

So what is web assembly? Web assembly is when a shopkeeper takes a hundred dollar bill and says that in his store it's only worth one cent. That was asmjs. Then someone else comes along, says "Hey, this is a great idea", and starts minting one cent coins that are technically worth $100 but then aren't because the economy fluctuates. That's web assembly.

So to sum up:

2 - Your build environment

Ah yes, everybody's favorite part. Configuring your build environment. If you've done this before this part's fun! There are so many shells to choose from, editors to choose from, editor themes to choose from. If you're new it's a lot of useless information. They all claim to be different, they all claim to be the best, they're all liars. Except Vim. Vim really is the best. Vimming is like swimming or riding a bike. It's painful to learn but it's a skill everyone should have. I recommend this Hacker News comment by Sevensor.

The following section is a lot of information in a small space. Read everything twice before doing it yourself and please email me if something is confusing. - Robert

Alright, so I've decided on my text editor, there's also Atom, SublimeText, Notepad++, and a host of others, Now on to your shell. I'm using the Ubuntu installfor Windows Subsystem for Linux. There's also git bash, and if you're on OSX or Linux you can just use your terminal app. Choose one.

Now let's install Rust. Open your shell and paste curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh. Read everything that prints to the screen, it will tell you how to uninstall if you want to start over. Now close and open your shell again to quickly source your ~/.profile.

Now let's createa Github (if you haven't already) and set up your ssh keys. Create a repo [no readme] named ${USERNAME}.github.io. This sets up Github Pages so you can view your website online. You can also use Netlify or Heroku.

Now familiarize yourself with some beginner bash commands. Then navigate to your home directory and mkdir a project folder. We've got some initialization to do.

Bash prompt showing multiple bash commands and their successful completion. Source
               is on my github at src/source/tetris_2/initialization_step.sh
Note: I linked my project folder to the Desktop behind the scenes. You can do the same with ln -s /mnt/c ~/Desktop
Ok, so this is our initialization step. After creating our project folder we create a new Rust project using Cargo. Why is it called Cargo and not Rust? Copyright reasons. Next we install the web assembly (wasm) compilation target using Rustup. Why Rustup and not Cargo? Territorial dispute.

Now we'll create our first git commit. First initialize the project folder as a git repository. Why does it say "Reinitialized" instead of "Initialized"? Cause Cargo interacts with git under the hood. Then add the .gitignore, add the files not ignored, and commit with a message.

This is a local commit. It's not pushed to Github yet.
See, Git's designed to work in a nuclear apocalypse, so until we use git remote your repository is completely independent from all networks. So grab the SSH link for your remote repository and git remote add origin so we can push our commit. Now open Cargo.toml.

Cargo.toml edited to compile as a cdylib. See src/source/tetris_2/edited_cargo.toml

We didn't change too much. Just added the [lib] and ["cdylib"] lines. See, when we compile we want our package to be a "C dynamic library", a cdylib. We want that because we need our library to be compatible with WASM and as a rule of thumb, C is compatible with everything.
Bash prompt adding, committing, and pushing Cargo.toml. See src/source/tetris_2/commit_cargo.sh
This is your basic git workflow. git add all the files you changed, git commit -m your changes, then git push your new commits. There's also git reset to un-add a file, git diff to view your un-added changes, and git log to view your history. Here we commit our change to Cargo.toml.

Now create a file index.html, save, and commit it to test if your github.io works. You can see my last post for an example skeleton.
firefox opened to robert-snakard.github.io

3 - Rust and Wasm

Alright you made it! It's time to get started with web assembly! Open up src/lib.rs and index.html It's time for some double text editing action!
Vim opened in a split window. On the left is <code>src/lib.rs<code>, on the right is <code>index.html<code>. See src/source/tetris_2/edit_lib.rs and edit_index.html
Here's our first bit of Rust. We create a test module and a function that asserts 2 + 2 does equal 4. Now lets run the test.
Bash prompt showing the output of cargo test. See src/source/tetris_2/test.sh
Now I got a warning cause Desktop is uppercase. I'll fix that later. Let's focus on the two new objects. Cargo.lock is generated from Cargo.toml, you can ignore it. target on the other hand, has all of our compiled binaries and will become very important as we compile our Wasm.
Vim opened in a split window again. On the left we've added a function <code>two_plus<code> to <code>src/lib.rs<code>, on the right is <code>index.html<code>. See src/source/tetris_2/edit_lib_2.rs and edit_index_2.html
Now we've added a function called two_plus that takes in an integer and returns an integer. Specifically it returns the original number plus 2. I also deleted the line that segregated modules. I'll explain them at another time.
Bash prompt showing the output of the second test. See src/source/tetris_2/test_2.sh
Congratulations! Once yours passes the test you've successfully written your first Rust code! Are you ready for some wasm?
Vim opened in a split window again. On the left we've <code>pub<code>ed our <code>two_plus<code> function, on the right we're importing wasm into <code>index.html<code> and sending an alert. See src/source/tetris_2/edit_lib_3.rs and edit_index_3.html
Ok so first things first in the src/lib.rs, we added #[no_mangle] and made two_plus a public function. These are both very important for the compiler. pub means public. It exports the function. Without it, two_plus wouldn't appear in obj.instance.exports. no_mangle is a command. It stops the compiler from mangling.

Now what is mangling? Time for another history lesson. People have been writing code for a while now. There's a lot of code out there. And as much as we like to think we're all random and original, we're not. There are probably hundreds of programs out there with a function named two_plus (although actually there aren't, I checked). So if I import two programs that export a two_plus function what happens? Things break. So instead one program exports a _RN15mycrate_4a3b56d3two_plusFE and the other exports a _JX17othercrate_e36ce00ctwo_plusAB. There's an entire scheme.

Ok. So. This is kinda out of order but let's look at the index.html side. We added some script tags. The first line imports our wasm (./ for current directory, desktop.wasm cause that's my project name). After that we call our WASM function! obj because it's a wasm object, instance cause the other choice is module but we put all our functions in instance, exports cause that holds our exports, then two_plus, our function! We'll then call alert so we get a message saying it worked. Now to compile.

Bash prompt showing the output of the second test. See src/source/tetris_2/test_3.sh

Remember how I said target was going to be important later? Well it's important now. First we're going to cargo build to compile, and specify that wasm32 target we installed earlier with rustup. Then we need to go find it. The binary. Open your target directory and look for the folder wasm32. Cargo compiles in debug mode by default so you'll find your binary there but you can set the --release flag if you want.

Anyway copy your binary to ./${NAME}.wasm then commit and push to github.io.
screenshot of firefox displaying the correct alert message
Tada!

4 - Javascript and Wasm

Ok. So we can call Rust from Javascript. That's all fine and dandy. But what if we want to call Javascript functions from Rust? We've got this CanvasRenderingContext2d we've got to use and we don't want to be zigzagging from Javascript to Rust. That's just ugly. So we import them. Import from Javascript into Wasm, export from Wasm into Javascript. It's easy.
Vim opened in a split window again. On the left we've externed a function named alert, on the right we create an import object and pass it to WebAssembly. See src/source/tetris_2/import_lib.rs and import_index.html
WebAssembly takes in an importObject and Rust externs the function. When they work together you can pass Javascript functions into Rust. Some questions:

What is an importObject? Well first, importObject is just a name but a Javascript Object is a variable that you create with curly braces. They're also sometimes called Dictionaries because every entry has a key, and a value.

What is extern? Extern is an adjective. It means the function lives somewhere else and the compiler (technically the linker) will figure it out. When we compile with --target wasm32 the compiler sets it up so all our externs come from WebAssembly importObjects.

Wait, but last time we passed alert a string, why didn't we do that this time? Well first, the alert message is optional, there's no black magic trickery going on. But second, strings are hard. It can take a university course to fully grok the problem but basically, strings cause memory leaks because they're variable size.
Vim opened in a split window again. On the left we've externed a new alert that takes a pointer as a parameter. On the right we create a wrapper function around our alert that reads memory then frees it when we're done. See src/source/tetris_2/string_lib.rs and string_index.html
This is what you have to do use strings. First thing you’ll notice is that we've added an env: to our import object. That’s some boilerplate cause we’re using window.module, you can ignore it, it’s not important. Second thing you’ll notice is our Rust code is using pointers! Yes pointers! That’s because we’re accessing memory directly. When dealing with objects that can change in size (or ones that are just really big), we needt to store them in RAM. We then use pointers to point to RAM.

So src/lib.rs creates a string, then we cast it to a pointer and pass it to alert. It’s important to note this cast doesn’t transform the string. The string is always stored in memory. It just makes the pointer explicit. The unsafe is there because Rust tries to protect us from memory leaks and unprotected pointers are always dangerous.

Now index.html. We create a module object so we can use parts of obj in our imports. We then import a custom function instead of importing alert directly. This custom function accesses memory. We go through each memory location byte by byte and turn the integer located there into a letter using ASCII. Doing this we can then construct the message one letter at a time. Only after we finish the message (when we find the null character) do we free the memory and display the message.

Firefox with alert message displaying 'Hello World'