print! and println! in rust no_std

In the previous post, we created a rust executable which didn't use std. It could write to stdout using a write_string() function, but if we tried to use print! or println! to print formatted strings, it wouldn't work:

error: cannot find macro `println` in this scope

We're gonna see next how to use these formatting macros work.

Initial code

We start with the following source code:

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

write_string

Having to define a binary string and an usafe block just to print a string is ugly, so we're going to create a function that takes a string and prints it to stdout

use core::ffi::c_void;

fn write_string(s: &str) {
    unsafe {
        write(1, s.as_ptr() as *const c_void, s.len());
    }
}

Nothing special there, it just takes what we've already done and wraps it inside a safe function. It also imports core::ffi::c_void because it's used several times in our source code.

Implementing a fmt::Write

Formatted output in rust uses the fmt::Write trait. Let's create a new struct that implements it

use core::fmt;

pub struct Writer;

impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        write_string(s);
        Ok(())
    }
}

We can call it with the following:

use fmt::Write;
Writer{}.write_str("It works!\n").unwrap();

We need to import fmt::Write because methods from traits needs to imported before use.

Implementing fmt::Write is not just a convoluted way to output strings, it also enables the Write::write_fmt function which can be used for formatted output:

use fmt::Write;
Writer{}.write_fmt(format_args!("It works\n")).unwrap();

Write::write_fmt is declared as:

fn write_fmt(&mut self, args: Arguments<'_>) -> Result

The format_args! just takes a list of arguments and create an Argument struct out of them. More complicated formatting is possible thusly:

use fmt::Write;
Writer{}.write_fmt(format_args!("{}\n", "It works")).unwrap();

Simplifying formatted output using a print function

We can simplify this code further by creating a function that creates a temporary writer and writes to it:

fn print(args: fmt::Arguments) {
    use fmt::Write;
    Writer{}.write_fmt(args).unwrap();
}

Our formatted output call can then be simplified to:

print(format_args!("{}!\n", "It works"));

print! and println!

We can forego the usage of format_args by defining a macro that calls it for us:

macro_rules! print {
    ($($arg:tt)*) => {{
        print(format_args!($($arg)*));
    }}
}

This macro forwards its arguments to format_args, and calls print, simplifying our earlier call to:

print!("{}!\n", "It works");

And we can now implement a println! macro by using print:

macro_rules! println {
    () => {{
        print!("\n");
    }};
    
    ($($arg:tt)*) => {{
        print!("{}\n", format_args!($($arg)*));
    }}
}

At the call site:

println!("{}!", "It works");

Reusable output printing functions

All our printing functions are defined in the same file for now, let's put them in a separate file so they can be reused in other modules and source files.

Copy all our formatted output functions in a new src/print.rs file, and fix imports:

// src/print.rs
use core::ffi::c_void;
use core::fmt;

extern "C" {
    fn write(fildes: i32, buf: *const c_void, nbyte: usize);
}

pub fn write_string(s: &str) {
    unsafe {
        write(1, s.as_ptr() as *const c_void, s.len());
    }
}

pub struct Writer;

impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        write_string(s);
        Ok(())
    }
}

pub fn print(args: fmt::Arguments) {
    use fmt::Write;
    Writer{}.write_fmt(args).unwrap();
}

macro_rules! print {
    ($($arg:tt)*) => {{
        print(format_args!($($arg)*));
    }}
}

macro_rules! println {
    () => {{
        print!("\n");
    }};

    ($($arg:tt)*) => {{
        print!("{}\n", format_args!($($arg)*));
    }}
}

Back into src/main.rs, we can get rid of the imports, and we need to import the print module:

// src/main.rs

#![no_std]
#![no_main]

mod print;

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}   

#[no_mangle]
pub extern "C" fn main() -> u32 {
    println!("{}!", "It works");
    0
}

We're greeted with this lovely compile error:

error: cannot find macro `println` in this scope

That's because we didn't export our macros. We need to annotate print! and println! with #[macro_export]. That will make the macros exportable and put them at the crate root:

// src/print.rs
#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => {{
        print(format_args!($($arg)*));
    }}
}

#[macro_export]
macro_rules! println {
    () => {{
        print!("\n");
    }};

    ($($arg:tt)*) => {{
        print!("{}\n", format_args!($($arg)*));
    }}
}

We get the following error:

error[E0423]: expected function, found module `print`
  --> src/print.rs:31:9
   |
31 |         print(format_args!($($arg)*));
   |         ^^^^^ not a function

That's because back at main.rs, the println! macro was expanded, and it contains a call to the print functions, which is not defined in main.rs but in print.rs. We need make the macro expand to the full path to the print function.

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => {{
        $crate::print::print(format_args!($($arg)*));
    }}
}

$crate is expanded to the current crate, so our macro can be called from other crates.

Our program now compiles and run without error.

However, if we remove the println! invocation in main, the compiler complains:

warning: function `print` is never used

So we need annotate our print function #[allow(unused)].

#[allow(unused)]
pub fn print(args: fmt::Arguments) {
    use fmt::Write;
    Writer{}.write_fmt(args).unwrap();
}

Exiting with an error code

Until now, our panic handler just enters an infinite loop. So if we had the following in main.rs, we wouldn't be able to know if there's a panic, yet alone read the error message from the panic call.

panic!("Someone set us the bomb!");

To solve this, we first need to make our executable exit with an error code. That's accomplished by calling the libc function exit. We declare it thusly:

extern "C" {
    fn exit(status: i32) -> !;
}

It's a function that never returns, so we can make it return the never type.

Now our panic handler becomes:

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
    unsafe {
        exit(1);
    }
}

When we run our program, it exits with a status code of 1.

Implementing panic messages

The core::panic::PanicInfo conveniently implements core::fmt::Display, so it can be formatted:

#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    println!("{}", info);
    unsafe {
        exit(1);
    }
}

Which gives us a nice panic message when ran:

panicked at 'Somebody set us the bomb', src/main.rs:21:5

Putting it all together

src/main.rs

// src/main.rs
#![no_std]
#![no_main]

mod print; 

extern "C" {
    fn exit(status: i32) -> !;
}
    
#[panic_handler]
fn panic(info: &core::panic::PanicInfo) -> ! {
    println!("{}", info);
    unsafe {
        exit(1);
    }
}

#[no_mangle]
pub extern "C" fn main() -> u32 {
    panic!("Somebody set us the bomb");
    0
}

src/print.rs

// src/print.rs
use core::ffi::c_void;
use core::fmt;

extern "C" {
    fn write(fildes: i32, buf: *const c_void, nbyte: usize);
}

pub fn write_string(s: &str) {
    unsafe {
        write(1, s.as_ptr() as *const c_void, s.len());
    }
}

pub struct Writer;

impl fmt::Write for Writer {
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        write_string(s);
        Ok(())
    }
}

#[allow(unused)]
pub fn print(args: fmt::Arguments) {
    use fmt::Write;
    Writer{}.write_fmt(args).unwrap();
}

#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => {{
        $crate::print::print(format_args!($($arg)*));
    }}
}

#[macro_export]
macro_rules! println {
    () => {{
        print!("\n");
    }};

    ($($arg:tt)*) => {{
        print!("{}\n", format_args!($($arg)*));
    }}
}