Rust is a programming language that originated at Mozilla Research in 2010. Today, it’s used by all the big companies.
Both Amazon and Microsoft endorsed it as the best alternative to C/C++ for their systems. But Rust doesn’t stop there. Companies like Figma and Discord are now leading the way by also using Rust in their client applications.
This Rust tutorial aims to give a brief overview of Rust, how to use it in the browser, and when you should consider using it. I’ll start by comparing Rust with JavaScript, and then walk you through the steps to get Rust up and running in the browser. Finally, I’ll present a quick performance evaluation of my COVID simulator web app that uses Rust and JavaScript.
Rust in a Nutshell
Table of Contents
Rust is conceptually very different from JavaScript. But there are also similarities to point out. Let’s have a look at both sides of the coin.
Similarities
Both languages have a modern package managing system. JavaScript has npm, Rust has Cargo. Instead of package.json
, Rust has Cargo.toml
for dependency management. To create a new project, use cargo init
, and to run it, use cargo run
. Not too alien, is it?
There are many cool features in Rust that you’ll already know from JavaScript, just with a slightly different syntax. Take this common JavaScript pattern to apply a closure to every element in an array:
let staff = [ {name: "George", money: 0}, {name: "Lea", money: 500000}, ]; let salary = 1000; staff.forEach( (employee) => { employee.money += salary; } );
In Rust, we would write it like this:
let salary = 1000; staff.iter_mut().for_each( |employee| { employee.money += salary; } );
Admittedly, it takes time to get used to this syntax, with the pipe (|
) replacing the parentheses.
But after overcoming the initial awkwardness, I find it clearer to read than another set of parentheses.
As another example, here’s an object destructuring in JavaScript:
let point = { x: 5, y: 10 }; let {x,y} = point;
Similarly in Rust:
let point = Point { x: 5, y: 10 }; let Point { x, y } = point;
The main difference is that in Rust we have to specify the type (Point
). More generally, Rust needs to know all types at compile time. But in contrast to most other compiled languages, the compiler infers types on its own whenever possible.
To explain this a bit further, here’s code that is valid in C++ and many other languages. Every variable needs an explicit type declaration:
int a = 5; float b = 0.5; float c = 1.5 * a;
In JavaScript, as well as in Rust, this code is valid:
let a = 5; let b = 0.5; let c = 1.5 * a;
The list of shared features goes on and on:
- Rust has the
async
+await
syntax. - Arrays can be created as easily as
let array = [1,2,3]
. - Code is organized in modules with explicit imports and exports.
- String literals are encoded in Unicode, handling special characters without issues.
I could go on with the list, but I think my point is clear by now: Rust has a rich set of features that are also used in modern JavaScript.
Differences
Rust is a compiled language, meaning that there’s no runtime that executes Rust code. An application can only run after the compiler (rustc
) has done its magic. The benefit of this approach is usually better performance.
Luckily, Cargo takes care of invoking the compiler for us. And with webpack, we’ll be able to also hide cargo
behind npm run build
. With this guide, the normal workflow of a web developer can be retained, once Rust is set up for the project.
Rust is a strongly typed language, which means all types must match at compile time. For example, you can’t call a function with parameters of the wrong type or the wrong number of parameters. The compiler will catch the error for you before you run into it at runtime. The obvious comparison is TypeScript. If you like TypeScript, then you’re likely to love Rust.
But don’t worry: if you don’t like TypeScript, Rust might still be for you. Rust has been built from the ground up in recent years, taking into account everything humanity has learned about programming-language design in the past few decades. The result is a refreshingly clean language.
Pattern matching in Rust is a pet feature of mine. Other languages have switch
and case
to avoid long chains like this:
if ( x == 1) { // ... } else if ( x == 2 ) { // ... } else if ( x == 3 || x == 4 ) { // ... } // ...
Rust uses the more elegant match
that works like this:
match x { 1 => { /* Do something if x == 1 */}, 2 => { /* Do something if x == 2 */}, 3 | 4 => { /* Do something if x == 3 || x == 4 */}, 5...10 => { /* Do something if x >= 5 && x <= 10 */}, _ => { /* Catch all other cases */ } }
I think that’s pretty neat, and I hope JavaScript developers can also appreciate this syntax extension.
Unfortunately, we also have to talk about the dark side of Rust. To say it straight, using a strict type system can feel very cumbersome at times. If you thought the type systems of C++ or Java are strict, then brace yourself for a rough journey with Rust.
Personally, I love that part about Rust. I rely on the strictness of the type system and can thus turn off a part of my brain — a part that tingles violently every time I find myself writing JavaScript. But I understand that for beginners, it can be very annoying to fight the compiler all the time. We’ll see some of that later in this Rust tutorial.
Hello Rust
Now, let’s get a hello world
with Rust running in the browser. We start by making sure all the necessary tools are installed.
Tools
- Install Cargo + rustc using rustup. Rustup is the recommended way to install Rust. It will install the compiler (rustc) and the package manager (Cargo) for the newest stable version of Rust. It can also manage beta and nightly versions, but that won’t be necessary for this example.
- Check the installation by typing
cargo --version
in a terminal. You should see something likecargo 1.48.0 (65cbdd2dc 2020-10-14)
. - Also check Rustup:
rustup --version
should yieldrustup 1.23.0 (00924c9ba 2020-11-27)
.
- Check the installation by typing
- Install wasm-pack. This is to integrate the compiler with npm.
- Check the installation by typing
wasm-pack --version
, which should give you something likewasm-pack 0.9.1
.
- Check the installation by typing
- We also need Node and npm. We have a full article that explains the best way to install these two.
Writing the Rust code
Now that everything’s installed, let’s create the project. The final code is also available in this GitHub repository. We start with a Rust project that can be compiled into an npm package. The JavaScript code that imports that package will come afterward.
To create a Rust project called hello-world
, use cargo init --lib hello-world
. This creates a new directory and generates all files required for a Rust library:
├──hello-world ├── Cargo.toml ├── src ├── lib.rs
The Rust code will go inside lib.rs
. Before that, we have to adjust Cargo.toml
. It defines dependencies and other package information using TOML. For a hello world in the browser, add the following lines somewhere in your Cargo.toml
(for example, at the end of the file):
[lib] crate-type = ["cdylib"]
This tells the compiler to create a library in C-compatibility mode. We’re obviously not using C in our example. C-compatible just means not Rust-specific, which is what we need to use the library from JavaScript.
We also need two external libraries. Add them as separate lines in the dependencies section:
[dependencies] wasm-bindgen = "0.2.68" web-sys = {version = "0.3.45", features = ["console"]}
These are dependencies from crates.io, the default package repository that Cargo uses.
wasm-bindgen is necessary to create an entry point that we can later call from JavaScript. (You can find the full documentation here.) The value "0.2.68"
specifies the version.
web-sys contains Rust bindings to all Web APIs. It will give us access to the browser console. Note that we have to select the console feature explicitly. Our final binary will only contain the Web API bindings selected like this.
Next is the actual code, inside lib.rs
. The auto-generated unit test can be deleted. Just replace the content of the file with this code:
use wasm_bindgen::prelude::*; use web_sys::console; #[wasm_bindgen] pub fn hello_world() { console::log_1("Hello world"); }
The use
statements at the top are for importing items from other modules. (This is similar to import
in JavaScript.)
pub fn hello_world() { ... }
declares a function. The pub
modifier is short for “public” and acts like export
in JavaScript. The annotation #[wasm_bindgen]
is specific to Rust compilation to WebAssembly (Wasm). We need it here to ensure the compiler exposes a wrapper function to JavaScript.
In the body of the function, “Hello world” is printed to the console. console::log_1()
in Rust is a wrapper for a call to console.log()
. (Read more here.)
Have you noticed the _1
suffix at the function call? This is because JavaScript allows a variable number of parameters, while Rust doesn’t. To get around that, wasm_bindgen
generates one function for each number of parameters. Yes, that can get ugly quickly! But it works. A full list of functions that can be called on the console from within Rust is available in the web-sys documentation.
We should now have everything in place,. Try compiling it with the following command. This downloads all dependencies and compiles the project. It may take a while the first time:
cd hello-world wasm-pack build
Huh! The Rust compiler isn’t happy with us:
error[E0308]: mismatched types --> srclib.rs:6:20 | 6 | console::log_1("Hello world"); | ^^^^^^^^^^^^^ expected struct `JsValue`, found `str` | = note: expected reference `&JsValue` found reference `&'static str
Note: if you see a different error (error: linking with cc failed: exit code: 1
) and you’re on Linux, you lack cross-compilation dependencies. sudo apt install gcc-multilib
should resolve this.
As I mentioned earlier, the compiler is strict. When it expects a reference to a JsValue
as an argument to a function, it won’t accept a static string. An explicit conversion is necessary to satisfy the compiler.
console::log_1(&"Hello world".into());
The method into() will convert one value to another. The Rust compiler is smart enough to defer which types are involved in the conversion, since the function signature leaves only one possibility. In this case, it will convert to JsValue
, which is a wrapper type for a value managed by JavaScript. Then, we also have to add the &
to pass it by reference rather than by value, or the compiler will complain again.
Try running wasm-pack build
again. If everything goes well, the last line printed should look like this:
[INFO]: :-) Your wasm pkg is ready to publish at /home/username/intro-to-rust/hello-world/pkg.
If you managed to get this far, you’re now able to compile Rust manually. Next, we’ll integrate this with npm and webpack, which will do this for us automatically.
Continue reading Rust Tutorial: An Introduction to Rust for JavaScript Devs on SitePoint.