Reference page
Orphan Rule Challenges: Feature Crate Extraction
We explored extracting miniextendr-api's optional features (ndarray, nalgebra, serde, rayon, etc.) into separate miniextendr-<name> crates. The goal was to reduce miniextendr-api's surface area and let users depend only on what they need.
๐Context
We explored extracting miniextendr-apiโs optional features (ndarray, nalgebra, serde, rayon, etc.) into separate miniextendr-<name> crates. The goal was to reduce miniextendr-apiโs surface area and let users depend only on what they need.
๐The Problem
Rustโs orphan rule prevents impl ForeignTrait for ForeignType in a third-party crate. For a hypothetical miniextendr-ndarray crate:
// miniextendr-ndarray/src/lib.rs
use miniextendr_api::from_r::TryFromSexp;
impl TryFromSexp for ndarray::Array2<f64> { ... }
// ^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
// from api from ndarray
// (foreign) (foreign)
// โ orphan rule violation
The impl must live in the crate that owns either the trait (miniextendr-api) or the type (ndarray). Since we donโt control ndarray, the impl must stay in miniextendr-api.
๐Approaches Considered
๐1. Bridge trait with blanket impl
Define a bridge trait in api, blanket-impl TryFromSexp for it, then impl the bridge trait in the feature crate.
// miniextendr-api
pub trait SexpBridge { fn try_from_sexp(sexp: SEXP) -> Result<Self, SexpError>; }
impl<T: SexpBridge> TryFromSexp for T { ... } // fine: both traits local to api
// miniextendr-ndarray
impl SexpBridge for Array2<f64> { ... }
// ^^^^^^^^^^ ^^^^^^^^^^
// foreign (api) foreign (ndarray) โ still blocked
Result: Same orphan violation โ SexpBridge is foreign to miniextendr-ndarray.
๐2. TryFrom<SEXP> (std trait)
// miniextendr-ndarray
impl TryFrom<SEXP> for Array2<f64> { ... }
// ^^^^^^^ ^^^^ ^^^^^^^^^^
// core api ndarray โ all foreign, blocked
Result: core::TryFrom is even more foreign. Same constraint.
๐3. Derive macro on mirror traits
Have feature crates define mirror traits, with a derive macro from api that validates compatibility and generates TryFromSexp impls.
Result: No matter what macro generates the code, the impl lives in the crate where it expands. If that crate owns neither the trait nor the type, the orphan rule blocks it. Macros cannot bypass the orphan rule.
๐4. Newtype wrappers
// miniextendr-ndarray
pub struct RArray2<T>(pub ndarray::Array2<T>); // local type
impl<T> TryFromSexp for RArray2<T> { ... } // valid: RArray2 is local
impl<T> Deref for RArray2<T> { type Target = Array2<T>; ... }
Result: Works, but degrades ergonomics. Users write RArray2<f64> in #[miniextendr] signatures instead of Array2<f64>. Deref coercion helps inside function bodies but not at the API boundary.
๐5. Free functions (no traits)
Feature crate exports pub fn array2_from_sexp(sexp: SEXP) -> Result<Array2<f64>, Error>. Users call manually.
Result: Works, but loses the automatic conversion that #[miniextendr] provides. The macro would need per-type configuration to know which function to call.
๐6. User-crate macro expansion
Have a registration macro in the userโs crate generate the impls.
Result: The userโs crate also doesnโt own TryFromSexp or Array2<f64>. Same orphan violation, regardless of where the macro is defined.
๐Conclusion
There is no stable Rust mechanism to write impl ForeignTrait for ForeignType outside the crate that owns one of them. The only escape hatches (#[fundamental], specialization) are unstable/nightly-only.
Decision: Optional feature support stays in miniextendr-api as feature-gated code. This is the same pattern used by the broader Rust ecosystem (e.g., serde relies on external crates opting in, which we canโt do since we donโt control ndarray, nalgebra, etc.).
๐serde Comparison
serde solves this differently: external crates (chrono, uuid) add serde as an optional dependency and impl Serialize/Deserialize themselves (they own the type). We canโt use this approach because we donโt control the external crates.
The serde_with / remote derive pattern still requires the impl to live in a crate that owns either the trait or the type โ it just provides ergonomic sugar for defining mirror types.