Using Rust in TypeScript Projects via WASM
September 26th, 2024

Rust is becoming more and more popular. Its unique combination of performance, safety, and ease of use has attracted developers from various fields, from low-level system design to web development. With the rise of WebAssembly (WASM), Rust has also become a powerful choice for projects that need to bridge the gap between high-performance code and web technologies.

When I was trying to create benchmarks for Incremental Merkle Trees (IMT) comparing the Rust implementation with one of TypeScript, I encountered the problem of converting Rust code into WASM, and then using it inside the TS project for comparison. I had to compare them running on a server and on a browser, so I had to create two projects.

The challenges I encountered were not easy, and I couldn't find explanations of many things online. This tutorial aims to provide all the material I wish I had when I started this project, so anyone who’s willing to do something similar can learn from it. It also helps me organize all the research in one place for future reference.

Why Rust?

Rust offers low-level control over memory and code execution, resulting in excellent performance.

It’s also a popular programming language for implementations related to cryptography.

What’s Web Assembly?

WebAssembly(WASM) is a binary instruction format for a stack-based virtual machine. It enables cross-platform deployment for client and server applications.

Its advantages include efficiency, speed, and safety, as it executes in a memory-safe environment.

From Rust to WASM, to TS project

WASM is a good compilation target for Rust because it allows code to run on both browsers and servers.

Some believe data structure or protocol implementations are faster when compiled from Rust to WASM, compared to a direct JS implementation (one thing to note is the garbage collection of JavaScript, which can slow down the execution).

For this project, I compared two implementations of the Incremental Merkle Tree data structure: one developed in TS and the other in Rust. The project can be found here.

Preparation and Setup

The first thing I did was convert the Rust code into WASM using wasm-bindgen (find out more about wasm_bindgen here).

The implementation I used had a parameterized hashing function for the data structure, allowing the user to choose which one to use. I had to hardcode one specific hashing function to allow wasm_bindgen to pack everything up and convert it to WASM. This way, the data structure contained the nodes, zeroes, depth and arity, but excluded the hashing function. Every time the function imt.hash was used, I replaced it with hash_function, defined in the hash.rs file.

Another adjustment involved converting the error messages from Err(“…“) to Err(JsValue::from_str(“…“)). JsValue is a wrapper around JavaScript values and handles errors in JS/WASM interoperability. (More about JsValue here).

The last thing I did was annotate the functions to be exposed to JS with #[wasm_bindgen]. This way, the WASM output includes the necessary bindings to be used in JS/TS. Some more information about this can be found here.

We then use the wasm-pack tool to generate the WASM files. The --target argument will very depending on where we want our code to run, which we’ll cover in the next sections. More about --target here.

Server

To use the rust implementation in a Node.js server, the following command needs to be executed:

wasm-pack build --target nodejs

This will generate a pkg folder with all the necessary files.

In the TS file, import the package as follows:

import * as wasm from "path-to-rs-project/pkg/imt_poseidon_rs"

Then, directly use the functions, like:

const imt_wasm = new wasm.IMT(...);
imt_wasm.insert("2");

As you can see, it’s incredible easy to use.

Browser

To compile Rust for the browser, run:

wasm-pack build --target web

This will also generate a pkg folder. I copied this folder into an assets/wasm/ folder in my React project.

According to the documentation, this command outputs JS that can be natively imported as an ES module in a browser, but the WebAssembly must be manually instantiated and loaded.

So, I created a loader function:

export const loadWasm = async () => {
  const wasm = await import("../assets/wasm/pkg/imt_poseidon_rs")
  await wasm.default()
  return wasm
}

Next, import it to your file:

import { loadWasm } from "./wasm-loader"

Then load it using:

const wasm = await loadWasm()

Then the functions can be used the same way as shown in the Node.js server section.

Conclusion / future work

As you may realize now, it's really easy to use Rust code inside a TypeScript project. You can view the benchmark results on this website. Spoiler: most of the functions were faster using the WASM compiled from the Rust implementation than their TypeScript implementation.

This opens up exciting opportunities, particularly in cryptography. Many primitives are already implemented in Rust, well-tested, and highly efficient. For example, arkworks, provides several cryptographic primitives. With this tutorial, I hope you can integrate them to your TypeScript project, tor just Rust ones.

Thanks for following along with this tutorial! I hope it helps you explore the potential of combining Rust and TypeScript through WebAssembly. If you have any questions or suggestions, feel free to reach out.

Subscribe to Sebastian
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from Sebastian

Skeleton

Skeleton

Skeleton