A no_std rust binary is an executable that does not link to the rust standard library.
cargo new --bin nostd
To tell the compiler that we don't need to stinking std, we need to add
[no_std]
at the top of our main file
// src/main.rs
#![no_std]
fn main() {
println!("Hello, world!");
}
Now, if we try to build the project with cargo build
, we'll get a bunch of
errors.
error: cannot find macro `println` in this scope
The first is about println
, which we can get around by removing it from our
code
// src/main.rs
#![no_std]
fn main() {
}
error: `#[panic_handler]` function required, but not found
The second is about panic_handler
. To get around this error, we're gonna
define a function that we will annotate as a panic handler
// src/main.rs
#![no_std]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
fn main() {
}
The panic function's return type is !
because it's
a function that never returns.,
moreover, the compiler will warns us about this if we don't specify !
as its
return type.
error: language item required, but not found: `eh_personality`
eh_personality
is a language item
that is used by the compiler for stack unwinding and general failure. We do not
support stack unwinding for now, so we'll just tell the compiler to abort on
failure. To do so, create a .cargo
directory inside our project, then create
a config.toml
file inside it
# .cargo/config.toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
error: requires `start` lang_item
This means the compiler cannot find the entry point of our program. How come?
We've defined main
, ain't it? Well, turns out main
is not the real entry
point of programs, the rust standard library sets up some required magic
before handing control to us. This magic should happen in the start language
item. For that, we could change the arguments of main
and annotate it with
#[start]
// src/main.rs
#![no_std]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[start]
fn main(argc: isize, argv: *const *const u8) -> isize {
0
}
If we do that however, the compiler will complain that it's an experimental feature, and we would need to use the nightly toolchain. We do not want to use the nightly toolchain, however.
To get around this error, we're gonna add #![no_main]
to our source file to
tell the compiler to shut up about main
// src/main.rs
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
But now, it's the linker that complains it can't find a _main
symbol:
note: ld: entry point (_main) undefined. for architecture arm64
It's libc that wants to call the program's entry point (main). No problem, we just have to create a function called main, disable name mangling, and give it C linkage
// src/main.rs
#![no_std]
#![no_main]
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn main() -> ! {
loop {}
}
On macOS however, every executable needs to link to libSystem.dylib, so we also
have to tweak .cargo/config.toml
# .cargo/config.toml
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-args=-lSystem"]
If we run the executable with cargo run
, it just hangs indefinitely, because
main
only contains an infinite loop. We need to make it do something so we
can be sure it really works, for that, we're gonna print a string to stdout.
Printing a string to stdout is writing a string to file descriptor number 1.
Writing to a file descriptor is calling the write
libc function, which is
defined as:
ssize_t write(int fildes, const void *buf, size_t nbyte);
To call a C function from rust, we need to define it inside an extern "C"
block, and convert it's definition to rust syntax
extern "C" {
fn write(fildes: i32, buf: *const core::ffi::c_void, nbyte: usize);
}
To call it from rust, it must be inside an unsafe block:
let msg = b"It works!\n";
unsafe {
write(1, msg.as_ptr() as *const core::ffi::c_void, msg.len());
}
And now that it does something, we could also just return a value from main instead of looping indefinitely.
Putting it all together:
// src/main.rs
#![no_std]
#![no_main]
extern "C" {
fn write(fildes: i32, buf: *const core::ffi::c_void, nbyte: usize);
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn main() -> u32 {
let msg = b"It works!\n";
unsafe {
write(1, msg.as_ptr() as *const core::ffi::c_void, msg.len());
}
0
}
If we run this program, it will print It works
to stdout, and exit with a
status code of 0