Aside: First, use Git to create a new branch for your project. That way, if things don’t work out, you can easily undo all changes.
Mark the top of lib.rs
with:
#![cfg_attr(not(test), no_std)]
This tells the Rust compiler not to include the standard library, except when testing.
Aside 1: My project is a library project with a
lib.rs
. I believe the steps for a binary project with amain.rs
are about the same, but I haven’t tested them.Aside 2: We’ll talk much more about code testing in later rules.
Adding the “no_std” line to range-set-blaze
’s lib.rs
, causes 40 compiler problems, most of this form:
Fix some of these by changing, “std::” to “core::” in your main code (not in test code). For range-set-blaze
, this reduces the number of problems from 40 to 12. This fix helps because many items, such as std::cmp::max
, are also available as core::cmp::max
.
Sadly, items such as Vec
and Box
cannot be in core
because they need to allocate memory. Happily, if you’re willing to support memory allocation, you can still use them.
Should you allow your crate to allocate memory? For WASM you should. For many embedded applications, you also should. For some embedded applications, however, you should not. If you decide to allow memory allocation, then at the top of lib.rs
add:
extern crate alloc;
You can now add lines such as these to get access to many memory-allocated items:
extern crate alloc;use alloc::boxed::Box;
use alloc::collections::btree_map;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use alloc::{format, string::String};
use alloc::vec;
With range-set-blaze
, this reduces the number of problems from 12 to two. We’ll fix these in Rule 3.
Aside: What if you are writing for an embedded environment that can’t use memory allocation and are having problems with, for example,
Vec
. You may be able to re-write. For example, you may be able to use an array in place of a vector. If that doesn’t work, take a look at the other rules. If nothing works, you may not be able to port your crate tono_std
.
The Rust compiler complains if your project used a crate that puts “std” functions in your code. Sometimes, you can search crates.io and find alternative “no_std” crates. For example, the popular thiserror
crate injects “std” into your code. However, the community has created alternatives that do not.
In the case of range-set-blaze
, the two remaining problems relate to crate gen_ops
— a wonderful crate for defining operators such as “+” and “&” conveniently. Version 0.3.0 of gen_ops
did not fully support “no std”. Version 0.4.0, however, does. I updated my dependencies in Cargo.toml
and improved my “no std” compatibility.
I can now run these commands:
cargo check # check that compiles as no_std
cargo test # check that tests, using std, still pass
The command cargo check
confirms that my crate isn’t directly using the standard library. The command cargo test
confirms that my tests (which still use the standard library) continue to pass. If your crate still doesn’t compile, take a look at the next rule.
Embedded processors generally don’t support reading and writing files. Likewise, WASM doesn’t yet fully support files. While you can find some file-related “no std” crates, none seem comprehensive. So, if file IO is central to your crate, porting to WASM and embedded may not be practical.
However, if file IO — or any other std-only function — is merely incidental to your crate, you can make that function optional via a “std” feature. Here is how:
Add this section to your Cargo.toml
:
[package]
#...
resolver = "2" # the default for Rust 2021+[features]
default = ["std"]
std = []
alloc = []
This says that your crate now has two features, “std” and “alloc”. By default, the compiler should use “std”.
At the top of your lib.rs
, replace:
#![cfg_attr(not(test), no_std)]
with:
#![cfg_attr(not(feature = "std"), no_std)]
This says that if you do not apply the “std” feature, the compiler should compile without the standard library.
On the line before any code that is std-only, placed #[cfg(feature = "std")]
. For example, here we define a function that creates a RangeSetBlaze
struct based on the contents of a file:
#[cfg(feature = "std")]
use std::fs::File;
#[cfg(feature = "std")]
use std::io::{self, BufRead};
#[cfg(feature = "std")]
use std::path::Path;#[cfg(feature = "std")]
#[allow(missing_docs)]
pub fn demo_read_ranges_from_file<P, T>(path: P) -> io::Result<RangeSetBlaze<T>>
where
P: AsRef<Path>,
T: FromStr + Integer,
{
//...code not shown
}
To check the “std” and “alloc” features, do this:
cargo check # std
cargo check --features alloc --no-default-features
We can test “std” with
cargo test
Aside: Surprisingly,
cargo test --features alloc --no-default-features
does not test “alloc”. That is because tests require threads, allocation, and other things that may not be available inno_std
so cargo always runs regular tests as “std”.
At this point we’re checking both “std” and “alloc”, so can we assume that our library will work with WASM and embedded. No! Generally, Nothing works without being tested. Specifically, we might be depending on crates that use “std” code internally. To find these issues, we must test in the WASM and embedded environments.
Install the WASM cross compiler and check your project with these commands:
rustup target add wasm32-unknown-unknown # only need to do this once
# may find issues
cargo check --target wasm32-unknown-unknown --features alloc --no-default-features
When I do this on range-set-blaze
, it complains that the getrandom
crate doesn’t work with WASM. On the one hand, I’m not surprised that WASM does not fully support random numbers. On the other hand, I am surprised because my project doesn’t directly depend on getrandom
. To find the indirect dependency, I use cargo tree
. I discover that my project depends on crate rand
which depends on getrandom
. Here is the cargo tree
command to use:
cargo tree --edges no-dev --format "{p} {f}" --features alloc --no-default-features
The command outputs both crates and the features they use:
range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm3) alloc
├── gen_ops v0.4.0
├── itertools v0.10.5 default,use_alloc,use_std
│ └── either v1.8.1 use_std
├── num-integer v0.1.45 default,std
│ └── num-traits v0.2.15 default,std
│ [build-dependencies]
│ └── autocfg v1.1.0
│ [build-dependencies]
│ └── autocfg v1.1.0
├── num-traits v0.2.15 default,std (*)
├── rand v0.8.5 alloc,default,getrandom,libc,rand_chacha,std,std_rng
│ ├── rand_chacha v0.3.1 std
│ │ ├── ppv-lite86 v0.2.17 simd,std
│ │ └── rand_core v0.6.4 alloc,getrandom,std
│ │ └── getrandom v0.2.9 std
│ │ └── cfg-if v1.0.0
...
The output shows that range-set-blaze
depends on rand
. Also, it shows that rand
depends on getrandom
with its “std” feature.
I read the getrandom
documentation and learn that its “js” feature supports WASM. So, how do we tell rand
to use getrandom/js
, but only when we compile with our “alloc” feature? We update our Cargo.toml
like so:
[features]
default = ["std"]
std = ["getrandom/std"]
alloc = ["getrandom/js"][dependencies]
# ...
getrandom = "0.2.10"
This says that our “std” feature depends on getrandom
’s “std” feature. Our “alloc” feature, however, should use the js
feature of getrandom
.
This now works:
cargo check --target wasm32-unknown-unknown --features alloc --no-default-features
So, we have WASM compiling, but what about testing WASM?
Let’s put the WASM version to work, first with tests and then with a demo web page.
Create WASM tests in tests/wasm.rs
You can test on WASM almost as easily as you can test natively. We do this by having the original tests only run natively while an almost duplicate set of tests run on WASM. Here are the steps based on The wasm-bindgen
Guide:
- Do
cargo install wasm-bindgen-cli
- Copy your current integration tests from, for example,
tests/integration_tests.rs
totests/wasm.rs
. (Recall that in Rust, integration tests are tests that live outside thesrc
directory and that see only the public methods of a project.) - At the top of
tests/wasm.rs
, remove#![cfg(test)]
and add#![cfg(target_arch = “wasm32”)]
use wasm_bindgen_test::*;
wasm_bindgen_test_configure!(run_in_browser); - In
wasm.rs
, replace all#[test]
’s to#[wasm_bindgen_test]
’s. - Everywhere you have
#![cfg(test)]
(typically, intests/integration_tests.rs
andsrc/tests.rs
) add the additional line:#![cfg(not(target_arch = "wasm32"))]
- In your,
Cargo.toml
, change your[dev-dependencies]
(if any) to[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
- In your,
Cargo.toml
, add a section:
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3.37"
With all this set up, native tests, cargo test
, should still work. If you don’t have the Chrome browser installed, install it. Now try to run the WASM tests with:
wasm-pack test --chrome --headless --features alloc --no-default-features
It will likely fail because your WASM tests use dependencies that haven’t or can’t be put in Cargo.toml
. Go through each issue and either:
- Add the needed dependencies to
Cargo.toml’
s[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
section, or - Remove the tests from
tests/wasm.rs
.
For range-set-blaze
, I removed all WASM tests related to testing the package’s benchmarking framework. These tests will still be run on the native side. Some useful tests in tests\wasm.rs
needed crate syntactic-for
, so I added it to Cargo.toml
, under [target.'cfg(target_arch = "wasm32")'.dev-dependencies]
. With this fixed, all 59 WASM tests run and pass.
Aside: If your project includes an examples folder, you may need create a
native
module inside your example and awasm
module. See thisrange-set-blaze
file for an “example” example of how to do this.
Create a WASM demo in tests/wasm-demo
Part of the fun of supporting WASM is that you can demo your Rust code in a web page. Here is a web demo of range-set-blaze
.
Follow these steps to create your own web demo:
In your project’s main Cargo.toml
file, define a workspace and add tests/wasm-demo
to it:
[workspace]
members = [".", "tests/wasm-demo"]
In your tests folder, create a test/wasm-demo
subfolder.
It should contain a new Cargo.toml
like this (change range-set-blaze
to the name of your project):
[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
range-set-blaze = { path = "../..", features = ["alloc"], default-features = false}
Also, create a file tests/wasm-demo/src/lib.rs
. Here is mine:
#![no_std]
extern crate alloc;
use alloc::{string::ToString, vec::Vec};
use range_set_blaze::RangeSetBlaze;
use wasm_bindgen::prelude::*;#[wasm_bindgen]
pub fn disjoint_intervals(input: Vec<i32>) -> JsValue {
let set: RangeSetBlaze<_> = input.into_iter().collect();
let s = set.to_string();
JsValue::from_str(&s)
}
This file defines a function called disjoint_intervals
that takes a vector of integers as input, for example, 100,103,101,102,-3,-4
. Using the range-set-blaze
package, the function returns a string of the integers as sorted, disjoint ranges, for example, -4..=-3, 100..=103
.
As your final step, create file tests/wasm-demo/index.html.
Mine uses a little JavaScript to accept a list of integers and then call the Rust WASM function disjoint_intervals
.
<!DOCTYPE html>
<html>
<body>
<h2>Rust WASM RangeSetBlaze Demo</h2>
<p>Enter a list of comma-separated integers:</p>
<input id="inputData" type="text" value="100,103,101,102,-3,-4" oninput="callWasmFunction()">
<br><br>
<p id="output"></p>
<script type="module">
import init, { disjoint_intervals } from './pkg/wasm_demo.js';function callWasmFunction() {
let inputData = document.getElementById("inputData").value;
let data = inputData.split(',').map(x => x.trim() === "" ? NaN : Number(x)).filter(n => !isNaN(n));
const typedArray = Int32Array.from(data);
let result = disjoint_intervals(typedArray);
document.getElementById("output").innerHTML = result;
}
window.callWasmFunction = callWasmFunction;
init().then(callWasmFunction);
</script>
</body>
</html>
To run the demo locally, first move your terminal to tests/wasm-demo
. Then do:
# from tests/wasm-demo
wasm-pack build --target web
Next, start a local web server and view the page. I use the Live Preview extension to VS Code. Many people use python -m http.server
. The range-set-blaze
demo looks like this (also available, live on GitHub):
I find watching my Rust project run in a web page very gratifying. If WASM-compatibility is all you are looking for, you can skip to Rule 9.
If you want to take your project a step beyond WASM, follow this rule and the next.
Be sure you move your terminal back to your project’s home directory. Then, install thumbv7m-none-eabi
, a popular embedded processor, and check your project with these commands:
# from project's home directory
rustup target add thumbv7m-none-eabi # only need to do this once
# will likely find issues
cargo check --target thumbv7m-none-eabi --features alloc --no-default-features
When I do this on range-set-blaze
, I get errors related to four sets of dependencies:
thiserror
— My project depended on this crate but didn’t actually use it. I removed the dependency.rand
andgetrandom
— My project only needs random numbers for native testing, so I moved the dependency to[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
. I also updated my main and testing code.itertools
,num-traits
, andnum-integer
— These crates offer features for “std” and “alloc”. I updatedCargo.toml
like so:
...
[features]
default = ["std"]
std = ["itertools/use_std", "num-traits/std", "num-integer/std"]
alloc = ["itertools/use_alloc", "num-traits", "num-integer"][dependencies]
itertools = { version = "0.10.1", optional = true, default-features = false }
num-integer = { version = "0.1.44", optional = true, default-features = false }
num-traits = { version = "0.2.15", optional = true, default-features = false }
gen_ops = "0.4.0"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
#...
rand = "0.8.4"
#...
How did I know which feature of which dependancies to use? Understanding the features of a crate such as itertools
requires reading its documentation and (often) going to its GitHub repository and reading its Cargo.toml
. You should also use cargo tree
to check that you are getting the desire feature from each dependency. For example, this use of cargo tree
shows that for a default compile, I get the “std” features of range-set-blaze
, num-integer
, and num-traits
and the “use-std” features of itertools
and either:
cargo tree --edges no-dev --format "{p} {f}"
range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm4) default,itertools,num-integer,num-traits,std
├── gen_ops v0.4.0
├── itertools v0.10.5 use_alloc,use_std
│ └── either v1.8.1 use_std
├── num-integer v0.1.45 std
│ └── num-traits v0.2.15 std
│ [build-dependencies]
│ └── autocfg v1.1.0
│ [build-dependencies]
│ └── autocfg v1.1.0
└── num-traits v0.2.15 std (*)
And this shows that for a --features alloc --no-default-feature
compile, I get the desired “use_alloc” feature of itertools
and “no default” version of the other dependances:
cargo tree --edges no-dev --format "{p} {f}" --features alloc --no-default-features
range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm4) alloc,itertools,num-integer,num-traits
├── gen_ops v0.4.0
├── itertools v0.10.5 use_alloc
│ └── either v1.8.1
├── num-integer v0.1.45
│ └── num-traits v0.2.15
│ [build-dependencies]
│ └── autocfg v1.1.0
│ [build-dependencies]
│ └── autocfg v1.1.0
└── num-traits v0.2.15 (*)
When you think you have everything working, use these commands to check/test native, WASM, and embedded:
# test native
cargo test
cargo test --features alloc --no-default-features
# check and test WASM
cargo check --target wasm32-unknown-unknown --features alloc --no-default-features
wasm-pack test --chrome --headless --features alloc --no-default-features
# check embedded
cargo check --target thumbv7m-none-eabi --features alloc --no-default-features
These check embedded, but what about testing embedded?
Let’s put our embedded feature to work by creating a combined test and demo. We will run it on an emulator called QEMU.
Testing native Rust is easy. Testing WASM Rust is OK. Testing embedded Rust is hard. We will do only a single, simple test.
Aside 1: For more, about running and emulating embedded Rust see: The Embedded Rust Book.
Aside 2: For ideas on a more complete test framework for embedded Rust, see defmt-test. Sadly, I couldn’t figure out how to get it to run under emulation. The cortex-m/testsuite project uses a fork of defmt-test and can run under emulation but doesn’t offer a stand-alone testing crate and requires three additional (sub)projects.
Aside 3: One embedded test is infinitely better than no tests. We’ll do the rest of our testing at the native and WASM level.
We will create the embedded test and demo inside our current tests
folder. The files will be:
tests/embedded
├── .cargo
│ └── config.toml
├── Cargo.toml
├── build.rs
├── memory.x
└── src
└── main.rs
Here are the steps to creating the files and setting things up.
- Install the QEMU emulator. On Windows, this involves running an installer and then manually adding
"C:\Program Files\qemu\"
to your path.
2. Create a tests/embedded/Cargo.toml
that depends on your local project with “no default features” and “alloc”. Here is mine:
[package]
edition = "2021"
name = "embedded"
version = "0.1.0"[dependencies]
alloc-cortex-m = "0.4.4"
cortex-m = "0.6.0"
cortex-m-rt = "0.6.10"
cortex-m-semihosting = "0.3.3"
panic-halt = "0.2.0"# reference your local project here
range-set-blaze = { path = "../..", features = ["alloc"], default-features = false }
[[bin]]
name = "embedded"
test = false
bench = false
3. Create a file tests/embedded/src/main.rs
. Put your test code after the “test goes here” comment. Here is my file:
// based on https://github.com/rust-embedded/cortex-m-quickstart/blob/master/examples/allocator.rs
// and https://github.com/rust-lang/rust/issues/51540
#![feature(alloc_error_handler)]
#![no_main]
#![no_std]extern crate alloc;
use alloc::string::ToString;
use alloc_cortex_m::CortexMHeap;
use core::{alloc::Layout, iter::FromIterator};
use cortex_m::asm;
use cortex_m_rt::entry;
use cortex_m_semihosting::{debug, hprintln};
use panic_halt as _;
use range_set_blaze::RangeSetBlaze;
#[global_allocator]
static ALLOCATOR: CortexMHeap = CortexMHeap::empty();
const HEAP_SIZE: usize = 1024; // in bytes
#[entry]
fn main() -> ! {
unsafe { ALLOCATOR.init(cortex_m_rt::heap_start() as usize, HEAP_SIZE) }
// test goes here
let range_set_blaze = RangeSetBlaze::from_iter([100, 103, 101, 102, -3, -4]);
assert!(range_set_blaze.to_string() == "-4..=-3, 100..=103");
hprintln!("{:?}", range_set_blaze.to_string()).unwrap();
// exit QEMU/ NOTE do not run this on hardware; it can corrupt OpenOCD state
debug::exit(debug::EXIT_SUCCESS);
loop {}
}
#[alloc_error_handler]
fn alloc_error(_layout: Layout) -> ! {
asm::bkpt();
loop {}
}
4. Copy build.rs
and memory.x
from cortex-m-quickstart
’s GitHub to tests/embedded/
.
5. Create a tests/embedded/.cargo/config.toml
containing:
[target.thumbv7m-none-eabi]
runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"[build]
target = "thumbv7m-none-eabi"
6. Update your project’s main Cargo.toml
by adding tests/embedded
to your workspace:
[workspace]
members = [".", "tests/wasm-demo", "tests/embedded"]
This post originally appeared on TechToday.