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.
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
}
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.
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();
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"));
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");
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();
}
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.
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
// 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
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)*));
}}
}