Consider targeting wasm32v1-none
You are targeting the wrong kind of wasm
When building WebAssembly (wasm) projects, especially with rust, most advice is to build to wasm32-unknown-unknown.
This is generally bad advice.
With relatively simple changes you can use the compiler to your advantage rather than letting it lie to you.
| This is part 2 of my rust-wasm journey, see part 1 here |
The problem
The issue is that running something like:
cargo build --release --target wasm32-unknown-unknown
… ends up having the compiler shift many kinds of error to runtime instead of compile time.
One of the biggest benefits to using a language like rust is that you can make illegal states unrepresentable.[1] This allows you to have the compiler stop you from making errors quickly and early in the design process. So when you want to use rust to build a wasm library for the web, you shouldn’t have to give up on all the benefits of "compiler-driven-development".
This is compounded by the fact that testing dynamic libraries (wasm is by definition a dynamic library) is not straightforward at all. So you may be fairly deep into coding or design and not realize you are trying to use something that will almost never work on the web.
You will be better served by instead targeting wasm32v1-none for the web.
More on how to do that later.
Small note: wasm for the web vs others
Most wasm is built for the web, and that’s what this blog post is discussing.
If you are building a wasm library as a plugin, or something to run in a "web worker" type setup, then you are probably aren’t targeting the wasm32-unknown-unknown target anyway.
You are probably building for the wasm32-wasip2 or similar target, and swapping to one of those targets gets you largely the same benefit of targeting wasm32v1-none.
Shooting yourself in the foot
The problem is that wasm32-unknown-unknown includes the std library, but most of the std library is unusable in a wasm context.
There’s no filesystem, no I/O, no threads, etc.
This is (sort of) the point: the wasm sandbox can’t get into any trouble so from a consumer perspective, they can download and run wasm code without having to worry about it installing a rootkit or malware or something.
A somewhat fair reason for this situation’s existence is because wasm32-unknown-unknown is meant to be easy, and does not only target the web.
The big things missing
std in rust contains a fair amount of things that won’t work on the web.
These are:
-
I/O:
-
std::fs- there’s no filesystem -
std::net- network calls are not permitted
-
-
Concurrency:
-
std::thread(or any non-stdequivalent liketokio::task) - mainstream wasm is only single-threaded for now [2] -
std::sync::Mutex(they sort of work, but not correctly)
-
-
Time
-
std::time::Instant- there’s no clock available and besides, even timing in JavaScript (js) has all sorts of limitations -
SystemTime- you have to access time via js
-
-
Operating system
-
std::env- environment variables don’t exist in a wasm context -
std::process- there’s no shell (likebash) available
-
There are others but they are not easy to discover and the wasm32-unknown-unknown target gets active maintenance anyway, but these big gaps are unlikely to change anytime soon.
Can’t you just… not use those calls?
Just don’t write code using any of these prohibited actions, you might think. For simple wasm libraries this is actually quite feasible. But once you start pulling in libraries, it can become very difficult to tell if a transitive dependency is going to try to do a network call or something else that won’t work before you ship your code.
Example
I have created a demonstration project that compiles but will absolutely not work on the web: https://codeberg.org/humble_proton/blog-demos/src/branch/main/targeting_wasm32v1_none/stub_example
For clarity here is a snippet of the wasm code, which compiles (and even passes unit tests):
use std::time::{SystemTime, UNIX_EPOCH};
use wasm_bindgen::prelude::wasm_bindgen;
#[wasm_bindgen]
pub fn epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards!")
.as_secs()
}
After it is loaded into a webpage, if you try to call this function you end up with an error message like: "unreachable executed" in firefox.[3]
>> wasm.epoch_secs()
Uncaught RuntimeError: unreachable executed
epoch_secs http://[::1]:8000/target/pkg/stub_example.js:11
<anonymous> debugger eval code:1
stub_example_bg.wasm:38494:1
This is not a fun way to be writing code!
Solutions
Option 1: Use wasm32v1-none
There is a straightforward solution: compile for the wasm32v1-none target instead.
Unfortunately, straightforward does not mean simple - this is not a drop in fix if you already have a large project.
The good news is that wasm32v1-none (in rust at least) still uses wasm32-unknown-unknown in the LLVM backend, so all other tools, browsers, etc still work exactly the same.[4]
The main benefit is that wasm32v1-none does not include the std library.
It only includes core and alloc.
Furthermore it only encompasses the W3C WebAssembly Core 1.0 spec with one W3C proposal: Mutable Global variable import & export.
This means that this target is highly compatible across browsers.
In short, if you can compile to this target, this wasm will run on the web!
As a bonus, you can still use any of the W3C proposals that LLVM supports like reference types, multi-value, fixed width SIMD, etc - these are just a short Cargo.toml config away.
Compiling the stub_example from with wasm32v1-none correctly fails:
$ cargo build --release --target wasm32v1-none
Compiling wasm-bindgen-shared v0.2.114
Compiling unicode-ident v1.0.24
Compiling cfg-if v1.0.4
Compiling once_cell v1.21.4
Compiling wasm-bindgen v0.2.114
error[E0463]: can't find crate for `std`
--> /Users/humble_proton/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/wasm-bindgen-0.2.114/src/lib.rs:53:1
|
53 | extern crate std;
| ^^^^^^^^^^^^^^^^^ can't find crate
|
= note: the `wasm32v1-none` target may not support the standard library
Indeed it does not support std!
How to exclude std
If you are unfamiliar with no_std build in rust, you just need to do a few steps (that are mostly copy and paste):
First you should exclude the std feature of wasm-bindgen (and any other crates) in your Cargo.toml:
[dependencies]
wasm-bindgen = { version = "0.2.114", default-features = false } (1)
| 1 | This should match the version of wasm-bindgen installed on your workstation. |
Then you need to do just a bit of ceremony (this is less scary than it looks) to tell the compiler you want to use the alloc crate (which usually isn’t available in #[no_std] environments, but is here).
Then specify a heap allocator[5] and specify a panic handler.
#![no_std]
// Link the allocator and import the heap types: Vec, Rc, String, etc.
extern crate alloc;
use alloc::format;
use alloc::string::String;
use wasm_bindgen::prelude::wasm_bindgen;
// You can use DLmalloc just like `std` does, but we can do better (1)
// https://github.com/SFBdragon/talc/blob/master/talc/README_WASM.md
#[cfg(all(not(target_feature = "atomics"), target_family = "wasm"))]
#[global_allocator]
static TALC: talc::wasm::WasmDynamicTalc = talc::wasm::new_wasm_dynamic_allocator();
#[cfg(target_family = "wasm")]
#[panic_handler]
// A panic results in a trap on wasm anyway so we can cut out the panic handler
// with wasm-bindgen later. We'll keep the standard panic handler on non-wasm
// builds (like `cargo test`) (2)
fn panic(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
#[wasm_bindgen]
pub fn join_strings(a: &str, b: &str) -> String {
format!("{a}{b}")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alloc::string::ToString;
// Tests run on native arch; the logic is target-agnostic since we only use
// `alloc`
#[test]
fn duplicate_string() {
let input = "abc";
let result = join_strings(input, input);
assert_eq!(result, "abcabc".to_string());
}
}
| 1 | See: talc’s wasm readme |
| 2 | Copying directly from the rustc book: "[wasm32v1-none] is compiled with -Cpanic=abort by default. Using -Cpanic=unwind would require using the WebAssembly exception-handling proposal stabilized mid-2025, and if that’s desired then you most likely don’t want to use this target and instead want to use wasm32-unknown-unknown instead." |
Additional benefits
By excluding std you end up with a few nice side-benefits.
For one, your WASM blobs are smaller and probably more effecient because you are doing fewer and less allocations and when you do allocate you are using a better allocator.
Another benefit is that #[no_std] crates tend to be smaller and "do less" than the bigger crates.
This leads us to the downsides though:
Pitfalls
Unfortunately, this approach may not be feasible for some projects.
For crates to support #[no_std], they usually have to be built with that in mind from the beginning and many if not most "batteries included" type crates assume you in a std library environment, making compiling them without std close to impossible.
It is much easier start a new WASM project with this approach than it is to migrate a WASM project to a #[no_std] style dynamic library.
Option 2: Better testing
Improved testing is always an improvement to the overall system. There’s no such thing as a test you get for free, and tests are an overhead on development - they require maintenance and they only help improve the system, not actually fix anything. Of course catching bugs early is much more effective than catching them in production (or even worse, having a customer report it to you because you didn’t catch it).
Testing is only as good as your coverage[6] and the logic/assumptions it validates.
The compiler can catch bugs as you write them, whereas tests only catch bugs after you’ve written them (unless you are a strict Test-Driven-Development developer).
However, we do have a way to test the existing WASM dynamic library in situ, with a wasm executor which is a huge improvement to the alternative of doing unit tests with your computer’s architecture (i.e. when you run cargo test).
The only real method for this option is the admittedly experimental wasm-bindgen-test.
Their documentation on usage is pretty good, so I’ll only briefly summarize what we do.
Running wasm-bindgen-test
A complete example can be found at my codeberg blog-demos repo.
First, add a "rlib" type of library build.
That is in your Cargo.toml:
[lib]
crate-type = ["cdylib", "rlib"]
This will allow you to run integration tests.
Secondly, add the wasm-bindgen-test crate to your project:
cargo add --dev wasm-bindgen-test
Thirdly, write your integration tests in tests/wasm.rs.
For example:
use wasm_bindgen_test::*;
// Run tests in a browser (remove this to run in Node.js/headless)
wasm_bindgen_test_configure!(run_in_browser);
use stub_example::time::measure_delay_ms;
#[wasm_bindgen_test]
pub fn check_delay_test() {
# measure_delay_ms() uses time which isn't available on wasm
assert!(measure_delay_ms() > 1);
}
Then you can run the tests:
# check for logic bugs first, running your other tests:
cargo test --release
# check for wasm errors:
wasm-pack test --firefox # or safari, or chrome
Open a browser to http://127.0.0.1:8000 and you’ll see output like:
Loading Wasm module...
running 2 tests
test check_delay_test ... FAIL
failures:
---- check_delay_test output ----
error output:
panicked at /rustc/4a4ef493e3a1488c6e321570238084b38948f6db/library/std/src/sys/pal/wasm/../unsupported/time.rs:13:9:
time not implemented on this platform
Stack:
__wbg_get_imports/__wbg_new_d6846beabaecc372/<@http://127.0.0.1:8000/wasm-bindgen-test:375:25
logError@http://127.0.0.1:8000/wasm-bindgen-test:705:18
__wbg_new_d6846beabaecc372@http://127.0.0.1:8000/wasm-bindgen-test:374:57
The important line is: time not implemented on this platform
You can also run in --headless mode which doesn’t require a manual step of refreshing the browser page, but headless mode doesn’t capture output when the code panicks (or at least I couldn’t figure out how to get it to work).
If there’s a logic bug that only exists in the wasm version, the output in headless mode is probably enough - you can always try headless mode first.
|
Additional benefits
You can use all the crates you want, even if they have functions that would execute unsupported system calls. As long as your code doesn’t launch those functions, you can run in the browser without problems!
Pitfalls
Larger wasm bundles.
Safety is reliant on you writing good tests - which is not as great a strategy when compared to compiler-enforced checks. We could accomplish largely the same thing in TypeScript or JavaScript and avoid this whole wasm mess!
Conclusion
By default, you should start rust wasm projects with wasm32v1-none instead of wasm32-unknown-unknown - this trims the size of your wasm bundle and gives you better safety guarantees.
If you are working with existing projects (especially ones with lots of imported crates) then I would recommend integrating wasm-pack testing into your workflow, this isn’t as seamless and integrated as targeting wasm32v1-none but you can still check that you aren’t calling non-existing functions if you get good test coverage.
If you know about other languages compiling to wasm like C or GoLang, you may want to consider wasm32v1-none for much of the same reasons.