9 Guidelines for Working Rust on WASM WASI | by Carl M. Kadie | Sep, 2024

Based mostly on my expertise with range-set-blaze, an information construction undertaking, listed here are the selections I like to recommend, described one after the other. To keep away from wishy-washiness, I’ll specific them as guidelines.

In 2019, Docker co-creator Solomon Hykes tweeted:

If WASM+WASI existed in 2008, we wouldn’t have wanted to created Docker. That’s how vital it’s. Webassembly on the server is the way forward for computing. A standardized system interface was the lacking hyperlink. Let’s hope WASI is as much as the duty.

At the moment, for those who comply with know-how information, you’ll see optimistic headlines like these:

A collage of headlines about WebAssembly System Interface (WASI). The first headline from Forbes reads “WebAssembly Is Finally Usable, Almost.” A YouTube video thumbnail shows “WASI Will Change .NET Forever! Run WebAssembly Outside The Browser!” with James Montemagno. The bottom headline says “Unexpectedly Useful: A Real World Use Case For WebAssembly System Interface (WASI).”

If WASM WASI have been actually prepared and helpful, everybody would already be utilizing it. The truth that we maintain seeing these headlines suggests it’s not but prepared. In different phrases, they wouldn’t have to maintain insisting that WASM WASI is prepared if it actually have been.

As of WASI Preview 1, right here is how issues stand: You’ll be able to entry some file operations, setting variables, and have entry to time and random quantity technology. Nevertheless, there isn’t a help for networking.

WASM WASI may be helpful for sure AWS Lambda-style internet providers, however even that’s unsure. As a result of wouldn’t you like to compile your Rust code natively and run twice as quick at half the fee in comparison with WASM WASI?

Perhaps WASM WASI is helpful for plug ins and extensions. In genomics, I’ve a Rust extension for Python, which I compile for 25 completely different combos (5 variations of Python throughout 5 OS targets). Even with that, I don’t cowl each doable OS and chip household. Might I substitute these OS targets with WASM WASI? No, it could be too gradual. Might I add WASM WASI as a sixth “catch-all” goal? Perhaps, but when I actually need portability, I’m already required to help Python and may simply use Python.

So, what’s WASM WASI good for? Proper now, its major worth lies in being a step towards working code within the browser or on embedded methods.

In Rule 1, I discussed “OS targets” in passing. Let’s look deeper into Rust targets — important data not only for WASM WASI, but in addition for normal Rust growth.

On my Home windows machine, I can compile a Rust undertaking to run on Linux or macOS. Equally, from a Linux machine, I can compile a Rust undertaking to focus on Home windows or macOS. Listed here are the instructions I exploit so as to add and examine the Linux goal to a Home windows machine:

rustup goal add x86_64-unknown-linux-gnu
cargo examine --target x86_64-unknown-linux-gnu

Apart: Whereas cargo examine verifies that the code compiles, constructing a totally purposeful executable requires further instruments. To cross-compile from Home windows to Linux (GNU), you’ll additionally want to put in the Linux GNU C/C++ compiler and the corresponding toolchain. That may be tough. Happily, for the WASM targets we care about, the required toolchain is straightforward to put in.

To see all of the targets that Rust helps, use the command:

rustc --print target-list

It would checklist over 200 targets together with x86_64-unknown-linux-gnu, wasm32-wasip1, and wasm32-unknown-unknown.

Goal names include as much as 4 components: CPU household, vendor, OS, and setting (for instance, GNU vs LVMM):

A diagram explaining the components of the target triple x86_64-unknown-linux-gnu. It breaks down as follows: CPU architecture (64-bit x86), Vendor (unspecified, hence ‘unknown’), Operating system (Linux), and Environment (GNU C library).
Goal Identify components — determine from writer

Now that we perceive one thing of targets, let’s go forward and set up the one we want for WASM WASI.

To run our Rust code on WASM exterior of a browser, we have to goal wasm32-wasip1 (32-bit WebAssembly with WASI Preview 1). We’ll additionally set up WASMTIME, a runtime that permits us to run WebAssembly modules exterior of the browser, utilizing WASI.

rustup goal add wasm32-wasip1
cargo set up wasmtime-cli

To check our setup, let’s create a brand new “Whats up, WebAssembly!” Rust undertaking utilizing cargo new. This initializes a brand new Rust bundle:

cargo new hello_wasi
cd hello_wasi

Edit src/major.rs to learn:

fn major() {
#[cfg(not(target_arch = "wasm32"))]
println!("Whats up, world!");
#[cfg(target_arch = "wasm32")]
println!("Whats up, WebAssembly!");
}

Apart: We’ll look deeper into the #[cfg(...)] attribute, which permits conditional compilation, in Rule 4.

Now, run the undertaking with cargo run, and it is best to see Whats up, world! printed to the console.

Subsequent, create a .cargo/config.toml file, which specifies how Rust ought to run and take a look at the undertaking when concentrating on WASM WASI.

[target.wasm32-wasip1]
runner = "wasmtime run --dir ."

Apart: This .cargo/config.toml file is completely different from the principle Cargo.toml file, which defines your undertaking’s dependencies and metadata.

Now, for those who say:

cargo run --target wasm32-wasip1

It’s best to see Whats up, WebAssembly!. Congratulations! You’ve simply efficiently run some Rust code within the container-like WASM WASI setting.

Now, let’s examine #[cfg(...)]—an important software for conditionally compiling code in Rust. In Rule 3, we noticed:

fn major() {
#[cfg(not(target_arch = "wasm32"))]
println!("Whats up, world!");
#[cfg(target_arch = "wasm32")]
println!("Whats up, WebAssembly!");
}

The #[cfg(...)] traces inform the Rust compiler to incorporate or exclude sure code gadgets based mostly on particular circumstances. A “code merchandise” refers to a unit of code reminiscent of a perform, assertion, or expression.

With #[cfg(…)] traces, you possibly can conditionally compile your code. In different phrases, you possibly can create completely different variations of your code for various conditions. For instance, when compiling for the wasm32 goal, the compiler ignores the #[cfg(not(target_arch = "wasm32"))] block and solely consists of the next:

fn major() {
println!("Whats up, WebAssembly!");
}

You specify circumstances through expressions, for instance, target_arch = "wasm32". Supported keys embody target_os and target_arch. See the Rust Reference for the total checklist of supported keys. You too can create expressions with Cargo options, which we’ll study in Rule 6.

It’s possible you’ll mix expressions with the logical operators not, any, and all. Rust’s conditional compilation doesn’t use conventional if...then...else statements. As an alternative, you have to use #[cfg(...)] and its negation to deal with completely different instances:

#[cfg(not(target_arch = "wasm32"))]
...
#[cfg(target_arch = "wasm32")]
...

To conditionally compile a whole file, place #![cfg(...)] on the high of the file. (Discover the “!”). That is helpful when a file is simply related for a particular goal or configuration.

You too can use cfg expressions in Cargo.toml to conditionally embody dependencies. This lets you tailor dependencies to completely different targets. For instance, this says “depend upon Criterion with Rayon when not concentrating on wasm32”.

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { model = "0.5.1", options = ["rayon"] }

Apart: For extra data on utilizing cfg expressions in Cargo.toml, see my article: 9 Rust Cargo.toml Wats and Wat Nots: Grasp Cargo.toml formatting guidelines and keep away from frustration | In the direction of Knowledge Science (medium.com).

It’s time to attempt to run your undertaking on WASM WASI. As described in Rule 3, create a .cargo/config.toml file in your undertaking. It tells Cargo the best way to run and take a look at your undertaking on WASM WASI.

[target.wasm32-wasip1]
runner = "wasmtime run --dir ."

Subsequent, your undertaking — like all good code — ought to already include checks. My range-set-blaze undertaking consists of, for instance, this take a look at:

#[test]
fn insert_255u8() {
let range_set_blaze = RangeSetBlaze::<u8>::from_iter([255]);
assert!(range_set_blaze.to_string() == "255..=255");
}

Let’s now try to run your undertaking’s checks on WASM WASI. Use the next command:

cargo take a look at --target wasm32-wasip1

If this works, it’s possible you’ll be completed — however it in all probability gained’t work. Once I do that on range-set-blaze, I get this error message that complains about utilizing Rayon on WASM.

 error: Rayon can't be used when concentrating on wasi32. Attempt disabling default options.
--> C:Userscarlk.cargoregistrysrcindex.crates.io-6f17d22bba15001fcriterion-0.5.1srclib.rs:31:1
|
31 | compile_error!("Rayon can't be used when concentrating on wasi32. Attempt disabling default options.");

To repair this error, we should first perceive Cargo options.

To resolve points just like the Rayon error in Rule 5, it’s vital to grasp how Cargo options work.

In Cargo.toml, an elective [features] part permits you to outline completely different configurations, or variations, of your undertaking relying on which options are enabled or disabled. For instance, here’s a simplified a part of the Cargo.toml file from the Criterion benchmarking undertaking:

[features]
default = ["rayon", "plotters", "cargo_bench_support"]
rayon = ["dep:rayon"]
plotters = ["dep:plotters"]
html_reports = []
cargo_bench_support = []

[dependencies]
#...
# Optionally available dependencies
rayon = { model = "1.3", elective = true }
plotters = { model = "^0.3.1", elective = true, default-features = false, options = [
"svg_backend",
"area_series",
"line_series",
] }

This defines 4 Cargo options: rayon, plotters, html_reports, and cargo_bench_support. Since every characteristic may be included or excluded, these 4 options create 16 doable configurations of the undertaking. Word additionally the particular default Cargo characteristic.

A Cargo characteristic can embody different Cargo options. Within the instance, the particular default Cargo characteristic consists of three different Cargo options — rayon, plotters, and cargo_bench_support.

A Cargo characteristic can embody a dependency. The rayon Cargo characteristic above consists of the rayon crate as a dependent bundle.

Furthermore, dependent packages might have their very own Cargo options. For instance, the plotters Cargo characteristic above consists of the plotters dependent bundle with the next Cargo options enabled: svg_backend, area_series, and line_series.

You’ll be able to specify which Cargo options to allow or disable when working cargo examine, cargo construct, cargo run, or cargo take a look at. For example, for those who’re engaged on the Criterion undertaking and need to examine solely the html_reports characteristic with none defaults, you possibly can run:

cargo examine --no-default-features --features html_reports

This command tells Cargo to not embody any Cargo options by default however to particularly allow the html_reports Cargo characteristic.

Inside your Rust code, you possibly can embody/exclude code gadgets based mostly on enabled Cargo options. The syntax makes use of #cfg(…), as per Rule 4:

#[cfg(feature = "html_reports")]
SOME_CODE_ITEM

With this understanding of Cargo options, we will now try to repair the Rayon error we encountered when working checks on WASM WASI.

Once we tried working cargo take a look at --target wasm32-wasip1, a part of the error message said: Criterion ... Rayon can't be used when concentrating on wasi32. Attempt disabling default options. This means we should always disable Criterion’s rayon Cargo characteristic when concentrating on WASM WASI.

To do that, we have to make two modifications in our Cargo.toml. First, we have to disable the rayon characteristic from Criterion within the [dev-dependencies] part. So, this beginning configuration:

[dev-dependencies]
criterion = { model = "0.5.1", options = ["html_reports"] }

turns into this, the place we explicitly flip off the default options for Criterion after which allow all of the Cargo options besides rayon.

[dev-dependencies]
criterion = { model = "0.5.1", options = [
"html_reports",
"plotters",
"cargo_bench_support"],
default-features = false }

Subsequent, to make sure rayon remains to be used for non-WASM targets, we add it again in with a conditional dependency in Cargo.toml as follows:

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
criterion = { model = "0.5.1", options = ["rayon"] }

Usually, when concentrating on WASM WASI, it’s possible you’ll want to change your dependencies and their Cargo options to make sure compatibility. Typically this course of is easy, however different occasions it may be difficult — and even unattainable, as we’ll talk about in Rule 8.

Apart: Within the subsequent article on this collection — about WASM within the Browser — we’ll go deeper into methods for fixing dependencies.

After working the checks once more, we transfer previous the earlier error, solely to come across a brand new one, which is progress!

#[test]
fn test_demo_i32_len() {
assert_eq!(demo_i32_len(i32::MIN..=i32::MAX), u32::MAX as usize + 1);
^^^^^^^^^^^^^^^^^^^^^ try to compute
`usize::MAX + 1_usize`, which might overflow
}

The compiler complains that u32::MAX as usize + 1 overflows. On 64-bit Home windows the expression doesn’t overflow as a result of usize is identical as u64 and might maintain u32::MAX as usize + 1. WASM, nonetheless, is a 32-bit setting so usize is identical as u32 and the expression is one too massive.

The repair right here is to switch usize with u64, guaranteeing that the expression doesn’t overflow. Extra typically, the compiler gained’t at all times catch these points, so it’s vital to assessment your use of usize and isize. In the event you’re referring to the dimensions or index of a Rust knowledge construction, usize is right. Nevertheless, for those who’re coping with values that exceed 32-bit limits, it is best to use u64 or i64.

Apart: In a 32-bit setting, a Rust array, Vec, BTreeSet, and so on., can solely maintain as much as 2³²−1=4,294,967,295 components.

So, we’ve fastened the dependency difficulty and addressed a usize overflow. However can we repair the whole lot? Sadly, the reply is not any.

WASM WASI Preview 1 (the present model) helps file entry (inside a specified listing), studying setting variables, and dealing with time and random numbers. Nevertheless, its capabilities are restricted in comparison with what you may anticipate from a full working system.

In case your undertaking requires entry to networking, asynchronous duties with Tokio, or multithreading with Rayon, Sadly, these options aren’t supported in Preview 1.

Happily, WASM WASI Preview 2 is predicted to enhance upon these limitations, providing extra options, together with higher help for networking and probably asynchronous duties.

So, your checks go on WASM WASI, and your undertaking runs efficiently. Are you completed? Not fairly. As a result of, as I wish to say:

If it’s not in CI, it doesn’t exist.

Steady integration (CI) is a system that may routinely run your checks each time you replace your code, guaranteeing that your code continues to work as anticipated. By including WASM WASI to your CI, you possibly can assure that future modifications gained’t break your undertaking’s compatibility with the WASM WASI goal.

In my case, my undertaking is hosted on GitHub, and I exploit GitHub Actions as my CI system. Right here’s the configuration I added to .github/workflows/ci.yml to check my undertaking on WASM WASI:

test_wasip1:
title: Check WASI P1
runs-on: ubuntu-latest
steps:
- title: Checkout
makes use of: actions/checkout@v4
- title: Arrange Rust
makes use of: dtolnay/rust-toolchain@grasp
with:
toolchain: secure
targets: wasm32-wasip1
- title: Set up Wasmtime
run: |
curl https://wasmtime.dev/set up.sh -sSf | bash
echo "${HOME}/.wasmtime/bin" >> $GITHUB_PATH
- title: Run WASI checks
run: cargo take a look at --verbose --target wasm32-wasip1

By integrating WASM WASI into CI, I can confidently add new code to my undertaking. CI will routinely take a look at that every one my code continues to help WASM WASI sooner or later.