Skip to content

Commit a02b816

Browse files
bors[bot]japaric
andcommitted
Merge #25
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 <[email protected]>
2 parents 2ba145c + ba1ceb2 commit a02b816

File tree

14 files changed

+318
-0
lines changed

14 files changed

+318
-0
lines changed

ci/script.sh

+15
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ main() {
194194
popd
195195

196196
popd
197+
198+
# # Logging with symbols
199+
pushd singleton
200+
201+
pushd app
202+
diff dev.out \
203+
<(cargo run | xxd -p)
204+
diff dev.objdump \
205+
<(cargo objdump --bin app -- -t | grep '\.log')
206+
diff release.objdump \
207+
<(cargo objdump --bin app --release -- -t | grep LOGGER)
208+
edition_check
209+
popd
210+
211+
popd
197212
}
198213

199214
# checks that 2018 idioms are being used

ci/singleton/app/.cargo

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../logging/app2/.cargo

ci/singleton/app/Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
authors = ["Jorge Aparicio <[email protected]>"]
3+
edition = "2018"
4+
name = "app"
5+
version = "0.1.0"
6+
7+
[profile.release]
8+
codegen-units = 1
9+
lto = true
10+
11+
[dependencies]
12+
cortex-m = "0.5.7"
13+
cortex-m-semihosting = "0.3.1"
14+
log = { path = "../log" }
15+
rt = { path = "../rt" }

ci/singleton/app/dev.objdump

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
00000001 l .log 00000001 Goodbye
2+
00000000 l .log 00000001 Hello, world!

ci/singleton/app/dev.out

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0001

ci/singleton/app/release.objdump

Whitespace-only changes.

ci/singleton/app/src/main.rs

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#![no_main]
2+
#![no_std]
3+
4+
use cortex_m::interrupt;
5+
use cortex_m_semihosting::{
6+
debug,
7+
hio::{self, HStdout},
8+
};
9+
10+
use log::{global_logger, log, GlobalLog};
11+
use rt::entry;
12+
13+
struct Logger;
14+
15+
global_logger!(Logger);
16+
17+
entry!(main);
18+
19+
fn main() -> ! {
20+
log!("Hello, world!");
21+
22+
log!("Goodbye");
23+
24+
debug::exit(debug::EXIT_SUCCESS);
25+
26+
loop {}
27+
}
28+
29+
impl GlobalLog for Logger {
30+
fn log(&self, address: u8) {
31+
// we use a critical section (`interrupt::free`) to make the access to the
32+
// `static mut` variable interrupt safe which is required for memory safety
33+
interrupt::free(|_| unsafe {
34+
static mut HSTDOUT: Option<HStdout> = None;
35+
36+
// lazy initialization
37+
if HSTDOUT.is_none() {
38+
HSTDOUT = Some(hio::hstdout()?);
39+
}
40+
41+
let hstdout = HSTDOUT.as_mut().unwrap();
42+
43+
hstdout.write_all(&[address])
44+
}).ok(); // `.ok()` = ignore errors
45+
}
46+
}

ci/singleton/log/Cargo.toml

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[package]
2+
name = "log"
3+
version = "0.1.0"
4+
authors = ["Jorge Aparicio <[email protected]>"]
5+
edition = "2018"
6+
7+
[dependencies]

ci/singleton/log/build.rs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../logging/log/build.rs

ci/singleton/log/log.x

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../logging/log/log.x

ci/singleton/log/src/lib.rs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
#![no_std]
2+
3+
// NEW!
4+
pub trait GlobalLog: Sync {
5+
fn log(&self, address: u8);
6+
}
7+
8+
pub trait Log {
9+
type Error;
10+
11+
fn log(&mut self, address: u8) -> Result<(), Self::Error>;
12+
}
13+
14+
#[macro_export]
15+
macro_rules! log {
16+
// NEW!
17+
($string:expr) => {
18+
unsafe {
19+
extern "Rust" {
20+
static LOGGER: &'static dyn $crate::GlobalLog;
21+
}
22+
23+
#[export_name = $string]
24+
#[link_section = ".log"]
25+
static SYMBOL: u8 = 0;
26+
27+
$crate::GlobalLog::log(LOGGER, &SYMBOL as *const u8 as usize as u8)
28+
}
29+
};
30+
31+
($logger:expr, $string:expr) => {{
32+
#[export_name = $string]
33+
#[link_section = ".log"]
34+
static SYMBOL: u8 = 0;
35+
36+
$crate::Log::log(&mut $logger, &SYMBOL as *const u8 as usize as u8)
37+
}};
38+
}
39+
40+
// NEW!
41+
#[macro_export]
42+
macro_rules! global_logger {
43+
($logger:expr) => {
44+
#[no_mangle]
45+
pub static LOGGER: &dyn $crate::GlobalLog = &$logger;
46+
};
47+
}

ci/singleton/rt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../logging/rt

src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@
77
- [Exception handling](./exceptions.md)
88
- [Assembly on stable](./asm.md)
99
- [Logging with symbols](./logging.md)
10+
- [Global singletons](./singleton.md)
1011
---
1112
[A note on compiler support](./compiler-support.md)

src/singleton.md

+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Global singletons
2+
3+
In this section we'll cover how to implement a global, shared singleton. The
4+
embedded Rust book covered local, owned singletons which are pretty much unique
5+
to Rust. Global singletons are essentially the singleton pattern you see in C
6+
and C++; they are not specific to embedded development but since they involve
7+
symbols they seemed a good fit for the embedonomicon.
8+
9+
> **TODO**(resources team) link "the embedded Rust book" to the singletons
10+
> section when it's up
11+
12+
To illustrate this section we'll extend the logger we developed in the last
13+
section to support global logging. The result will be very similar to the
14+
`#[global_allocator]` feature covered in the embedded Rust book.
15+
16+
> **TODO**(resources team) link `#[global_allocator]` to the collections chapter
17+
> of the book when it's in a more stable location.
18+
19+
Here's the summary of what we want to:
20+
21+
In the last section we created a `log!` macro to log messages through a specific
22+
logger, a value that implements the `Log` trait. The syntax of the `log!` macro
23+
is `log!(logger, "String")`. We want to extend the macro such that
24+
`log!("String")` also works. Using the `logger`-less version should log the
25+
message through a global logger; this is how `std::println!` works. We'll also
26+
need a mechanism to declare what the global logger is; this is the part that's
27+
similar to `#[global_allocator]`.
28+
29+
It could be that the global logger is declared in the top crate and it could
30+
also be that the type of the global logger is defined in the top crate. In this
31+
scenario the dependencies can *not* know the exact type of the global logger. To
32+
support this scenario we'll need some indirection.
33+
34+
Instead of hardcoding the type of the global logger in the `log` crate we'll
35+
declare only the *interface* of the global logger in that crate. That is we'll
36+
add a new trait, `GlobalLog`, to the `log` crate. The `log!` macro will also
37+
have to make use of that trait.
38+
39+
``` console
40+
$ cat ../log/src/lib.rs
41+
```
42+
43+
``` rust
44+
{{#include ../ci/singleton/log/src/lib.rs}}
45+
```
46+
47+
There's quite a bit to unpack here.
48+
49+
Let's start with the trait.
50+
51+
``` rust
52+
{{#include ../ci/singleton/log/src/lib.rs:4:6}}
53+
```
54+
55+
Both `GlobalLog` and `Log` have a `log` method. The difference is that
56+
`GlobalLog.log` takes a shared reference to the receiver (`&self`). This is
57+
necessary because the global logger will be a `static` variable. More on that
58+
later.
59+
60+
The other difference is that `GlobalLog.log` doesn't return a `Result`. This
61+
means that it can *not* report errors to the caller. This is not a strict
62+
requirement for traits used to implement global singletons. Error handling in
63+
global singletons is fine but then all users of the global version of the `log!`
64+
macro have to agree on the error type. Here we are simplifying the interface a
65+
bit by having the `GlobalLog` implementer deal with the errors.
66+
67+
Yet another difference is that `GlobalLog` requires that the implementer is
68+
`Sync`, that is that it can be shared between threads. This is a requirement for
69+
values placed in `static` variables; their types must implement the `Sync`
70+
trait.
71+
72+
At this point it may not be entirely clear why the interface has to look this
73+
way. The other parts of the crate will make this clearer so keep reading.
74+
75+
Next up is the `log!` macro:
76+
77+
``` rust
78+
{{#include ../ci/singleton/log/src/lib.rs:17:29}}
79+
```
80+
81+
When called without a specific `$logger` the macros uses an `extern` `static`
82+
variable called `LOGGER` to log the message. This variable *is* the global
83+
logger that's defined somewhere else; that's why we use the `extern` block. We
84+
saw this pattern in the [main interface] chapter.
85+
86+
[main interface]: /main.html
87+
88+
We need to declare a type for `LOGGER` or the code won't type check. We don't
89+
know the concrete type of `LOGGER` at this point but we know, or rather require,
90+
that it implements the `GlobalLog` trait so we can use a trait object here.
91+
92+
The rest of the macro expansion looks very similar to the expansion of the local
93+
version of the `log!` macro so I won't explain it here as it's explained in the
94+
[previous] chapter.
95+
96+
[previous]: /logging.html
97+
98+
Now that we know that `LOGGER` has to be a trait object it's clearer why we
99+
omitted the associated `Error` type in `GlobalLog`. If we had not omitted then
100+
we would have need to pick a type for `Error` in the type signature of `LOGGER`.
101+
This is what I earlier meant by "all users of `log!` would need to agree on the
102+
error type".
103+
104+
Now the final piece: the `global_logger!` macro. It could have been a proc macro
105+
attribute but it's easier to write a `macro_rules!` macro.
106+
107+
``` rust
108+
{{#include ../ci/singleton/log/src/lib.rs:41:47}}
109+
```
110+
111+
This macro creates the `LOGGER` variable that `log!` uses. Because we need a
112+
stable ABI interface we use the `no_mangle` attribute. This way the symbol name
113+
of `LOGGER` will be "LOGGER" which is what the `log!` macro expects.
114+
115+
The other important bit is that the type of this static variable must exactly
116+
match the type used in the expansion of the `log!` macro. If they don't match
117+
Bad Stuff will happen due to ABI mismatch.
118+
119+
Let's write an example that uses this new global logger functionality.
120+
121+
``` console
122+
$ cat src/main.rs
123+
```
124+
125+
``` rust
126+
{{#include ../ci/singleton/app/src/main.rs}}
127+
```
128+
129+
> **TODO**(resources team) use `cortex_m::Mutex` instead of a `static mut`
130+
> variable when `const fn` is stabilized.
131+
132+
We had to add `cortex-m` to the dependencies.
133+
134+
``` console
135+
$ tail -n5 Cargo.toml
136+
```
137+
138+
``` text
139+
{{#include ../ci/singleton/app/Cargo.toml:11:15}}
140+
```
141+
142+
This is a port of one of the examples written in the [previous] section. The
143+
output is the same as what we got back there.
144+
145+
``` console
146+
$ cargo run | xxd -p
147+
```
148+
149+
``` text
150+
{{#include ../ci/singleton/app/dev.out}}
151+
```
152+
153+
``` console
154+
$ cargo objdump --bin app -- -t | grep '\.log'
155+
```
156+
157+
``` text
158+
{{#include ../ci/singleton/app/dev.objdump}}
159+
```
160+
161+
---
162+
163+
Some readers may be concerned about this implementation of global singletons not
164+
being zero cost because it uses trait objects which involve dynamic dispatch,
165+
that is method calls are performed through a vtable lookup.
166+
167+
However, it appears that LLVM is smart enough to eliminate the dynamic dispatch
168+
when compiling with optimizations / LTO. This can be confirmed by searching for
169+
`LOGGER` in the symbol table.
170+
171+
``` console
172+
$ cargo objdump --bin app --release -- -t | grep LOGGER
173+
```
174+
175+
``` text
176+
{{#include ../ci/singleton/app/release.objdump}}
177+
```
178+
179+
If the `static` is missing that means that there is no vtable and that LLVM was
180+
capable of transforming all the `LOGGER.log` calls into `Logger.log` calls.

0 commit comments

Comments
 (0)