enum_dispatch/lib.rs
1//! `enum_dispatch` provides a set of macros that can be used to easily refactor dynamically
2//! dispatched trait accesses to improve their performance by up to 10x.
3//!
4//! Accessing structures through dynamic dispatch is known to have a high runtime cost. Dynamic
5//! dispatch is traditionally used to hide unnecessary type information, improving encapsulation
6//! and making it trivial to add new implementations. However, this hiding of information means
7//! that each time a structure is dynamically accessed, the program must perform a lookup of the
8//! type's information in a virtual table. The extra round-trips to the vtable quickly add up.
9//!
10//! In Rust, dynamic dispatch is done using traits. Rust 2018 adds the `impl` and `dyn` keywords to
11//! make it easier to keep track of instances of dynamic dispatch, but it's not always easy to
12//! avoid it entirely.
13//!
14//! # Feature documentation
15//!
16//! For full documentation of features like generic support, custom variant names, and more, please
17//! check the repository's
18//! [README](https://gitlab.com/antonok/enum_dispatch/-/blob/master/README.md).
19//!
20//! # How it works
21//!
22//! Observe the following example of code describing a user interface with knobs. Each knob can
23//! hold a value between 0.0 and 1.0. Some knobs provide a *linear* range, whereas other knobs
24//! provide a *logarithmic* range.
25//!
26//! ```
27//! trait KnobControl {
28//! fn set_position(&mut self, value: f64);
29//! fn get_value(&self) -> f64;
30//! }
31//!
32//! struct LinearKnob {
33//! position: f64,
34//! }
35//!
36//! struct LogarithmicKnob {
37//! position: f64,
38//! }
39//!
40//! impl KnobControl for LinearKnob {
41//! fn set_position(&mut self, value: f64) {
42//! self.position = value;
43//! }
44//!
45//! fn get_value(&self) -> f64 {
46//! self.position
47//! }
48//! }
49//!
50//! impl KnobControl for LogarithmicKnob {
51//! fn set_position(&mut self, value: f64) {
52//! self.position = value;
53//! }
54//!
55//! fn get_value(&self) -> f64 {
56//! (self.position + 1.).log2()
57//! }
58//! }
59//!
60//! fn main() {
61//! let v: Vec<Box<dyn KnobControl>> = vec![
62//! //set the knobs
63//! ];
64//!
65//! //use the knobs
66//! }
67//! ```
68//!
69//! There are other ways to keep an arbitrarily ordered list of different knob types, but none of
70//! them are quite as simple or easy to maintain. Unfortunately, this implementation uses both heap
71//! allocated `Box`es and dynamic dispatch, which will have performance implications.
72//!
73//! One alternative is to use introduce a new enum type that can hold either a `LinearKnob` or a
74//! `LogarithmicKnob` as a variant, and also implements `KnobControl` by matching on itself and
75//! delegating calls to its variants. This would look like the following:
76//!
77//! ```
78//! # trait KnobControl {
79//! # fn set_position(&mut self, value: f64);
80//! # fn get_value(&self) -> f64;
81//! # }
82//! #
83//! # struct LinearKnob {
84//! # position: f64,
85//! # }
86//! #
87//! # struct LogarithmicKnob {
88//! # position: f64,
89//! # }
90//! #
91//! # impl KnobControl for LinearKnob {
92//! # fn set_position(&mut self, value: f64) {
93//! # self.position = value;
94//! # }
95//! #
96//! # fn get_value(&self) -> f64 {
97//! # self.position
98//! # }
99//! # }
100//! #
101//! # impl KnobControl for LogarithmicKnob {
102//! # fn set_position(&mut self, value: f64) {
103//! # self.position = value;
104//! # }
105//! #
106//! # fn get_value(&self) -> f64 {
107//! # (self.position + 1.).log2()
108//! # }
109//! # }
110//! #
111//! enum Knob {
112//! Linear(LinearKnob),
113//! Logarithmic(LogarithmicKnob),
114//! }
115//!
116//! impl KnobControl for Knob {
117//! fn set_position(&mut self, value: f64) {
118//! match self {
119//! Knob::Linear(inner_knob) => inner_knob.set_position(value),
120//! Knob::Logarithmic(inner_knob) => inner_knob.set_position(value),
121//! }
122//! }
123//!
124//! fn get_value(&self) -> f64 {
125//! match self {
126//! Knob::Linear(inner_knob) => inner_knob.get_value(),
127//! Knob::Logarithmic(inner_knob) => inner_knob.get_value(),
128//! }
129//! }
130//! }
131//! ```
132//!
133//! Performance with this implementation is significantly improved, since all the information the
134//! program could possibly need to know about each knob can be deduced at compile time. Besides
135//! avoiding heap allocations and vtable lookups, this allows the compiler to squeeze out even more
136//! optimization through function inlining.
137//!
138//! However, it's easy to see that the cost of maintaining the source code for this extra structure
139//! is quite high. What happens when we add more knob types? What happens when we add more trait
140//! methods? Even worse, what happens when we do both!
141//!
142//! The resulting code is very repetitive, but that makes it a great target for automatic
143//! generation. The `enum_dispatch` macro can do the automatic generation for you. Examine the code
144//! to generate the same implementation when using `enum_dispatch`.
145//!
146//! ```
147//! # use enum_dispatch::enum_dispatch;
148//! #
149//! #[enum_dispatch]
150//! trait KnobControl {
151//! //...
152//! # fn set_position(&mut self, value: f64);
153//! # fn get_value(&self) -> f64;
154//! }
155//! #
156//! # struct LinearKnob {
157//! # position: f64,
158//! # }
159//! #
160//! # struct LogarithmicKnob {
161//! # position: f64,
162//! # }
163//! #
164//! # impl KnobControl for LinearKnob {
165//! # fn set_position(&mut self, value: f64) {
166//! # self.position = value;
167//! # }
168//! #
169//! # fn get_value(&self) -> f64 {
170//! # self.position
171//! # }
172//! # }
173//! #
174//! # impl KnobControl for LogarithmicKnob {
175//! # fn set_position(&mut self, value: f64) {
176//! # self.position = value;
177//! # }
178//! #
179//! # fn get_value(&self) -> f64 {
180//! # (self.position + 1.).log2()
181//! # }
182//! # }
183//!
184//! #[enum_dispatch(KnobControl)]
185//! enum Knob {
186//! LinearKnob,
187//! LogarithmicKnob,
188//! }
189//! ```
190//!
191//! That's it. `enum_dispatch` will also automatically generate implementations of
192//! `std::convert::From` for each enum variant, so that new `Knob`s can be created without concern
193//! for the names of each enum variant.
194//!
195//! ```
196//! # use enum_dispatch::enum_dispatch;
197//! #
198//! # #[enum_dispatch]
199//! # trait KnobControl {
200//! # fn set_position(&mut self, value: f64);
201//! # fn get_value(&self) -> f64;
202//! # }
203//! #
204//! # struct LinearKnob {
205//! # position: f64,
206//! # }
207//! #
208//! # struct LogarithmicKnob {
209//! # position: f64,
210//! # }
211//! #
212//! # impl KnobControl for LinearKnob {
213//! # fn set_position(&mut self, value: f64) {
214//! # self.position = value;
215//! # }
216//! #
217//! # fn get_value(&self) -> f64 {
218//! # self.position
219//! # }
220//! # }
221//! #
222//! # impl KnobControl for LogarithmicKnob {
223//! # fn set_position(&mut self, value: f64) {
224//! # self.position = value;
225//! # }
226//! #
227//! # fn get_value(&self) -> f64 {
228//! # (self.position + 1.).log2()
229//! # }
230//! # }
231//! #
232//! # #[enum_dispatch(KnobControl)]
233//! # enum Knob {
234//! # LinearKnob,
235//! # LogarithmicKnob,
236//! # }
237//! #
238//! # fn some_existing_knobs() -> (LinearKnob, LogarithmicKnob) {
239//! # (LinearKnob { position: 0.5 }, LogarithmicKnob { position: 0.5 })
240//! # }
241//! #
242//! let (a_linear_knob, a_logarithmic_knob) = some_existing_knobs();
243//!
244//! let knob = Knob::from(a_linear_knob);
245//! let knob = Knob::from(a_logarithmic_knob);
246//! ```
247//!
248//! # Performance
249//!
250//! The `benches` directory contains three benchmarks of different natures, each comparing four
251//! different methods of accessing a traited struct of an arbitrary type. The four methods are as
252//! follows:
253//!
254//! | test name | explanation |
255//! |--------------|-----------------------------------------------------------------------------------------|
256//! | boxdyn | The easiest way to access a struct, using a heap allocation and dynamic dispatch. |
257//! | refdyn | Accesses the struct by reference, but still using dynamic dispatch. No heap allocation. |
258//! | customderive | Uses a similar macro approach from the external [`enum_derive`](https://github.com/DanielKeep/rust-custom-derive) crate, which implements a method that returns an inner type as a dynamic trait object. |
259//! | enumdispatch | Implemented using this crate. |
260//!
261//! ## The benchmarks
262//!
263//! The following benchmark results were measured on a Ryzen 7 2700x CPU.
264//!
265//! ### compiler_optimized
266//!
267//! The first set of benchmarks creates trait objects and measures the speed of accessing a method
268//! on them.
269//!
270//! ```text
271//! test benches::boxdyn_compiler_optimized ... bench: 2,135,418 ns/iter (+/- 12,575)
272//! test benches::customderive_compiler_optimized ... bench: 2,611,860 ns/iter (+/- 18,644)
273//! test benches::enumdispatch_compiler_optimized ... bench: 0 ns/iter (+/- 0)
274//! test benches::refdyn_compiler_optimized ... bench: 2,132,591 ns/iter (+/- 22,114)
275//! ```
276//!
277//! It's easy to see that `enum_dispatch` is the clear winner here!
278//!
279//! Ok, fine. This wasn't a fair test. The compiler is able to "look through" the trait method call
280//! in the enum_dispatch case, notices that the result is unused, and removes it as an
281//! optimization. However, this still highlights an important property of `enum_dispatch`ed types:
282//! the compiler is able to infer much better optimizations when possible.
283//!
284//! ### blackbox
285//!
286//! The next set of benchmarks uses the `test::black_box` method to hide the fact that the result
287//! of the method is unused.
288//!
289//! ```text
290//! test benches::boxdyn_blackbox ... bench: 2,131,736 ns/iter (+/- 24,937)
291//! test benches::customderive_blackbox ... bench: 2,611,721 ns/iter (+/- 23,502)
292//! test benches::enumdispatch_blackbox ... bench: 471,740 ns/iter (+/- 1,439)
293//! test benches::refdyn_blackbox ... bench: 2,131,978 ns/iter (+/- 21,547)
294//! ```
295//!
296//! The competitors faced virtually no impact, whereas `enum_dispatch` takes the full force of the
297//! `black_box` call. This test shows the power that avoiding dynamic dispatch gives to the
298//! compiler in the context of the previous test, but also demonstrates how much faster
299//! `enum_dispatch` is in real code: almost 5 times faster than the closest alternative.
300//!
301//! ### homogenous_vec
302//!
303//! The final set of benchmarks puts 1024 traited structs of arbitrary types at random into a `Vec`
304//! and measures the time it takes to successively iterate over the entire `Vec`, calling
305//! `black_box`ed methods on each element.
306//!
307//! ```text
308//! test benches::boxdyn_homogeneous_vec ... bench: 5,900,191 ns/iter (+/- 95,169)
309//! test benches::customderive_homogeneous_vec ... bench: 4,831,808 ns/iter (+/- 140,437)
310//! test benches::enumdispatch_homogeneous_vec ... bench: 479,630 ns/iter (+/- 3,531)
311//! test benches::refdyn_homogeneous_vec ... bench: 5,658,461 ns/iter (+/- 137,128)
312//! ```
313//!
314//! This might be one of the most likely use cases for traited structs of arbitrary types, and it's
315//! where `enum_dispatch` really shines. Since a `Vec` of `enum_dispatch` objects is actually a
316//! `Vec` of enums rather than addresses, accessing an element takes half the indirection of the
317//! other techniques. Add that to the lack of vtable accesses, and we have a result that is 10
318//! times faster than the closest alternative, and almost 12 times faster than the best technique
319//! from the standard library.
320
321#![doc(
322 html_logo_url = "https://gitlab.com/antonok/enum_dispatch/raw/master/enum_dispatch.svg",
323 html_favicon_url = "https://gitlab.com/antonok/enum_dispatch/raw/master/enum_dispatch.svg"
324)]
325
326extern crate proc_macro;
327
328use proc_macro2::TokenStream;
329use quote::{ToTokens, TokenStreamExt};
330
331/// Used for converting a macro input into an ItemTrait or an EnumDispatchItem.
332mod attributed_parser;
333/// Provides local storage for enum and trait definitions so that they can be accessed later.
334mod cache;
335/// Provides a custom syntax specification for the arguments to an `#[enum_dispatch(...)]` attribute.
336mod enum_dispatch_arg_list;
337/// Provides a custom syntax specification for enum dispatch syntax blocks.
338mod enum_dispatch_item;
339/// Provides a custom syntax specification for the variants of enum dispatch syntax blocks.
340mod enum_dispatch_variant;
341/// Provides utilities for building enum dispatch implementations.
342mod expansion;
343/// Convenience trait for token parsing.
344mod filter_attrs;
345/// Codifies the kinds of generic arguments supported in an `#[enum_dispatch(T<...>)]` attribute.
346mod supported_generics;
347/// Convenience methods for constructing `syn` types.
348mod syn_utils;
349
350use crate::expansion::add_enum_impls;
351use crate::supported_generics::{convert_to_supported_generic, num_supported_generics};
352
353/// Annotating a trait or enum definition with an `#[enum_dispatch]` attribute will register it
354/// with the enum_dispatch library, allowing it to be used to generate impl blocks elsewhere.
355///
356/// Annotating a trait or enum definition with an `#[enum_dispatch(BlockName)]` attribute will
357/// generate an enum dispatch implementation for the specified trait/enum pair, as well as adding
358/// an impl of std::convert::From for each variant. When annotating a trait, BlockName should be the
359/// name of a registered enum. When annotating an enum, BlockName should be the name of a registered
360/// trait.
361///
362/// An annotated enum should have variants that are simply the names of types imported to the
363/// current scope. To force individual variants to use a custom name when expanded, each variant
364/// can also take the form of a normal tuple-style enum variant with a single field.
365#[proc_macro_attribute]
366pub fn enum_dispatch(attr: proc_macro::TokenStream, item: proc_macro::TokenStream) -> proc_macro::TokenStream {
367 enum_dispatch2(attr.into(), item.into()).into()
368}
369
370/// `proc_macro2::TokenStream` compatible version of the `enum_dispatch` function.
371///
372/// Using only `proc_macro2::TokenStream` inside the entire crate makes methods unit-testable and
373/// removes the need for conversions everywhere.
374fn enum_dispatch2(attr: TokenStream, item: TokenStream) -> TokenStream {
375 let new_block = attributed_parser::parse_attributed(item.clone()).unwrap();
376 let mut expanded = match &new_block {
377 attributed_parser::ParsedItem::Trait(traitdef) => {
378 cache::cache_trait(traitdef.to_owned());
379 item
380 }
381 attributed_parser::ParsedItem::EnumDispatch(enumdef) => {
382 cache::cache_enum_dispatch(enumdef.clone());
383 syn::ItemEnum::from(enumdef.to_owned())
384 .into_token_stream()
385 }
386 };
387 // If the attributes are non-empty, the new block should be "linked" to the listed definitions.
388 // Those definitions may or may not have been cached yet.
389 // If one is not cached yet, the link will be pushed into the cache, and impl generation will
390 // be deferred until the missing definition is encountered.
391 // For now, we assume it is already cached.
392 if !attr.is_empty() {
393 let attr_parse_result = syn::parse2::<enum_dispatch_arg_list::EnumDispatchArgList>(attr)
394 .expect("Could not parse arguments to `#[enum_dispatch(...)]`.")
395 .arg_list
396 .into_iter()
397 .try_for_each(|p| {
398 if p.leading_colon.is_some() || p.segments.len() != 1 {
399 panic!("Paths in `#[enum_dispatch(...)]` are not supported.");
400 }
401 let syn::PathSegment {
402 ident: attr_name,
403 arguments: attr_generics
404 } = p.segments.last().unwrap();
405 let attr_generics = match attr_generics.clone() {
406 syn::PathArguments::None => vec![],
407 syn::PathArguments::AngleBracketed(args) => {
408 assert!(args.colon2_token.is_none());
409 match args.args.iter().map(convert_to_supported_generic).collect::<Result<Vec<_>, _>>() {
410 Ok(v) => v,
411 Err((unsupported, span)) => {
412 let error_string = unsupported.to_string();
413 return Err(quote::quote_spanned! {span=>
414 compile_error!(#error_string)
415 });
416 }
417 }
418 }
419 syn::PathArguments::Parenthesized(_) => panic!("Expected angle bracketed generic arguments, found parenthesized arguments"),
420 };
421 match &new_block {
422 attributed_parser::ParsedItem::Trait(traitdef) => {
423 let supported_generics = num_supported_generics(&traitdef.generics);
424 cache::defer_link((attr_name, attr_generics.len()), (&traitdef.ident, supported_generics))
425 }
426 attributed_parser::ParsedItem::EnumDispatch(enumdef) => {
427 let supported_generics = num_supported_generics(&enumdef.generics);
428 cache::defer_link((attr_name, attr_generics.len()), (&enumdef.ident, supported_generics))
429 }
430 }
431 Ok(())
432 });
433 if let Err(e) = attr_parse_result {
434 return e;
435 }
436 };
437 // It would be much simpler to just always retrieve all definitions from the cache. However,
438 // span information is not stored in the cache. Saving the newly retrieved definition prevents
439 // *all* of the span information from being lost.
440 match new_block {
441 attributed_parser::ParsedItem::Trait(traitdef) => {
442 let supported_generics = num_supported_generics(&traitdef.generics);
443 let additional_enums =
444 cache::fulfilled_by_trait(&traitdef.ident, supported_generics);
445 for enumdef in additional_enums {
446 expanded.append_all(add_enum_impls(enumdef, traitdef.clone()));
447 }
448 }
449 attributed_parser::ParsedItem::EnumDispatch(enumdef) => {
450 let supported_generics = num_supported_generics(&enumdef.generics);
451 let additional_traits =
452 cache::fulfilled_by_enum(&enumdef.ident, supported_generics);
453 for traitdef in additional_traits {
454 expanded.append_all(add_enum_impls(enumdef.clone(), traitdef));
455 }
456 }
457 }
458 expanded
459}