One of the Rust programming language promises is "zero-cost abstraction". Therefore the language itself is pretty conservative about what it incorporates. Types that are basics in other languages, such as string
, or features, such as async
, cannot be part of the language itself because they are costly abstractions. The user may not need them at all, or he may prefer other alternative implementations.
To make those types and functions available nonetheless, they are available as part of the Rust standard library, known as std
. Part of this library, known as the prelude
is imported by default in each .rs
file so you don't have to repeat yourself too much.
Most of the time having this standard library available is not a problem because you run your code on a pretty standard environment: your own computer or a Linux server somewhere in the cloud. However, sometimes, you have to build for a more exotic environment, maybe a microcontroller, or web assembly.
For those cases, you have to remove the standard library from your program. Indeed, because this library is designed for a more "classic" execution environment it assumes the existence of things such as a filesystem or a memory allocator.
Therefore we want to get rid of std
entirely and then manually import the parts we actually need and support.
Here are two PR I did on existing repositories to add no_std
support:
- https://github.com/starkware-libs/starknet-api/pull/26/files
- https://github.com/xJonathanLEI/starknet-rs/pull/306/files
Give them a look to find everything explained bellow illustrated.
If you add the #![no_std]
crate directive at the top of your lib.rs
or main.rs
file, it will instruct the compiler to get rid of the std
crate when it compiles. Instead, we can use the core
crate, which contains some of the symbols present in std
, but not all. Only the ones that are common to every compilation target.
Unfortunately, this is not enough. If we stopped there, our crate will only ever compile without the standard library. Yet we want to retain the ability to compile our code with the std
lib if it's supported by the execution environment. We want to be able to compile both with and without the standard library. To do so we have to use Rust features.
In your Cargo.toml
create a feature
section as follows:
[features]
default = ["std"]
std = []
And instead of using #![no_std]
you will use:
#![cfg_attr(not(feature = "std"), no_std)]
This way, the std
library is only removed from your binary if you are not compiling with the std
feature. Otherwise, it will stay exactly the same.
Now, your crate compiles in two ways, with and without std
.
Without std
:
cargo build --no-default-features
With std
(leveraging the implicit default
features):
cargo build
or if you want to be more explicit about the exact mix of features you are using:
cargo build --no-default-features --features std
Pro tip: By default, your IDE is probably configurated to use the
default
feature while runningrust-analyzer
. So you won't see anything changed or red at this point. In order to see what is broken when compiling withoutstd
, just removestd
from thedefault
feature.
At this point, trying to compile your crate without the std
feature will probably fail and raise a ton of errors. But, before we fix your code, we frist need to fix the dependencies your code relies upon.
In your Cargo.toml
you will have to express your intention not to use the std
version of the crate you depend on.
To do so, you will disable the default
feature
[dependencies]
serde = "1.0.152"
will become:
[dependencies]
serde = { version = "1.0.152", default-features = false }
This way, you are only importing the slimmest version of your dependency, with everything accessory stripped away. And you have to hope that the crate maintainers have got the same idea as us: making their crate no_std
compatible by putting everything relying on the standard library behind an std
feature flag.
If they didn't you have two options:
- don't use this crate at all
- fork it and do the whole process described in this article again, this time on a crate that is not yours (if you do so, please open a PR on the original repo so everybody can profit). It gets really tedious quite rapidly, that's why we should all be really consistent about always providing a
no_std
version of our crate ourselves, from the get-go.
Pro tip: features are not really well supported by rustdoc, so it's not always easy to know which features a crate offers if the maintainers didn't manually list them. The best way to find out is to read its
Cargo.toml
file.
At this point, you will have each one of your dependencies imported without std
. Nonetheless, when you compile with the std
feature, you may want to put those crates' std
-guarded content back in place.
If so, edit your std
feature to look something like this:
std = ["serde/std"]
This way, when compiling with the std
feature flag, the compiler will use the version of the dependency crates also compiled with the std
flag.
The last thing in order to be able to compile your crate in no_std
, is to fix your crate code.
You will face two main types of problems: broken imports and incompatible functionalities
When compiling without the std
feature your code imports will break in two manners:
- everything that was previously implicitly imported by the
prelude
won't be in scope anymore - any import that relies on the
std
crate won't be a valid import path anymore
If your crate is only a few files, you can manually fix every import.
Import, behind a feature flag, the symbols that were previously imported by the prelude:
#[cfg(not(feature = "std"))]
use core::boxed::Box;
And replace imports using std
with imports using core
.
use core::ops::BitOr;
Pro tip: here are three lints you can add to your crate to highlight the problematic imports:
The naive approach has drawbacks. It is verbose and error-prone but also tedious to maintain if you have many files and an evolving codebase. To avoid those problems a good solution is to centralize the imports at the crate root level and then use everything from there. Nonetheless, it's not a silver bullet, and a lot of small and medium crates would be better off keeping it simple. It goes as follows.
Create a with_std.rs
file where you re-export the stuff you need, from the standard library:
use std::{ops, any};
Create a without_std.rs
file where you re-export the exact same stuff, but from the core
crate this time:
use core::{ops, any};
In your crate root (lib.rs
or main.rs
) add the following lines:
#[cfg(feature = "std")]
include!("./with_std.rs");
#[cfg(not(feature = "std"))]
include!("./without_std.rs");
Now you can use those items anywhere in your crate, like this:
use crate::any::Any;
It will compile in both std
and no_std
mode, without having to use a cfg
feature flag anywhere else.
If you feel like it, you can customize it to be a bit more expressive by doing something like this:
// without_std.rs
pub mod without_std {
pub use core::{ops, any};
}
// with_std.rs
pub mod with_std {
pub use std::{ops, any};
}
// lib.rs
mod stdlib {
#[cfg(feature = "std")]
pub use crate::with_std::*;
#[cfg(not(feature = "std"))]
pub use crate::without_std::*;
}
// any_other_file.rs
use crate::stdlib::any::Any;
There are things that you simply cannot do on no_std
, such as interacting with the file system.
Therefore anything that is related to the file system, is not part of the core
crate and does not exist when compiling with no_std
. Symbols like std::io::Reader
or std::fs::read
are simply not accessible.
There are two things to do:
First, get rid of every no_std
-incompatible symbol in the core of your crate logic.
Then, add back no_std
-incompatible functionalities behind the #[cfg(feature = "std")]
flag.
Thus, anything io
-related can be added back as optional, but convenient, functions, that can be used only in std
mode, but are not part of the core crate logic.
Not all hardware comes with a memory allocator, but some do ( and web assembly does).
In Rust, in order to use a heap, you will have to define a #[global_allocator]
. It's a global data structure responsible for managing your program memory usage safely. It will give memory to your program when needed and take it back when not needed anymore, allowing you to create run-time growable data structures such as Vec
, String
, and HashMap
.
There are multiple implementations of such memory allocators (tcmalloc
, jemalloc
, ...) and often there is a default one installed on the system which is common to all processes.
If the target you are building for has such a thing, you can import the crate alloc
in your codebase and benefit from all the cool things it offers. Here is how.
In Cargo.toml
, add a new alloc
feature:
[features]
default = ["std"]
std = []
alloc = []
If some of your dependencies also have an alloc
feature you can include it too:
[features]
default = ["std"]
std = ["serde/std"]
alloc = ["serde/alloc"]
Create a with_alloc.rs
file:
#[macro_use]
extern crate alloc;
use alloc::{string, vec, boxed};
Include it in without_std.rs
, behind a feature flag:
#[cfg(feature = "alloc")]
include!("./with_alloc.rs");
Because Vec
, String
, etc are already present in the standard library we don't want to import them again from alloc
in case we are also compiling with the std
feature. Therefore if compiled with both std
and alloc
, the alloc
feature will just be ignored.
HashMap
is not part of the alloc
crate. You can use other collections instead such as alloc::collections::BTreeMap
to achieve the same role, but if you really want to use HashMap
there is still a way: the hashbrown crate.
Just add it to your dependencies in your Cargo.toml
:
[dependencies]
hashbrown = "0.13.2"
This crate is by default no_std
compatible, so there is no obligation to get rid of its default features.
Then edit your with_alloc.rs
file:
#[macro_use]
extern crate alloc;
use alloc::{string, vec, rc};
pub mod collections {
pub use hashbrown::{Hashmap, HashSet};
}
Along with your with_std.rs
file:
use std::{ops, boxed};
pub mod collections {
pub use std::collections::{HashMap, HashSet};
}
Now you can do the following in any rust file of your crate:
use crate::collections::HashMap;
If you use the alloc
crate and try to compile your crate as a staticlib
, an error will be raised asking you to define a global allocator and a bunch of other symbols.
My advice is the following: "don't".
Compile to rlib
, lib
or dylib
instead and everything will be fine.
More on the subject of Rust code linkage here.
thiserror
is a great crate to derive the Error
trait onto your own types.
Our problem is that the Error
trait is defined in std::error
, which is not part of the core
crate.
You can use thiserror_no_std instead, it's a wrapper crate over thiserror
, but it sometimes has some strange behaviours.
In Cargo.toml
:
[features]
std = ["thiserror-no-std/std"]
[dependencies]
# thiserror = "1.0.38" <- remove thiserror
thiserror-no-std = "2.0.2"
Use it in any Rust file:
use thiserror_no_std::Error;
snafu is another alternative that offers native no_std
support.
But if you don't want any trouble, the best way is still to implement everything yourself. There are three you will have to implement on your Error
type:
core::fmt::Display
std::error::Error
, behind a#[cfg(feature = "std")]
flagcore::convert::From
, only if some of your error variants require it
Pro tip: you can expand the macro to copy-paste the generated code directly and then adapt it.