Summary
High-level code needs to be able to interact with multiple different types of hardware (e.g. power code needs to interact with a PD port and a DC jack), but Rust forces us into a single concrete type. v0.1 code supported this, but we need to determine how this is handled in v0.2.
Background
Interacting with a service requires a device driver to implement one or more traits defined by the service. We want services to be able to interact with multiple different types of devices at the same time, i.e. the power policy needs to coordinate with all PSUs on the system even though one may be a DC barrel jack while another is a Type-C port. This is futher complicated by our use of async code, which normally requires boxing futures on the heap to allow for fully generic async code. We decided to address this by using enum dispatch, allowing code to be generic over only the enum dispatch type. However, the enum_dispatch crate doesn't support async and only works with traits and implementations that are in the same crate.
Additionally, hardware devices might need to be shared with mulitple services, or between ODP code and OEM code. ODP provides the Lockable trait as an abstraction over mutex-like objects. This allows service implementations to be generic over Lockable<Inner: SomeTrait>. When code needs to call a trait function, Lockable::lock is called to yield a guard object which implements DerefMut<Target: SomeTrait>, deref_mut is implicitly called to yield the end &mut impl Sometrait reference.
With enum dispatch, the above approach fails at the DerefMut step because we would need to return a reference to the dispatch type, as demonstrated in this example:
enum LockableDispatch<'a> {
TypeA(&'a Mutex<TypeA>),
TypeB(&'a Mutex<TypeB>).
}
enum GuardDispatch<'a> {
TypeA(MutexGuard<TypeA>),
TypeA(MutexGuard<TypeB>),
}
enum ReferenceDispatch<'a> {
TypeA(&mut TypeA),
TypeB(&mut TypeB),
}
impl<'a> DerefMut<Target = Dispatch<'a>> for GuardDispatch<'a> {
type Target = ReferenceDispatch<'a>;
fn deref_mut(&mut self) -> &Self::Target {
// Can't construct a `ReferenceDispatch` instance because we have to return a reference
// Can't store a `ReferenceDispatch` instance because that won't pass the borrow checker
}
}
Proposed Solution
Change Lockable so that the guard type is what implements SomeTrait instead of being required to implement DerefMut<Target = SomeTrait>. In the above example, this would move the enum dispatch implementation to the GuardDispatch type instead of needing another layer of indirection through DerefMut. This does lead to a complication when not using enum dispatch, which relies on DerefMut. This would be addressed through blanket impls on embassy guard types like impl<'a> SomeTrait for embassy_sync::mutex::MutexGuard<'a, T> where T: SomeTrait { ... }, or a blanket impl like impl SomeTrait for T where T: DerefMut<Target = SomeTrait> { ... }
Alternatives Considered
Put Dispatch Enum inside of the Lockable
In this approach the dispatch enum would be placed in the Lockable instead. With the following Dispatch type:
enum Dispatch {
TypeA(TypeA),
TypeB(TypeB),
}
The flow would then be Lockable::lock -> Guard<Dispatch> -> &mut Dispatch. This avoids the issue with DerefMut because the enum dispatch type is already in the guard. However, this precludes sharing among multiple services because only a single enum dispatch type would have access to the hardware. Similarly, OEM code would no longer have individual access to the hardware, access would have to be done through the aggregate Dispatch type.
Dispatch Before Locking
In this approach the SomeTrait implementation would instead be responsible for locking an inner mutex type and call the appropriate function, like so:
struct TypeA {
lock: Mutex<TypeAInner>,
}
impl SomeTrait for TypeA {
async fn some_func(&self) {
self.lock().await.some_func().await;
}
}
The downside to this approach is that we lose the ability to compose operations atomically. This could allow for concurrency bugs where task A and B are executing functions, which get interleaved and lead to incorrect behavior.
Summary
High-level code needs to be able to interact with multiple different types of hardware (e.g. power code needs to interact with a PD port and a DC jack), but Rust forces us into a single concrete type. v0.1 code supported this, but we need to determine how this is handled in v0.2.
Background
Interacting with a service requires a device driver to implement one or more traits defined by the service. We want services to be able to interact with multiple different types of devices at the same time, i.e. the power policy needs to coordinate with all PSUs on the system even though one may be a DC barrel jack while another is a Type-C port. This is futher complicated by our use of async code, which normally requires boxing futures on the heap to allow for fully generic async code. We decided to address this by using enum dispatch, allowing code to be generic over only the enum dispatch type. However, the
enum_dispatchcrate doesn't support async and only works with traits and implementations that are in the same crate.Additionally, hardware devices might need to be shared with mulitple services, or between ODP code and OEM code. ODP provides the
Lockabletrait as an abstraction over mutex-like objects. This allows service implementations to be generic overLockable<Inner: SomeTrait>. When code needs to call a trait function,Lockable::lockis called to yield a guard object which implementsDerefMut<Target: SomeTrait>,deref_mutis implicitly called to yield the end&mut impl Sometraitreference.With enum dispatch, the above approach fails at the
DerefMutstep because we would need to return a reference to the dispatch type, as demonstrated in this example:Proposed Solution
Change
Lockableso that the guard type is what implementsSomeTraitinstead of being required to implementDerefMut<Target = SomeTrait>. In the above example, this would move the enum dispatch implementation to theGuardDispatchtype instead of needing another layer of indirection throughDerefMut. This does lead to a complication when not using enum dispatch, which relies onDerefMut. This would be addressed through blanket impls on embassy guard types likeimpl<'a> SomeTrait for embassy_sync::mutex::MutexGuard<'a, T> where T: SomeTrait { ... }, or a blanket impl likeimpl SomeTrait for T where T: DerefMut<Target = SomeTrait> { ... }Alternatives Considered
Put Dispatch Enum inside of the
LockableIn this approach the dispatch enum would be placed in the
Lockableinstead. With the followingDispatchtype:The flow would then be
Lockable::lock->Guard<Dispatch>->&mut Dispatch. This avoids the issue withDerefMutbecause the enum dispatch type is already in the guard. However, this precludes sharing among multiple services because only a single enum dispatch type would have access to the hardware. Similarly, OEM code would no longer have individual access to the hardware, access would have to be done through the aggregateDispatchtype.Dispatch Before Locking
In this approach the
SomeTraitimplementation would instead be responsible for locking an inner mutex type and call the appropriate function, like so:The downside to this approach is that we lose the ability to compose operations atomically. This could allow for concurrency bugs where task A and B are executing functions, which get interleaved and lead to incorrect behavior.