--- 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>