Global singletons
In this section we'll cover how to implement a global, shared singleton. The embedded Rust book covered local, owned singletons which are pretty much unique to Rust. Global singletons are essentially the singleton pattern you see in C and C++; they are not specific to embedded development but since they involve symbols they seemed a good fit for the embedonomicon.
TODO(resources team) link "the embedded Rust book" to the singletons section when it's up
To illustrate this section we'll extend the logger we developed in the last
section to support global logging. The result will be very similar to the
#[global_allocator]
feature covered in the embedded Rust book.
TODO(resources team) link
#[global_allocator]
to the collections chapter of the book when it's in a more stable location.
Here's the summary of what we want to:
In the last section we created a log!
macro to log messages through a specific
logger, a value that implements the Log
trait. The syntax of the log!
macro
is log!(logger, "String")
. We want to extend the macro such that
log!("String")
also works. Using the logger
-less version should log the
message through a global logger; this is how std::println!
works. We'll also
need a mechanism to declare what the global logger is; this is the part that's
similar to #[global_allocator]
.
It could be that the global logger is declared in the top crate and it could also be that the type of the global logger is defined in the top crate. In this scenario the dependencies can not know the exact type of the global logger. To support this scenario we'll need some indirection.
Instead of hardcoding the type of the global logger in the log
crate we'll
declare only the interface of the global logger in that crate. That is we'll
add a new trait, GlobalLog
, to the log
crate. The log!
macro will also
have to make use of that trait.
$ cat ../log/src/lib.rs
#![allow(unused)] #![no_std] fn main() { // NEW! pub trait GlobalLog: Sync { fn log(&self, address: u8); } pub trait Log { type Error; fn log(&mut self, address: u8) -> Result<(), Self::Error>; } #[macro_export] macro_rules! log { // NEW! ($string:expr) => { unsafe { extern "Rust" { static LOGGER: &'static dyn $crate::GlobalLog; } #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8) } }; ($logger:expr, $string:expr) => {{ #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8) }}; } // NEW! #[macro_export] macro_rules! global_logger { ($logger:expr) => { #[no_mangle] pub static LOGGER: &dyn $crate::GlobalLog = &$logger; }; } }
There's quite a bit to unpack here.
Let's start with the trait.
#![allow(unused)] fn main() { pub trait GlobalLog: Sync { fn log(&self, address: u8); } }
Both GlobalLog
and Log
have a log
method. The difference is that
GlobalLog.log
takes a shared reference to the receiver (&self
). This is
necessary because the global logger will be a static
variable. More on that
later.
The other difference is that GlobalLog.log
doesn't return a Result
. This
means that it can not report errors to the caller. This is not a strict
requirement for traits used to implement global singletons. Error handling in
global singletons is fine but then all users of the global version of the log!
macro have to agree on the error type. Here we are simplifying the interface a
bit by having the GlobalLog
implementer deal with the errors.
Yet another difference is that GlobalLog
requires that the implementer is
Sync
, that is that it can be shared between threads. This is a requirement for
values placed in static
variables; their types must implement the Sync
trait.
At this point it may not be entirely clear why the interface has to look this way. The other parts of the crate will make this clearer so keep reading.
Next up is the log!
macro:
#![allow(unused)] fn main() { ($string:expr) => { unsafe { extern "Rust" { static LOGGER: &'static dyn $crate::GlobalLog; } #[export_name = $string] #[link_section = ".log"] static SYMBOL: u8 = 0; $crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8) } }; }
When called without a specific $logger
the macros uses an extern
static
variable called LOGGER
to log the message. This variable is the global
logger that's defined somewhere else; that's why we use the extern
block. We
saw this pattern in the main interface chapter.
We need to declare a type for LOGGER
or the code won't type check. We don't
know the concrete type of LOGGER
at this point but we know, or rather require,
that it implements the GlobalLog
trait so we can use a trait object here.
The rest of the macro expansion looks very similar to the expansion of the local
version of the log!
macro so I won't explain it here as it's explained in the
previous chapter.
Now that we know that LOGGER
has to be a trait object it's clearer why we
omitted the associated Error
type in GlobalLog
. If we had not omitted then
we would have need to pick a type for Error
in the type signature of LOGGER
.
This is what I earlier meant by "all users of log!
would need to agree on the
error type".
Now the final piece: the global_logger!
macro. It could have been a proc macro
attribute but it's easier to write a macro_rules!
macro.
#![allow(unused)] fn main() { #[macro_export] macro_rules! global_logger { ($logger:expr) => { #[no_mangle] pub static LOGGER: &dyn $crate::GlobalLog = &$logger; }; } }
This macro creates the LOGGER
variable that log!
uses. Because we need a
stable ABI interface we use the no_mangle
attribute. This way the symbol name
of LOGGER
will be "LOGGER" which is what the log!
macro expects.
The other important bit is that the type of this static variable must exactly
match the type used in the expansion of the log!
macro. If they don't match
Bad Stuff will happen due to ABI mismatch.
Let's write an example that uses this new global logger functionality.
$ cat src/main.rs
#![no_main] #![no_std] use cortex_m::interrupt; use cortex_m_semihosting::{ debug, hio::{self, HStdout}, }; use log::{global_logger, log, GlobalLog}; use rt::entry; struct Logger; global_logger!(Logger); entry!(main); fn main() -> ! { log!("Hello, world!"); log!("Goodbye"); debug::exit(debug::EXIT_SUCCESS); loop {} } impl GlobalLog for Logger { fn log(&self, address: u8) { // we use a critical section (`interrupt::free`) to make the access to the // `static mut` variable interrupt safe which is required for memory safety interrupt::free(|_| unsafe { static mut HSTDOUT: Option<HStdout> = None; // lazy initialization if HSTDOUT.is_none() { HSTDOUT = Some(hio::hstdout()?); } let hstdout = HSTDOUT.as_mut().unwrap(); hstdout.write_all(&[address]) }).ok(); // `.ok()` = ignore errors } }
TODO(resources team) use
cortex_m::Mutex
instead of astatic mut
variable whenconst fn
is stabilized.
We had to add cortex-m
to the dependencies.
$ tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
This is a port of one of the examples written in the previous section. The output is the same as what we got back there.
$ cargo run | xxd -p
0001
$ cargo objdump --bin app -- -t | grep '\.log'
00000001 g O .log 00000001 Goodbye
00000000 g O .log 00000001 Hello, world!
Some readers may be concerned about this implementation of global singletons not being zero cost because it uses trait objects which involve dynamic dispatch, that is method calls are performed through a vtable lookup.
However, it appears that LLVM is smart enough to eliminate the dynamic dispatch
when compiling with optimizations / LTO. This can be confirmed by searching for
LOGGER
in the symbol table.
$ cargo objdump --bin app --release -- -t | grep LOGGER
If the static
is missing that means that there is no vtable and that LLVM was
capable of transforming all the LOGGER.log
calls into Logger.log
calls.