Drop the peripheral_trait!
macro
#179
Labels
➕ improvement
Chores and fixes: the small things.
🔧 infra
Project infrastructure, meta, dev-ex, etc.
P-medium
Medium priority
T-peripheral traits
Topic: Peripheral Traits
what
Background
I.e. this macro:
core/traits/src/peripherals/mod.rs
Lines 196 to 358 in 5e0f0eb
which is invoked on all the peripheral trait definitions:
core/traits/src/peripherals/gpio.rs
Line 141 in 5e0f0eb
Like #176 this was a well-intentioned but ultimately ill-advised early design decision.
The idea here was to have a
Peripherals
trait that is a super trait of all of the individual peripheral traits, for a few reasons:P: Peripherals
instance instead of needing to grow 7+ generic parametersGpio::read(p, ...)
,Clock::get_milliseconds(p, ...)
. This is particularly useful in testsInput
impl and anOutput
impl that share state to act as a loopback device) to use external state sharing mechanisms (i.e.Arc<Mutex<_>>
,RefCell<_>
, etc.)The 3rd point is a little subtle; here's an example:
PeripheralsSet
:input
andoutput
to be separate instances; if we want one type (i.e.LoopbackIo
) and one instance of that type to provide both ourInput
andOutput
implementations we need to resort to "external" state sharing mechanisms likeArc<Mutex<_>>
to produce two instances that ultimately reference the same instance or some shared dataPeripherals
supertrait on the other hand lets us sidestep this:Peripherals
implementor can choose to shell out to another type for a particular peripheral traitFor the above reasons, we made
Peripherals
a supertrait:core/traits/src/peripherals/mod.rs
Line 37 in 5e0f0eb
We still recognized that the common case was definitely going to be separate instances for each peripheral and that it would be unacceptable for users to implement
Peripherals
for each unique set of peripheral trait implementors they wanted to use together, so:PeripheralSet
which held instances of each peripheral and was generic over the 7 peripheralsPeripheralSet
implement each of the peripheral traits by shelling out to the peripheral instance contained within (this is what theperipheral_trait!
macro did)Problems
This worked but had several downsides, mostly with the
peripheral_trait
macro:peripheral_trait
macro contains an ad-hoc redefinition of the Rust grammar's definition of what constitutes legal syntax for trait definitions: it's both very hard to read and subtly wrong in many waysrustfmt
) don't interact well with items declared inside declarative macros like this. errors stemming from mistakes in the declarative macro's definition of trait definition syntax are extremely hard to diagnose for developers not very familiar with Rust declarative macrosperipheral_trait!
because we can't have a declarative macro like it run on the output of a macro like#[async_trait]
A Solution
3 years later, with the benefit of hindsight, we can come up with a solution that sidesteps these issues while still fulfilling the design constraints that led us to write
peripheral_trait
.Peripherals
traitFirst: We should have the
Peripherals
trait have associated types for each of the peripheral traits within instead of being a super trait. Something like:Our goal was to hide the particular types used for the peripherals instead of having them leak into users of the peripherals as generic parameters. Associated types are a cleaner way of achieving this than supertraits.
As an added bonus it's now easier to selectively have some peripherals be shared; i.e. if you want to have a
LoopbackIo
device, you only need to:LoopbackIo
device and whatever other peripherals you want to usePeripherals
on that struct, exposing the same type and instance forInput
andOutput
and their gettersUnder the existing setup you would have had to write delegating trait implementations of all the peripheral traits for your new type (with the
Input
andOutput
impls delegating to the same instance).PeripheralsWrapper
The above addresses the "shared state between peripheral impls" concern (the 3rd point) and manages to make using the Peripherals in your own API not overly cumbersome (you can just be generic over
P: Peripherals
instead of all 7 individual peripheral traits) but it does not address the 2nd concern: actually using this version of thePeripherals
trait is still more cumbersome that the current incarnation because you now need to callget_gpio
, etc. before calling your particular peripheral trait's methods.We can fix this by: providing a type that does have all the peripheral traits implemented on it and delegates to an underlying
Peripherals
instance.I think we had kind of the right idea with
peripheral_trait!
but just bad execution: it's hard to make a declarative macro like this robust and composable but proc macro attributes are a good fit for this task.ambassador
is one such proc macro that does exactly whatperipheral_trait
was looking to do.We can use
ambassador
to provide delegated impls of all the peripheral traits onPeripheralsWrapper
, wherePeripheralsWrapper
is a type like:with delegations like this:
We can then offer this extension to actually construct a
PeripheralsWrapper
without consuming the underlyingPeripherals
instance:And have the
Deref
/DerefMut
impls onInstructionInterpreter
leverage the above methods.We can also provide delegated impls on
PeripheralSet
to cover the common case.steps
Peripherals
traitperipheral_trait!
macroambassador
provided delegated implswhere
branch:
imp/peripheral-trait-reform
open questions
The text was updated successfully, but these errors were encountered: