Getting Fancy with Rust and WebAssembly (Part 1) - Quick Start

Jun 13, 2023

I. Introduction

WebAssembly is a new type of code that can be run in modern web browsers - it is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages such as C/C++ with a compilation target so that they can run on the web. It is also designed to run alongside JavaScript, allowing both to work together.

I previously wrote an article about how to develop WebAssembly using Golang - WebAssembly: An Essential Skill for Future Front-end Development.

Both Rust and Go can be used to develop WebAssembly, but they have their own advantages and disadvantages.

Rust's advantages:

  • Faster performance and smaller binary files
  • Better memory safety

Go's advantages:

  • Easier to get started and learn
  • Better ecosystem and community support

Overall, if you prioritize performance and memory safety, then Rust might be the better choice. If you value development efficiency and ease of use, then Go might be more suitable for you. Of course, the actual choice should be based on specific project requirements and team circumstances.

Due to some work requirements, I've recently been working on some Rust projects, and I'd like to systematically document it here. I hope this can be helpful when you encounter scenarios that prioritize performance and memory safety.

II. Environment

  • Rust 1.70.0
  • wasm-bindgen 0.2.87

III. Create Project and Add Dependencies

This assumes Rust is already installed. If you need to install it, please refer to the official website.

Use Cargo to create a project named hello-wasm:

cargo new --lib hello-wasm

Enter the project, open the Cargo.toml file, and add dependencies:

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2.87"

IV. Update lib.rs

In the default project creation, there's a file named lib.rs. Replace all its content with:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

V. Compile

At this point, we've created the simplest function - one that returns the sum of two integers.

Now we can proceed with compilation. Before compiling, we need to install a tool called wasm-pack:

cargo install wasm-pack

Then compile:

wasm-pack build --target web

After compilation, a pkg folder will be created with the following content:

pkg
├── hello_wasm.d.ts
├── hello_wasm.js
├── hello_wasm_bg.wasm
├── hello_wasm_bg.wasm.d.ts
└── package.json

Although there are many files, we can see the wasm file we need, and based on Go's wasm import method, we might need to use the js file here.

VI. Frontend Integration

To quickly verify, let's create an index.html file directly in the hello-wasm project for frontend integration.

1. Create index.html

First, create an index.html file:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scal=1.0">
    <title>Getting Fancy with Rust and WebAssembly</title>
</head>

<body>
    Hello, World!
</body>

</html>

Yes, that's right! This is a standard opening! 😼

2. Import WASM

Unlike Go's wasm import method, Rust prefers to directly import the js file rather than having developers manually import the wasm file.

Here we use js import:

<script type="module">
    import init, { add } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();
        const result = add(1, 2);
        console.log(`the result from rust is: ${result}`);
    }

    run();
</script>

3. Complete Code

The complete HTML code is as follows:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scal=1.0">
    <title>Getting Fancy with Rust and WebAssembly</title>
</head>

<body>
    Hello, World!
</body>

<script type="module">
    import init, { add } from './pkg/hello_wasm.js';

    const run = async () => {
        await init();
        const result = add(1, 2);
        console.log(`the result from rust is: ${result}`);
    }

    run();
</script>

</html>

VII. Verification

Here we can quickly start an http server. I choose to use http-server, but you can also use methods like python3 -m http.server, depending on your personal preferences.

So, start the http server:

http-server

Open your browser, visit http://localhost:8080, open the debugger, and you should see the output the result from rust is: 3, which means we've taken the first step in doing some fancy stuff!

rust_wasm

VIII. Common Issues

1. Frontend Reports Response Type Error

The detailed error is as follows:

Failed to load module script: The server responded with a non-JavaScript MIME type of "application/wasm". Strict MIME type checking is enforced for module scripts per HTML spec.

You might encounter this error when importing the js file generated by WebAssembly. At first glance, it seems to be an http server response issue, and when searching, you might find posts saying it's a response problem.

In fact, when following this article step by step, you won't encounter this problem because the compilation parameter in this article directly solves this issue. When I was figuring this out on my own, solving this problem really drove me crazy...

The key lies in the --target parameter of the compilation command.

When this parameter is not set, the default is actually --target bundler, which compiles for use with scaffolds like webpack. Therefore, using --target web here compiles it for direct use in the web.

The relevant parameters are as follows:

  • bundler: Compile for use with scaffolds like webpack
  • web: Compile for direct use in web
  • nodejs: Compile into a node module that can be loaded via require
  • deno: Compile into a deno module that can be loaded via import
  • no-modules: Similar to web, but older and cannot use ES modules

2. Directly Importing the WASM File

If you try to directly import the wasm file instead of using the method described in this article, you'll find that it works too!

<script type="module">
    WebAssembly.instantiateStreaming(fetch("./pkg/hello_wasm_bg.wasm"), {}).then(
        (obj) => console.log('the result from rust is: ', obj.instance.exports.add(1, 2))
    );
</script>

Yes, that's right, it works for now, but when you introduce other things like DOM manipulation, it starts to break...

3. Updated WASM Import Method

Regarding the previous issue, let's not discuss whether we can directly import the wasm file. Here, I'll just mention the instantiateStreaming method. This is an updated method that doesn't require conversion to arrayBuffer, which I discovered while exploring Rust projects. If you're importing wasm in other languages, please use this updated method.