2 releases
0.1.1 | Mar 11, 2022 |
---|---|
0.1.0 | Mar 11, 2022 |
#486 in Procedural macros
74KB
1.5K
SLoC
Provides a macro for implementing functions with multiple dynamic argument dispatch at runtime.
The [double_dyn!] macro will define the specified trait(s) and emit implementations for all of the provided types, and then emit functions that call the appropriate implementation.
Usage
In your Cargo.toml
[dependencies]
double-dyn = "0.1.1"
Basics
The double_dyn!
macro invocation has 3 parts.
- Trait names for the
A
andB
traits, along with any subtrait bounds - Function prototypes
- Implementations for type pairs, in the form
<A, B>
Examples
use double_dyn::double_dyn;
double_dyn!{
type A: MyTraitA;
type B: MyTraitB: std::fmt::Display;
fn multiply(a: &dyn MyTraitA, b: &dyn MyTraitB) -> Box<dyn MyTraitB>;
impl for <i32, String>
{
fn multiply(a: &i32, b: &String) -> Box<dyn MyTraitB> {
let multiplied_val = *a * b.parse::<i32>().unwrap();
Box::new(multiplied_val.to_string())
}
}
impl for <[i8, i16, i32, i64, i128], [f32, f64]>
{
fn multiply(a: &#A, b: &#B) -> Box<dyn MyTraitB> {
Box::new((*a as #B) * *b)
}
}
}
let val = multiply(&2, &7.5);
assert_eq!(format!("{}", val), "15");
This macro invocation above will define the MyTraitA
and MyTraitB
traits, and provide implementations for all of the relevant types.
As you can see above, multiple A
and/or B
types may be specified in using a list in [square brackets]
.
You may use the concrete types explicitly Within the impl
block, or alternatively, #A
and #B
markers can be used as aliases within the function signature and implementation body, and they will be replaced by the type(s) they represent at compile time.
# use double_dyn::double_dyn;
double_dyn!{
type A: MyTrait: std::fmt::Display;
type B: MyTrait;
fn multiply(a: &dyn MyTrait, b: &dyn MyTrait) -> Box<dyn MyTrait>;
#[commutative]
impl for <[i8, i16, i32, i64, i128], [f32, f64]>
{
fn multiply(a: &#A, b: &#B) -> Box<dyn MyTrait> {
Box::new((*a as #B) * *b)
}
}
}
let val = multiply(&7.0, &2);
assert_eq!(format!("{}", val), "14");
The same trait may be supplied for both A
and B
. The A
and B
arguments may still be of different types within the implementation, however. The macro will attempt to infer which argument is A
and which is B
from the use of the #A
or #B
markers but will assume the first &dyn MyTrait
argument is A
if it is ambiguous.
The #[commutative]
attribute will cause an additional implementation to be generated where A
is replaced by B
and vice-versa.
In the case where the A
and B
trait is the same, the bounds from the A
trait take precedence.
You may declare multiple functions within the same double_dyn
macro invocation, and all functions will use the same trait(s). However, every declared function must be implemented in each impl
block.
Additional usage examples can be found here in the tests.
Limitations
-
All
impls
must be in the samedouble_dyn
macro invocation along with the definitions. I'd like to be able to support separating declarations from implementations and allow additionalimpls
to be added as appropriate, but I don't have a robust method to communicate between each macro invocation. This is blocked on this issue. -
Each
double_dyn
macro invocation defines a trait or pair of traits. This macro isn't designed to add methods to existing traits. It is possible to use this macro to define a trait, and then make that trait a supertrait of another trait you define, thus allowing double-dyn methods on your trait. But the lack of trait upcasting in the stable compiler is still limiting. Please contact me if you have an idea for how to make things better for adding methods to existing traits. -
Functions may not have generic arguments. This is a fundamental limitation based on the fact that functions are transformed into trait methods, and the traits need to remain object-safe.
-
impl
s don't support generic "blanket implementations".A
types can never support generic types for the same reason as above; object-safety forbids generics in trait methods.B
types could theoretically support blanket implementations but currently the macro doesn't parsewhere
clauses in theimpl
s. Please let me know if this feature is important to you, and I can add it. -
visibility qualifiers, e.g.
pub
, must be the same for every function prototype. The visibility will be applied to all generated traits and functions. -
Passing owned args isn't supported. For example, an arg must be of the form
&dyn ATrait
, as opposed toBox<dyn ATrait>
. -
Some errors and warnings may be reported multiple times.
Future Vision
I would like to allow the addition of new function implementations via impl
blocks that aren't part of the original invocation. In other words, to allow the function signatures to be in part of the code, and allow additional implementations to be added elsewhere. Unfortunately I don't believe this is possible on account of Rust not having an ability to communicate between macro invocations. This is discussed here.
I would also like to include more flexibility for implementing methods on existing traits. See the Limitations section above. I am open to suggestions about what you would find useful.
Acknowledgments
This crate implements a strategy proposed by @h2co3 in this thread.
I learned how to write proc macros by studying the code of @dtolnay, and I borrowed some utility functions from the seq-macro crate.
Dependencies
~120KB