We have a number of crates which define an interface. This interface gets used by other library crates, but its implementation gets defined only in a root binary crate (or as part of dev dependencies). Examples include:
alloc
log::Log
getrandom
- Global executors
- Some other parts of
std
such aspanic_fmt
and OOM hooks
They follow the same general pattern, but use different ad hoc solutions, which often also include runtime overhead due to dependency on dynamic dispatch.
It could be beneficial to introduce a common way for implementing this pattern.
Proposal
Introduce new "facade" and "facade implementation" crate types. Crate can not be simultaneously both facade and facade implementation.
A facade crate is defined using the following declaration in its Cargo.toml
:
[package]
name = "foo"
facade = true
# optional section, which MUST contain one dependency
[default-facade-impl]
some_crate = { version = "1", features = ["foo", "bar"] }
This allows us to use #[facade]
attributes in such crate. The first place where it can be used is on functions:
// getrandom
#[facade]
pub fn getrandom(dest: &mut [u8]) -> Result<(), Error>;
// log
pub mod logger {
#[facade]
fn enabled(metadata: &Metadata<'_>) -> bool;
#[facade]
fn log(record: &Record<'_>);
#[facade]
fn flush();
}
An item marked with #[facade]
MUST be public.
Note that in the log
case we remove the Log
trait and replace it with a number of functions which describe required logger functionality. This is because application overwhelmingly use global logger.
But in the case of alloc
, libraries may need to be generic over allocator, but use by default the global allocator. In such cases facade crates should define a facade type:
#[facade]
type GlobalAlloc: Allocator;
Facade implementation crates are defined in Cargo.toml
like this:
[package]
name = "my-logger"
# The specified crate MUST be presented in the dependencies section
facade-impl = "log"
[dependencies]
log = "1"
Facade implementation crates can use #[facade_impl = "..."]
attribute:
//getrandom_impl
// `getrandom::getrandom` MUST be a facade function with exactly the same
// signature. The impl item may not be public.
#[facade_impl = "getrandom::getrandom"]
fn getrandom(dest: &mut [u8]) -> Result<(), getrandom::Error> {
// ...
}
// my_logger
#[facade_impl="log::logger::enabled"]
fn enabled(metadata: &Metadata<'_>) -> bool { ... }
#[facade_impl="log::logger::log"]
fn log(record: &Record<'_>) { ... }
#[facade_impl="log::logger::flush"]
fn flush() { ... }
// my_allocator
#[facade_impl="alloc::GlobalAlloc"]
struct MyAlloc { ... }
// Without this trait impl the facade_impl on type will result
// in a compilation error
impl alloc::Allocator for MyAlloc { ... }
Facade implementation crate MUST cover all facade items from its facade crate.
Crates which depend on a facade crate can use facade items as simple "concrete" items. When library dependent on a facade crate gets compiled, the facade may not have known implementation (e.g. when we run cargo check
on a library crate). In such cases facade functions will be equivalent to extern "Rust" fn
functions, while facade types are equivalent to existential types.
Facade implementation crates get used by binary crates simply by pulling them as dependencies:
[package]
name = "my_bin_crate"
[dependencies]
my_getrandom = "1"
my_alloc = "1"
my_logger = "1"
# This crate can depend on `getrandom`, `alloc`, and `log`.
# It will use the facade implementations under the hood.
my_lib = "1"
Library crates can use facade crates only as a dev dependency:
[package]
name = "my_bin_crate"
[dependencies]
getrandom = "1"
alloc = "1"
logger = "1"
[dev-dependencies]
my_getrandom = "1"
my_alloc = "1"
my_logger = "1"
In other words, a library crate can not define things like global allocator outside of tests and benchmarks.
Root crates can use items from facade implementation crates, e.g. for configuration or setting up resources.
If a crate specifies two conflicting facade implementations, it immediately results in a compilation error. If a facade crate specifies a default facade implementation and root crate specifies its own implementation of this facade, then the default implementation gets replaced. If a facade crate does not specify a default facade implementation and root crate does not specify a facade implementation for it, then it results in a compilation error.
Note that facade implementations are specific to facade crate version. In other words, a project may pull two loggers with different facade implementations:
# root crate
[dependecies]
// facade impl for log v1
my_logger = "1"
// facade impl for log v0.5
other_logger = "0.1"
foo = "1"
bar = "1"
# foo
[dependencies]
log = "1"
# bar
[dependencies]
log = "0.5"
In this case foo
will use implementation from my_logger
, while bar
will use implementation from other_logger
.
When cargo
builds a dependency tree which includes facades and their impls, it performs the following dependency tree transformation:
// Naive dependency tree
my_bin_crate
|- my_logger
|- log
|- my_getrandom
|- getrandom
|- foo
|- log
|- getrandom
|- bar
|- log
// Transformed dependency tree
|- my_logger
|- my_getrandom
|- foo
|- log
|- my_logger
|- getrandom
|- my_getrandom
|- bar
|- log
|- my_logger
In other words, facade implementation crates become dependencies of their facades. Their facade items get replaced by respective facade implementation items. It allows compiler to perform various optimizations, which may significantly improve performance in some cases.
Unresolved questions
- Is it possible to migrate to facade crates in a backwards compatible way for
alloc
andstd
stuff? - Should we require that facade types must provide a dummy type (i.e.
type GlobalAlloc: Allocator = DummyAlloc;
), which will be used as a placeholder during compilation of library crates? - Should we allow generic facade functions? Such functions may require a dummy implementation as well.
- Should we support facade implementation of several facade crates in one crate?
- How to prevent circular dependency after the dependency tree transformation? In other words,
log
depends onmy_logger
for facade impls, whilemy_logger
depends onlog
and may use non-facade items from it (e.g. it could be traits which must be implemented by a facade item).