Skip to content

Commit afef680

Browse files
authored
Merge pull request #117 from derekdreery/macro_migration
Add notes on migrating crates to work with both editions
2 parents b16de5d + 54ed43e commit afef680

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ before_script:
55
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
66
- (test -x $HOME/.cargo/bin/mdbook || cargo install mdbook)
77
- cargo install-update -a
8+
- mdbook --version
89
script:
910
- mdbook build
1011
- mdbook test

src/rust-2018/macros/macro-changes.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
![Minimum Rust version: beta](https://img.shields.io/badge/Minimum%20Rust%20Version-beta-orange.svg)
44

5+
## `macro_rules!` style macros
6+
57
In Rust 2018, you can import specific macros from external crates via `use`
68
statements, rather than the old `#[macro_use]` attribute.
79

@@ -78,3 +80,192 @@ struct Bar;
7880

7981
This only works for macros defined in external crates.
8082
For macros defined locally, `#[macro_use] mod foo;` is still required, as it was in Rust 2015.
83+
84+
### Local helper macros
85+
86+
Sometimes it is helpful or necessary to have helper macros inside your module. This can make
87+
supporting both versions of rust more complicated.
88+
89+
For example, let's make a simplified (and slightly contrived) version of the `log` crate in 2015
90+
edition style:
91+
92+
```rust
93+
use std::fmt;
94+
95+
/// How important/severe the log message is.
96+
#[derive(Copy, Clone)]
97+
pub enum LogLevel {
98+
Warn,
99+
Error
100+
}
101+
102+
impl fmt::Display for LogLevel {
103+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
104+
match self {
105+
LogLevel::Warn => write!(f, "warning"),
106+
LogLevel::Error => write!(f, "error"),
107+
}
108+
}
109+
}
110+
111+
// A helper macro to log the message.
112+
#[doc(hidden)]
113+
#[macro_export]
114+
macro_rules! __impl_log {
115+
($level:expr, $msg:expr) => {{
116+
println!("{}: {}", $level, $msg)
117+
}}
118+
}
119+
120+
/// Warn level log message
121+
#[macro_export]
122+
macro_rules! warn {
123+
($($args:tt)*) => {
124+
__impl_log!($crate::LogLevel::Warn, format_args!($($args)*))
125+
}
126+
}
127+
128+
/// Error level log message
129+
#[macro_export]
130+
macro_rules! error {
131+
($($args:tt)*) => {
132+
__impl_log!($crate::LogLevel::Error, format_args!($($args)*))
133+
}
134+
}
135+
```
136+
137+
Our `__impl_log!` macro is private to our module, but needs to be exported as it is called by other
138+
macros, and in 2015 edition all used macros must be exported.
139+
140+
Now, in 2018 this example will not compile:
141+
142+
```rust,ignore
143+
use log::error;
144+
145+
fn main() {
146+
error!("error message");
147+
}
148+
```
149+
150+
will give an error message about not finding the `__impl_log!` macro. This is because unlike in
151+
the 2015 edition, macros are namespaced and we must import them. We could do
152+
153+
```rust,ignore
154+
use log::{__impl_log, error};
155+
```
156+
157+
which would make our code compile, but `__impl_log` is meant to be an implementation detail!
158+
159+
#### Macros with `$crate::` prefix.
160+
161+
The cleanest way to handle this situation is to use the `$crate::` prefix for macros, the same as
162+
you would for any other path. Versions of the compiler >= 1.30 will handle this in both editions:
163+
164+
```rust
165+
macro_rules! warn {
166+
($($args:tt)*) => {
167+
$crate::__impl_log!($crate::LogLevel::Warn, format_args!($($args)*))
168+
}
169+
}
170+
171+
// ...
172+
```
173+
174+
However, this will not work for older versions of the compiler that don't understand the
175+
`$crate::` prefix for macros.
176+
177+
#### Macros using `local_inner_macros`
178+
179+
We also have the `local_inner_macros` modifier that we can add to our `#[macro_export]` attribute.
180+
This has the advantage of working with older rustc versions (older versions just ignore the extra
181+
modifier). The downside is that it's a bit messier:
182+
183+
```rust,ignore
184+
#[macro_export(local_inner_macros)]
185+
macro_rules! warn {
186+
($($args:tt)*) => {
187+
__impl_log!($crate::LogLevel::Warn, format_args!($($args)*))
188+
}
189+
}
190+
```
191+
192+
So the code knows to look for any macros used locally. But wait - this won't compile, because we
193+
use the `format_args!` macro that isn't in our local crate (hence the convoluted example). The
194+
solution is to add a level of indirection: we create a macro that wraps `format_args`, but is local
195+
to our crate. That way everything works in both editions (sadly we have to pollute the global
196+
namespace a bit, but that's ok).
197+
198+
```rust
199+
// I've used the pattern `_<my crate name>__<macro name>` to name this macro, hopefully avoiding
200+
// name clashes.
201+
#[doc(hidden)]
202+
#[macro_export]
203+
macro_rules! _log__format_args {
204+
($($inner:tt)*) => {
205+
format_args! { $($inner)* }
206+
}
207+
}
208+
```
209+
210+
Here we're using the most general macro pattern possible, a list of token trees. We just pass
211+
whatever tokens we get to the inner macro, and rely on it to report errors.
212+
213+
So the full 2015/2018 working example would be:
214+
215+
```rust
216+
use std::fmt;
217+
218+
/// How important/severe the log message is.
219+
#[derive(Debug, Copy, Clone)]
220+
pub enum LogLevel {
221+
Warn,
222+
Error
223+
}
224+
225+
impl fmt::Display for LogLevel {
226+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
227+
match self {
228+
LogLevel::Warn => write!(f, "warning"),
229+
LogLevel::Error => write!(f, "error"),
230+
}
231+
}
232+
}
233+
234+
// A helper macro to log the message.
235+
#[doc(hidden)]
236+
#[macro_export]
237+
macro_rules! __impl_log {
238+
($level:expr, $msg:expr) => {{
239+
println!("{}: {}", $level, $msg)
240+
}}
241+
}
242+
243+
/// Warn level log message
244+
#[macro_export(local_inner_macros)]
245+
macro_rules! warn {
246+
($($args:tt)*) => {
247+
__impl_log!($crate::LogLevel::Warn, _log__format_args!($($args)*))
248+
}
249+
}
250+
251+
/// Error level log message
252+
#[macro_export(local_inner_macros)]
253+
macro_rules! error {
254+
($($args:tt)*) => {
255+
__impl_log!($crate::LogLevel::Error, _log__format_args!($($args)*))
256+
}
257+
}
258+
259+
#[doc(hidden)]
260+
#[macro_export]
261+
macro_rules! _log__format_args {
262+
($($inner:tt)*) => {
263+
format_args! { $($inner)* }
264+
}
265+
}
266+
```
267+
268+
Once everyone is using a rustc version >= 1.30, we can all just use the `$crate::` method (2015
269+
crates are guaranteed to carry on compiling fine with later versions of the compiler). We need to
270+
wait for package managers and larger organisations to update their compilers before this happens,
271+
so in the mean time we can use the `local_inner_macros` method to support everybody. :)

0 commit comments

Comments
 (0)