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