Dependency inversion and the orphan rule

I am facing a Rust-specific implementation / design issue.

I have a crate, let's call it zigbee, which implements a Zigbee-specific smart home API.

I have another crate, let's call itsmarthomelib, which abstracts across different smart home protocols.

I want the latter crate smarthomelib to expose a generic interface for smart home protocols, such as Zigbee, but also other protocols, such as Matter/Thread.
Now I don't want smarthomelib to know anything about possible smart home protocols or their libraries. It shall expose only a common API, which the other crates implement (-> dependency inversion).

In the zigbee crate, I now have something like

impl<T> crate::ColorControlZigbee for T {} which implements color control functionality on the Zigbee level. For any such implementation I could implement

impl<T> smarthomelib::GenericColorControl for T
where
    T:  crate::ColorControlZigbee
{
}

in terms of business logic, but of course I'm struck now by the orphan rule.
My current "workaround" is to just implement smarthomelib::GenericColorControl for a concrete type as exposed by the zigbee crate, but I find this unsatisfactory.

Do you have any suggestions on how I can have my cake and eat it?
I don't want to couple smarthomelib back to zigbee and I want anything that implements zigbee::ColorControlZigbee to also implement smarthomelib::GenericColorControl.
Is there a trick or something, which I can apply?
How do you approach issues like this?

unsatisfying it is, you always need a local type to implement foreign traits for, a.k.a. the newtype pattern, and there's no way to avoid it.

for example, the derail crate uses the CoreCompat wrapper in this way:

pub struct CoreCompat<E>(pub E);

impl<E> core::Error for CoreCompat<E> where E: crate::Error {
    //...
}

Thanks. Do you think that the dependency-inversion approach still makes sense in Rust, given the orphan rule?
For now my code will be fine implementing the foreign higher-abstraction trait for just one local type, so I'm inclined to leave my overall design as-is.

for these scenarios i think the idiomatic rust solution would be to have a SmartHomeController<Backend>
and a SmartHomeBackend trait then have your zigbee library or a glue library that implements the backend so that the final user can do SmartHomeController<ZigBeeBackend>

Alas, a single SmartHomeController<T> is not so feasible for my approach, which uses a network of Actors. Also, not all smart home controllers are built equal. I.e. any such controller can only provide GenericColorControl if the underlying backend implements GenericColorControl, which then leads me back to the original issue.
But your suggestions gave me another idea, which is to maybe have a trait SmartHomeBackend which provides the implementation-spectific details through a unified interface.
I'll explore where that'll lead me...

i guess you can always just split the backend into multiple traits

trait BackendFeature1{}
trait BackendFeature2{}
trait BackendFeature3{}

struct SmartHomeController<Backend>{...}

impl SmartHomeController<Backend>
   where Backend :BackendFeature1{
   // feature 1 functionalities
}

impl SmartHomeController<Backend>
   where Backend :BackendFeature2{
   // feature 2 functionalities
}

impl SmartHomeController<Backend>
   where Backend :BackendFeature3{
   // feature 3 functionalities
}

this way you can support any backend with any sets of features and have a controller that exposes all features that are actually available

Thanks, but this does not solve my original issue.
In the implementing crate I'll still have the problem, that I cannot

use smarthomelib::SmartHomeController;

pub trait Zigbee {}
pub struct ZigbeeBackend;
    
impl Zigbee for ZigbeeBackend {}

impl<T> SmartHomeController<T> where T: Zigbee {}  // Orphan rule strikes again.
error[E0116]: cannot define inherent `impl` for a type outside of the crate where the type is defined
 --> src/lib.rs:8:1
  |
8 | impl<T> SmartHomeController<T> where T: Zigbee {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ impl for type defined outside of crate
  |
  = note: define and implement a trait or new type instead

For more information about this error, try `rustc --explain E0116`.
error: could not compile `zigbee` (lib) due to 1 previous error

So I'd need a single newtype in the implementing crate, which defeats the original purpose of just having to specify the required trait bound.

it depends.

for example, if the smarthomelib is more of a "framework"-like crate, by which I mean it provides portable/unified interfaces for applications to use, with some form of "plugable" backend architecture, then it is natural for it to define the hooks and interfaces (traits), while the backends should provide concrete implementations (types).

now for the backends, I can see two scenarios:

  • the zigbee crate is itself a backend for smarthomelib, then it is required to provide the types that implements the "frontend" or "framework" interfaces.

    • local traits like colorControlZigbee are just "implemention details" in this case, the "frontend" user mainly uses the exported types to plug into the framework.
  • the zigbee crate is general purpose and implements only the network communication, it doesn't know smarthomelib, or any "frontend" crates, for that matter.

    • in this case, you need an "adapter" crate as the backend, which uses zigbee to provide the implementor of the framework backend protocol. in this case, they are almost certainly types that wrap zigbee types, they might be a simple "newtype" wrapper, but more than often, you want to add other fields to the wrappers for the added functionalities of the framework.

That's exactly its purpose.

As for the two scenarios:

The zigbee crate is a crate, which implements a Zigbee interface. It can be used standalone or through the abstraction of smarthomelib.
Therefore it implements the traits from smarthomelib in a submodule behind an appropriate feature flag. Through this feature flag, the dependency on smarthomelib is optional (only required if you want to use it through its reduced, unified interface).

The "problem" is, that zigbee itself is an abstraction crate, which can be implemented for different zigbee NCP (network co-processor) drivers. In my current case that's EZSP, but that may change in the future when the hardware changes.

So I have an abstraction hierarchy, which looks roughly like:

                        +-> EZSP
             +-> Zigbee |
             |          +-> Hardware B
smarthomelib |
             |                 +-> Hardware C
             +-> Matter/Thread |
                               +-> Hardware D

My intention is to not require knowledge of the implementation details further down the line in the final program, which will only use the current hardware in use through smarthomelib's APIs.

The problem now is, that the concrete type comes from the "driver" level below.
I.e. in terms of traits, I my idea was to impl<T> Smarthomelib for T where T: Zigbee in zigbee and to impl Zigbee for EzspConcreteType in EZSP, and then use fn run<T: Smarthomelib>(controller: T) {} in the application, which doesn't work due to the orphan rule.

So I guess I have two options:

  1. Require the low-level drivers to know about smarthomelib and implement its traits there for the concrete types.
  2. Require a wrapper type on the intermediate layers (which I am currently doing).

Or am I overlooking another option?

how is current hardware determined? the end user application must have some knowledge of the device being used, right? after all, it needs to configure the zigbee stack to use the correct device driver, no?

granted, the application might not need to directly refer to the concrete type, e.g. it can use a pre-compilation step (such as a build script) to generate the configuration code based on build environment or user input, or "dependency injection", as some like to call it; nevertheless, it must have a dependency on the device driver, if you want the zigbee layer be plugable, or configurable.

that's indeed the problem.

of course it doesn't work, and it should not IMO. imagine, what would happen if the same hardware can be used to implement different wireless protocols, e.g. Zigbee and Thread?

this is the solution I would approach the problem too.

what's the motivation that you want a solution that is solely trait based and relies on blanket implementation?

here's what I imagined the code might look like:

// crates/smarthomelib/src/lib.rs
pub trait Protocol { ... }
pub struct ColorController<P: Protocol> { ... }

// crates/zigbee/src/lib.rs
pub trait Driver { ... }
pub struct Zigbee<D: Driver> { ... }
impl<D: Driver> smarthomelib::Backend for Zigbee<D> { ... }

// crates/ezsp/src/lib.rs
pub struct Ezsp { ... }
impl zigbee::Driver for Ezsp { ... }

and the application could use it like this:

fn main() {
    // configure the device driver, e.g.
    // from code generated via build script,
    // or manually selected via feature flags,
    // or whatever.
    let driver = ezsp::Ezsp::new(...);
    // similarly for network protocol
    let protocal = zigbee::Zigbee::new(driver, ...);
    let controller = smarthomelib::ColorController::new(protocol, ...);
    run(controller);
}

fn run<P: smarthomelib::Protocol>(controller: smarthomelib::ColorController<P>)
{
    loop { todo!() }
}

I mean, you can enforce it[1], you just can’t have it be automatically created.

Enforcing by using a supertrait bound is just

trait ColorControlZigbee: smarthomelib::GenericColorControl { … }

The closest to “automatic” would probably be a macro. You could go as far as creating a helper proc macro crate just to define an attribute to use on the impls

#[some_macro_name]
impl<SomeArgs> ColorControlZigbee for SomeType<SomeArgs> { … }

and it would additionally generate from that a copy of your (indended to be generic) trait impl of GenericColorControl in terms of ColorControlZigbee

impl<SomeArgs> GenericColorControl for SomeType<SomeArgs> {
    // hint: you *can* use methods of `ColorControlZigbee` here
    // there’s nothing preventing a sub-trait impl to make use of
    // a supertrait impl’s API
}

There is some ongoing work that could help offer this kind of convenience (without even any added attribute needed) through a language feature.


  1. and this also then makes it so that even in a generic context with some T: ColorControlZigbee bound, the compier does understand this also gives you T: GenericColorControl ↩︎

You could also consider inverting your inversion and use a "vocabulary" crate like "smarthome-traits" that zigbee etc can optionally depend on and implement, that is then the interface used by the frontend smarthomelib crate.

Whether that makes sense depends on how much functionality is in the frontend vs the trait implementations; this works best if the traits are at least mostly very thin adapters.

In the prototype it's hard-coded for now. The idea is, that you'll just need to change one library / line of code when changing the HW, since any such "driver" object will implement the same higher-level traits. But it's conceivable to have this dynamically detected as well in the future, if the need should arise.

I agree. Theoretically, you can still split the Zigbee and Matter/Thread drivers into two libraries, but still, I understand why the orphan rule exists.

That's good to hear. That means that I'm at least not doing utter nonsense here. :smiley:

Just convenience and possibly a deeply rooted tendency to over-engineer stuff. :stuck_out_tongue:

That's certainly a technical possibility, but I don't want to force the user of the intermediate (Zigbee) level into knowing about smarthomelib. There may be applications, where someone wants to run the Zigbee library using the protocol directly instead of an abstraction on a higher level.

Well, this is what I am doing now, isn't it? :slight_smile:
I.e. this currently describes what smarthomelib is.
The problem is just that the orphan rule prevents me from implementing an adapter between the higher-level abstraction crate and the lower level drivers for all possible types. I can just do it for concrete types. But given the feedback in this thread so far, I guess that's fine and I'll probably continue down this road if not convinced otherwise. :wink:

From the original post it sounds like you currently have:

// smarthomelib
fn some_use_of_color(control: &dyn GenericColorControl) {
  // ...
}

trait GenericColorControl {
  // ...
}
impl GenericColorControl for zigbee::Zigbee {
  // map to Zigbee interface ...
}
// other impls of GenericColorControl ...

// zigbee
pub struct Zigbee { ... }

But I was suggesting:

// smarthomelib
fn some_use_of_color(control: &dyn GenericColorControl) {
  // ...
}

// smarthomelib/Cargo.toml
zigbee = { features = ["smarthome-traits"] }

// zigbee
pub struct Zigbee { ... }

#[cfg("smarthome-traits")]
impl smarthome_traits::GenericColorControl for Zigbee {
  // map to Zigbee interface ...
}

// zigbee/Cargo.toml
// I think you also need this? Haven't done this in a while
[features]
smarthome-traits = ["dep:smarthome-traits"]

[dependencies]
// ...
smarthome-traits = { optional = true }

// ... other crates that optionally impl smarthome-traits

// smarthome-traits
trait GenericColorControl {
  // ...
}

If the "map to Zigbee interface" is very thin, and especially if GenericColorControl etc. are very general then this is appropriate for the context of zigbee and other implementing crates regardless of if someone is using smarthomelib, and indeed could be used by other crates that are treating these libraries generically.

These "optionally implement a set of traits" features are pretty common, especially serde but also mint for math types and so on are prior art to look at.

Sorry for the misunderstanding then. I am actually currently doing what you suggest in the second variant. :slight_smile:
Except I'm using static dispatch only at the moment.

Fair enough! Sounds like you defaulted to what is seemingly the standard approach for loosely coupling crates then.

(&dyn wasn't the relevant bit of the example BTW, just that you were doing something with these traits instead of the types directly)