---
tags: engineering, rust
slug: rust-no-std
---
# Rust `no_std` Playbook
[![hackmd-github-sync-badge](https://hackmd.io/zS-Ys5fxTXWBvma_Mm9mXQ/badge)](https://hackmd.io/zS-Ys5fxTXWBvma_Mm9mXQ)
:::info
This 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](https://users.rust-lang.org/t/practical-guides-on-no-std-and-wasm-support/94762).
:::
## Why
### Bare-metal Environments
> 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 use `no_std`.
Some targets with arch like WASM or MIPS _could_ be bare-metal environments[^bare-metal-caveats], 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](https://docs.rust-embedded.org/book/intro/no-std.html).
[^bare-metal-caveats]: 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.
### On the `std` support inside WASM
"Wait!", you say, "I remember the entirety of `std` is supported in WASM according to [the Rustc Book](https://doc.rust-lang.org/rustc/platform-support.html#tier-2)". 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](https://github.com/cuviper)) on [Zulip thread](https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/std.20support.20in.20wasm/near/360339062)
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.
### More on Rust + WASM
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.
- There are 6 WASM-related targets: `wasm[32|64]-unknown-unknown`, `wasm[32|64]-emscripten`, `wasm[32|64]-wasi`.
- browsers only supports 32-bit WASM modules as of May 2023.
- `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.
By default, _the target_ doesn't have direct access to the file system or other system-level resources. But _the WASM modules_ for this target could interact with the host env via runtime-specific APIs (e.g. [crate `web-sys`](https://crates.io/crates/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](https://github.com/rustwasm/wasm-bindgen/blob/5453e332dc8222a1dc3f4d8dadb0d922da528790/guide/src/reference/rust-targets.md?plain=1) `wasm32-unknown-unknown`.
- To produce standalone binaries to run outside of the web env, use `wasm*-wasi` and runtime like [`wasmtime`](https://github.com/bytecodealliance/wasmtime) or [`wasmer`](https://github.com/wasmerio/wasmer).
- Shared-memory concurrency is supported in WASM through "[Threads Proposal](https://github.com/WebAssembly/threads)". It is usable today as [demonstrated here](https://rustwasm.github.io/docs/wasm-bindgen/examples/raytrace.html)! But the toolchain is underbaked and requires lots of manual setup (quoting [@alexcrichton](https://github.com/alexcrichton)).
Further reading [here](https://web.dev/webassembly-threads/).
| `std`-linking primitives | Replacement for WASM |
|---| ---|
| `/dev/(u)random` (system randomness) | [crate `getrandom`](https://docs.rs/getrandom/latest/getrandom/#webassembly-support), with `features = ["js"]` for target `wasm32-unknown-unknown` |
| `rayon` (data parallelism) | [crate `wasm-bingen-rayon`](https://github.com/GoogleChromeLabs/wasm-bindgen-rayon), some [`maybe-rayon` options](https://crates.io/search?q=maybe_rayon) for feature-flag-toggled parallelism |
| `std::time::Instant` | [crate `instant`](https://crates.io/crates/instant) |
| `std::collections::{HashMap, HashSet}` | [crate `hashbrown`](https://crates.io/crates/hashbrown) |
## How: a somewhat opinionated way
While there are many paths to `no_std` compliance, the steps below should be mostly idiomatic to the Rust community.
1. If your crate never uses `std`, you could simply add `#![no_std]` (read more on [no_std attribute here](https://doc.rust-lang.org/reference/names/preludes.html?#the-no_std-attribute)), then job done and jump to the next step!
More often, you have an optionally-std crate, then add to your `lib.rs`:
```rust
#![no_std]
#[cfg(feature = "std")]
extern crate std;
```
Then configure `Cargo.toml` as follows:
```toml
[features]
std = ["dep-a/std", "dep-b/std"]
# optionally use `std` by default, so that downstream could
# enable `no_std` using "default-features=false":
# default = ["std"]
```
- Another common practice is declaring `#![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."[^alt-no-std-declare]
[^alt-no-std-declare]: Pointed out by [@kpreid](https://github.com/kpreid/)
2. Run:
```shell
rustup target add thumbv7em-none-eabi
cargo build --target thumbv7em-none-eabi --no-default-features
```
If you just want WASM-compilable, you can replace the target with one of the [six aforementioned WASM targets](#More-on-Rust--WASM). 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`.
3. Adding checks to your CI (the following is specific to GitHub action only):
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Install stable toolchain
uses: actions-rs/toolchain@v1
with:
profile: default
toolchain: stable
override: true
default: true
target: |
wasm32-unknown-unknown
- name: Check no_std support and WASM compilation
env:
RUSTFLAGS: -C target-cpu=generic
run: |
cargo check --no-default-features
cargo build --target wasm32-unknown-unknown --no-default-features
```
Alternatively, if you have multiple targets to test, you could use the [`strategy.matrix` feature](https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs) like:
```yaml
# credit: https://github.com/RustCrypto/
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
rust:
- stable
target:
- thumbv7em-none-eabi
- wasm32-unknown-unknown
steps:
- uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust }}
targets: ${{ matrix.target }}
- run: cargo build --no-default-features --target ${{ matrix.target }}
```
### Debugging workflow (opinionated)
This is my approach for debugging compilation errors and I'd greatly appreciate suggestions on better approaches.
1. Comment out all my code in `lib.rs` or `main.rs` (faster compilation check later).
2. Iteratively comment out some `[dependencies]` and then run `cargo build --target wasm32-unknown-unknown --no-default-features` to _locate_ the problematic dependencies or their feature flag selections.
Usually, if they follow the idiosyncratic pattern, just use `crate-a = { version = "x.x.x", default-features = false }` should suffice.
When in trouble, refer to the [Common issues](#Common-issues) section below.
3. Sometimes, `[dev-dependencies]` also would cause compilation errors, so repeat Step 2 for those as well.
4. Once the (empty) package could compile with all dependencies required, uncomment the actual `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.
### Common issues
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::*`
- ~~Tips: for a smaller `.wasm` binary size, consider using [crate `wee_alloc`](https://github.com/rustwasm/wee_alloc) as the global allocator in place of the default [`alloc::alloc::Global`](https://doc.rust-lang.org/alloc/alloc/struct.Global.html) choice.[^wee-alloc]~~ (`wee_alloc` is buggy and [unmaintained](https://rustsec.org/advisories/RUSTSEC-2022-0054.html).)
[^wee-alloc]: 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.
- Using the default allocator (i.e. `alloc`) costs less than 8k with LTO after stripping debuginfo: `-Clto -Cstrip=debuginfo`. (quoting [@bjorn3](https://users.rust-lang.org/t/practical-guides-on-no-std-and-wasm-support/94762/5))
- 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`](https://github.com/rust-lang/hashbrown).
- If some of your dependencies are outright non-`no_std` compliant, e.g. [crate `threadpool`](https://github.com/rust-threadpool/rust-threadpool), then you could turn them into conditional dependencies:
```toml
[dependencies]
threadpool = "1.8.1"
[features]
std = ["dep:threadpool"]
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
threadpool = { version = "^1.8.1", optional = true }
```
- Similar to the conditional dependency, you could have conditionally compiled code:
```rust
#[cfg(feature = "std")]
use std::sync::Arc;
#[cfg(feature = "std")]
fn parallel_add(list: Arc<Vec<u8>>) -> Result<u8, std::error:Error> { }
#[cfg(not(feature = "std"))]
fn parallel_add(list: &[u8]) -> Result<u8, MyError> { }
```
- If one of your dependencies uses bindings for foreign code (via FFI), e.g. [crate `blst`](https://github.com/supranational/blst) with original code written in C and Assembly, then you need to further ensure your Clang and LLVM toolchain is configured properly. Specifically,
- first ensure that your `clang` recognizes wasm targets by running: `clang --version --target=wasm32-unknown-unknown`.
- Then ensure your environment variables like `CC` and `AR` are pointing to the right version.
- Furthermore, if you are running on arch different from that of the target (e.g. you run an Apple M1 or `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.
- For `nix` users, here's my `flake.nix` that setup the environment for `nix-shell` properly:
```nix
{
description = "My nix-shell dev env";
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
inputs.flake-utils.url = "github:numtide/flake-utils"; # for dedup
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [(import rust-overlay)];
pkgs = import nixpkgs { inherit system overlays; };
stableToolchain = pkgs.rust-bin.stable.latest.minimal.override {
extensions = [ "clippy" "llvm-tools-preview" "rust-src" ];
targets = ["wasm32-unknown-unknown"];
};
in with pkgs;
{
devShell = clang15Stdenv.mkDerivation {
name = "clang15-nix-shell";
buildInputs = [
git
stableToolchain
clang-tools_15
clangStdenv
llvm_15
] ++ lib.optionals stdenv.isDarwin [ darwin.apple_sdk.frameworks.Security ];
shellHook = ''
export C_INCLUDE_PATH="${llvmPackages_15.libclang.lib}/lib/clang/${llvmPackages_15.libclang.version}/include"
export CC="${clang-tools_15.clang}/bin/clang"
export AR="${llvm_15}/bin/llvm-ar"
export CFLAGS="-mcpu=generic"
'';
};
}
);
}
```
## Miscellaneous
- Tools like [`cargo-nono`](https://github.com/hobofan/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](https://github.com/hobofan/cargo-nono/issues/4#issuecomment-853387357)) and false negatives (like [this](https://github.com/hobofan/cargo-nono/issues/61)). I might come back to re-check its status in the future.
- :pray: Acknowledgment[^ack], more thanks to everyone in [this discussion](https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/std.20support.20in.20wasm/near/360339062), and [@kpreid](https://github.com/kpreid/), [@bjorn3](https://github.com/bjorn3/) for precious feedback.
[^ack]: Shout out to my friends who have helped me debug and shared their lessons/code on this seemingly simple task :sweat_smile: -- [@sveitser](https://github.com/sveitser), [@DieracDelta](https://github.com/DieracDelta), [@jbearer](https://github.com/jbearer), [@nomaxg](https://github.com/nomaxg), [@nyospe](https://github.com/nyospe), [@ec2](https://github.com/ec2), [@mike1729](https://github.com/mike1729).
<!-- ================ -->
<!-- CSS styles below -->
<style>
.markdown-body {
font: normal normal normal Palatino,Georgia,serif;
font-family: Palatino,Palatino Linotype,Palatino LT STD,Book Antiqua,Georgia,serif;
/* font-size: 18px !important; */
}
h1, h2, h3, h4, h5 {
font-family: 'Circular Std', sans-serif;
color: #5B5B66;
}
h1:hover, h2:hover, h3:hover, h4:hover, h5:hover {
color: #15141A;
}
a {
color: #ba110c;
}
a:hover {
color: #fe2106;
}
code {
font-family: JetBrains Mono, Menlo, Monaco, Consolas, "Courier New", monospace;
}
</style>