R
RUSTY YELLOW PAGES

The orphan rule & coherenceConcept


Explanation

The orphan rule says you may only implement a trait for a type if either the trait or the type is defined in your own crate. You cannot, from a third crate, implement a foreign trait (say, std's Display) for a foreign type (say, Vec<T>) — both are "orphans" to your crate, and the rule forbids it. (More precisely, since RFC 2451: an impl is also allowed when one of your local types appears as a generic parameter of the foreign trait — impl PartialEq<Meters> for f64 is legal — and #[fundamental] types like &T and Box<T> are "looked through" when locality is judged, so impl ForeignTrait for Box<LocalType> is allowed.)

This exists to guarantee coherence: for any given (Trait, Type) pair, there is at most one implementation in the entire program, with no possibility of two different crates each providing a conflicting one. Without this guarantee, adding a dependency could silently change which implementation of a trait gets used somewhere else in your program, or the compiler could face a genuine ambiguity it has no principled way to resolve — coherence is what lets trait resolution be a single, unambiguous, whole-program-wide answer rather than something that depends on which crates happen to be linked in.

The practical consequence is the newtype pattern: if you need to implement a foreign trait for a foreign type, wrapping the foreign type in your own newtype gives you a type that isn't an orphan, which you're then free to implement anything on.

Basic usage example

struct Meters(f64); // <- local type, so a foreign trait may be implemented on it

impl std::fmt::Display for Meters { // allowed: Meters is defined in this crate
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}m", self.0)
    }
}

Restriction: impl std::fmt::Display for Vec<f64> directly would be rejected — both Display and Vec are foreign to this crate. The fix is the newtype pattern: wrap Vec<f64> in a local type like Meters above, then implement on that.

Best practices & deeper information

Scenario

Designing a public API

Needing to implement a foreign trait (Display) for a foreign type (Vec<f64>) is blocked by the orphan rule — the workaround is a local newtype, which is a genuinely local type the rule permits implementing anything on.

struct Readings(Vec<f64>); // <- local newtype: turns a foreign Vec<f64> into a type this crate owns

impl std::fmt::Display for Readings { // allowed: Readings is local, even though Display isn't
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{:?}", self.0)
    }
}

println!("{}", Readings(vec![1.0, 2.5, 3.75]));

Why this way: the newtype costs one wrapper type in exchange for full freedom to implement anything on it — the Rust Design Patterns book documents the newtype pattern, and wrapping a foreign type in a local one is the standard route around the orphan rule, rather than forking or vendoring the foreign crate.

Scenario

Converting between types

Once data lives inside a newtype, From/Into make moving between the newtype and the underlying foreign type ergonomic, instead of exposing the wrapped field directly.

struct Readings(Vec<f64>);

impl From<Vec<f64>> for Readings { // <- lets `.into()` build a Readings from a plain Vec<f64>
    fn from(values: Vec<f64>) -> Self {
        Readings(values)
    }
}

let readings: Readings = vec![1.0, 2.5, 3.75].into(); // <- conversion, not direct field access

Why this way: the API Guidelines' C-CONV-TRAITS recommend From/Into specifically so a newtype composes with any code already written against the standard conversion traits, instead of requiring callers to learn a bespoke constructor name.

Explanation

Embedded support: Full

The orphan rule doesn't change based on target — it's a compile-time coherence rule, identical under #![no_std]. But it comes up in embedded Rust in a very concrete, recurring way: a chip's peripheral-access crate (PAC), typically generated by a tool like svd2rust from the vendor's SVD file, defines register types that are foreign to any crate that depends on it. A board-support or HAL crate wanting to implement embedded-hal's traits — also foreign, from a separate crate — for one of those generated register types hits the orphan rule squarely: both the trait and the type are defined outside the HAL crate. The fix is the same one used everywhere else: wrap the PAC's register type in a local newtype, and implement the foreign trait on that.

This is precisely why real-world HAL crates (stm32f4xx-hal, rp2040-hal, and similar) are structured the way they are — a thin layer of newtypes wrapping the PAC's generated types, each with embedded-hal trait impls attached. The newtype isn't incidental scaffolding; it's the orphan rule's mandatory seam between "code a tool generated for you" and "the shared trait ecosystem you want that generated code to plug into."

Basic usage example

mod pac { // stands in for a vendor's generated peripheral-access crate — foreign to this HAL crate
    pub struct Pa5;
}

trait OutputPin { // foreign trait: embedded-hal's, not defined in this crate
    type Error;
    fn set_high(&mut self) -> Result<(), Self::Error>;
}

struct Pin(pac::Pa5); // <- local newtype: turns the foreign PAC type into one this crate owns

impl OutputPin for Pin { // allowed: Pin is local, even though OutputPin is foreign too
    type Error = core::convert::Infallible;
    fn set_high(&mut self) -> Result<(), Self::Error> {
        // drive the underlying register via self.0 here
        Ok(())
    }
}

Best practices & deeper information

Scenario

Designing a public API

A HAL crate wanting to implement embedded-hal's OutputPin (foreign trait) for a PAC-generated GPIO register type (foreign type) is blocked by the orphan rule — the fix is a local newtype wrapping the PAC type, which the HAL crate is then free to implement anything on.

mod pac {
    pub struct Pb3; // generated register type, foreign to the HAL crate
}

trait OutputPin {
    type Error;
    fn set_high(&mut self) -> Result<(), Self::Error>;
}

pub struct GpioPb3(pac::Pb3); // <- local newtype wraps the foreign, generated register type

impl OutputPin for GpioPb3 { // allowed: GpioPb3 is local, even though OutputPin isn't
    type Error = core::convert::Infallible;
    fn set_high(&mut self) -> Result<(), Self::Error> { Ok(()) }
}

Why this way: this is the standard shape of a real HAL crate's source layout — a newtype per peripheral wrapping the PAC's generated type, each carrying the embedded-hal trait impls the PAC crate itself could never provide (the PAC is usually generated code with no knowledge of embedded-hal at all).

Scenario

Converting between types

Once a HAL crate's newtype wraps the PAC's generated register type, From/Into (where the PAC's split/free functions allow it) make moving between the raw register handle and the HAL's wrapped, trait-implementing type ergonomic for callers who need to drop back to the raw PAC API.

mod pac {
    pub struct Pb3;
}

pub struct GpioPb3(pac::Pb3);

impl From<pac::Pb3> for GpioPb3 { // <- lets `.into()` build a GpioPb3 from the raw PAC register handle
    fn from(raw: pac::Pb3) -> Self {
        GpioPb3(raw)
    }
}

let pin: GpioPb3 = pac::Pb3.into(); // <- conversion, not a bespoke constructor name

Why this way: callers who already hold a raw PAC-generated register (from splitting a GPIOB peripheral, say) get the HAL's trait-implementing wrapper through the same From/Into convention used everywhere else in the ecosystem, rather than learning a one-off constructor per HAL crate.