Skip to content

Commit

Permalink
Merge #25
Browse files Browse the repository at this point in the history
25: Global singletons r=therealprof a=japaric

This PR depends on #24

r? @rust-embedded/resources
@jamesmunns this is the global singleton pattern I mentioned some time ago on
IRC

Co-authored-by: Jorge Aparicio <jorge@japaric.io>
  • Loading branch information
bors[bot] and japaric committed Sep 24, 2018
2 parents 2ba145c + ba1ceb2 commit a02b816
Show file tree
Hide file tree
Showing 14 changed files with 318 additions and 0 deletions.
15 changes: 15 additions & 0 deletions ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,21 @@ main() {
popd

popd

# # Logging with symbols
pushd singleton

pushd app
diff dev.out \
<(cargo run | xxd -p)
diff dev.objdump \
<(cargo objdump --bin app -- -t | grep '\.log')
diff release.objdump \
<(cargo objdump --bin app --release -- -t | grep LOGGER)
edition_check
popd

popd
}

# checks that 2018 idioms are being used
Expand Down
1 change: 1 addition & 0 deletions ci/singleton/app/.cargo
15 changes: 15 additions & 0 deletions ci/singleton/app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
authors = ["Jorge Aparicio <jorge@japaric.io>"]
edition = "2018"
name = "app"
version = "0.1.0"

[profile.release]
codegen-units = 1
lto = true

[dependencies]
cortex-m = "0.5.7"
cortex-m-semihosting = "0.3.1"
log = { path = "../log" }
rt = { path = "../rt" }
2 changes: 2 additions & 0 deletions ci/singleton/app/dev.objdump
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
00000001 l .log 00000001 Goodbye
00000000 l .log 00000001 Hello, world!
1 change: 1 addition & 0 deletions ci/singleton/app/dev.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0001
Empty file.
46 changes: 46 additions & 0 deletions ci/singleton/app/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#![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
}
}
7 changes: 7 additions & 0 deletions ci/singleton/log/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "log"
version = "0.1.0"
authors = ["Jorge Aparicio <jorge@japaric.io>"]
edition = "2018"

[dependencies]
1 change: 1 addition & 0 deletions ci/singleton/log/build.rs
1 change: 1 addition & 0 deletions ci/singleton/log/log.x
47 changes: 47 additions & 0 deletions ci/singleton/log/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#![no_std]

// 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;
};
}
1 change: 1 addition & 0 deletions ci/singleton/rt
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@
- [Exception handling](./exceptions.md)
- [Assembly on stable](./asm.md)
- [Logging with symbols](./logging.md)
- [Global singletons](./singleton.md)
---
[A note on compiler support](./compiler-support.md)
180 changes: 180 additions & 0 deletions src/singleton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# 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.

``` console
$ cat ../log/src/lib.rs
```

``` rust
{{#include ../ci/singleton/log/src/lib.rs}}
```

There's quite a bit to unpack here.

Let's start with the trait.

``` rust
{{#include ../ci/singleton/log/src/lib.rs:4:6}}
```

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:

``` rust
{{#include ../ci/singleton/log/src/lib.rs:17:29}}
```

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.

[main interface]: /main.html

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.

[previous]: /logging.html

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.

``` rust
{{#include ../ci/singleton/log/src/lib.rs:41:47}}
```

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.

``` console
$ cat src/main.rs
```

``` rust
{{#include ../ci/singleton/app/src/main.rs}}
```

> **TODO**(resources team) use `cortex_m::Mutex` instead of a `static mut`
> variable when `const fn` is stabilized.
We had to add `cortex-m` to the dependencies.

``` console
$ tail -n5 Cargo.toml
```

``` text
{{#include ../ci/singleton/app/Cargo.toml:11:15}}
```

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.

``` console
$ cargo run | xxd -p
```

``` text
{{#include ../ci/singleton/app/dev.out}}
```

``` console
$ cargo objdump --bin app -- -t | grep '\.log'
```

``` text
{{#include ../ci/singleton/app/dev.objdump}}
```

---

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.

``` console
$ cargo objdump --bin app --release -- -t | grep LOGGER
```

``` text
{{#include ../ci/singleton/app/release.objdump}}
```

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.

0 comments on commit a02b816

Please sign in to comment.