no_std
PlaybookThis is a note summarizing my experience with making rust libraries no_std
compatible (especially for WASM supports).
I'm not a WASM expert and all feedback is welcomed!
You could also leave comments in the Rust forum post.
In a bare metal environment no code has been loaded before your program.
Without the software provided by an OS we can not load the standard library.
Instead the program, along with the crates it uses, can only use the hardware (bare metal) to run.
To prevent rust from loading the standard library useno_std
.
Some targets with arch like WASM or MIPS could be bare-metal environments[1], thus cross-compilation for them will render the best compatibility when your Rust code is no_std
compliant.
Read more on The Embedded Rust Book.
std
support inside WASM"Wait!", you say, "I remember the entirety of std
is supported in WASM according to the Rustc Book". Then on a second thought, you wonder how could std::fs
or std::os
be supported in such a sandboxed env?
"A lot of those things panic or they just return unimplemented error where possible (such as
Err(io::Error::Unimplemented)
)"
β Josh Stone (@cuviper) on Zulip thread
In short, while std
usages in your Rust code (or some of your dependencies) won't affect successful compilation into .wasm
modules, they might cause runtime errors. Therefore being no_std
compliant offers the "best effort" WASM support.
Put it another way, no_std
is a more stringent requirement than "wasm-compilable". Having your Rust libraries support no_std
is a sufficient but not strictly necessary condition for your code to run in WASM env.
In fact, the hardest part of your WASM work usually is not compiling into .wasm
module, but rather centered around exporting your Rust code and gluing them with JS using wasm-bingen
, and translate (usually conditionally compile) system resource calls into Web API calls offered by web-sys
, js-sys
etc.
Here are some related WASM knowledge that I find personally useful. If you only care about the "know-how", you can safely skip this section.
wasm[32|64]-unknown-unknown
, wasm[32|64]-emscripten
, wasm[32|64]-wasi
.
wasm32-unknown-unknown
doesn't specify a particular OS or runtime env, but is typically intended to run in env that provides necessary system interfaces, such as browsers.web-sys
for Web APIs). These APIs might access system resources indirectly, subjecting to the limitations of the host env.*-emscripten
is intended as a web target, but wasm-bingen
crate only supports wasm32-unknown-unknown
.wasm*-wasi
and runtime like wasmtime
or wasmer
.std -linking primitives |
Replacement for WASM |
---|---|
/dev/(u)random (system randomness) |
crate getrandom , with features = ["js"] for target wasm32-unknown-unknown |
rayon (data parallelism) |
crate wasm-bingen-rayon , some maybe-rayon options for feature-flag-toggled parallelism |
std::time::Instant |
crate instant |
std::collections::{HashMap, HashSet} |
crate hashbrown |
While there are many paths to no_std
compliance, the steps below should be mostly idiomatic to the Rust community.
std
, you could simply add #![no_std]
(read more on no_std attribute here), then job done and jump to the next step!lib.rs
:
Then configure Cargo.toml
as follows:
#![cfg_attr(not(feature = "std"), no_std)]
in the lib.rs
. The benefit of our approach above is "you never get the std
prelude items implicitly imported, so every use of std
in your crate will be explicit; this makes the two cfg scenarios closer to each other, so it's easier to understand what's happening and to debug."[2]Run:
If you just want WASM-compilable, you can replace the target with one of the six aforementioned WASM targets. But if you want "true no_std
" (i.e. neither your crate nor its dependencies links std
, so free of runtime panics in std
), then use a target that does not have std
at all, such as thumbv7em-none-eabi
.
To see the full list of target platforms, run rustc --print target-list
.
Adding checks to your CI (the following is specific to GitHub action only):
Alternatively, if you have multiple targets to test, you could use the strategy.matrix
feature like:
This is my approach for debugging compilation errors and I'd greatly appreciate suggestions on better approaches.
lib.rs
or main.rs
(faster compilation check later).[dependencies]
and then run cargo build --target wasm32-unknown-unknown --no-default-features
to locate the problematic dependencies or their feature flag selections.crate-a = { version = "x.x.x", default-features = false }
should suffice.[dev-dependencies]
also would cause compilation errors, so repeat Step 2 for those as well.lib.rs
, do minor cosmetic drop-in replacements or conditional compilation changes (as mentioned in the next section), then finally compile your Rust code to the target platform.If it's your first time trying to compile under no_std
mode, you will likely encounter some compilation errors.
Here are some steps you could take to debug.
If you have use std::*
, see if you could replace with alloc::*
or core::*
.wasm
binary size, consider using crate wee_alloc
as the global allocator in place of the default alloc::alloc::Global
choice.[3]wee_alloc
is buggy and unmaintained.)alloc
) costs less than 8k with LTO after stripping debuginfo: -Clto -Cstrip=debuginfo
. (quoting @bjorn3)Sometimes, you will need to switch to no_std
-compatible dependencies for some types or implementations. E.g. if you use structs like struct HashMap
or HashSet
, consider using the drop-in replacement from crate hashbrown
.
If some of your dependencies are outright non-no_std
compliant, e.g. crate threadpool
, then you could turn them into conditional dependencies:
Similar to the conditional dependency, you could have conditionally compiled code:
If one of your dependencies uses bindings for foreign code (via FFI), e.g. crate blst
with original code written in C and Assembly, then you need to further ensure your Clang and LLVM toolchain is configured properly. Specifically,
clang
recognizes wasm targets by running: clang --version --target=wasm32-unknown-unknown
.CC
and AR
are pointing to the right version.arm64
, but targetting wasm32
), then it's recommended to explicitly add RUSTFLAGS="-C target-cpu=generic"
, because by default it will be set to native
leading to lots of warning or even errors.nix
users, here's my flake.nix
that setup the environment for nix-shell
properly:
cargo-nono
could be helpful (especially after the first pass and you could add the cargo nono check
in your CI), but I encounter many false positives (like this) and false negatives (like this). I might come back to re-check its status in the future.Target architecture being wasm
or mips
doesn't necessarily mean bare-metal env β it depends on the full target triple. E.g. wasm-wasi
and mips-unknown-linux
do have system access through some abstracted interfaces. β©οΈ
If you need to use higher-level structs and implementations like Vec
, Rc
, or alloc::collections::*
, then you would still import both alloc
and wee_alloc
crates, as the latter only provides a replacement for the memory allocator. β©οΈ
Shout out to my friends who have helped me debug and shared their lessons/code on this seemingly simple task