[](LICENSE-MIT)
[](https://crates.io/crates/dylo)
[](https://docs.rs/dylo)
# dylo
`dylo` provides the `#[dylo::export]` attribute.
This crate has zero dependencies and does not perform any code generation of its own - it simply
provides the attribute definitions that the [dylo-cli](https://crates.io/crates/dylo-cli) tool looks
for when generating consumer crates.
## Usage
Slap `#[dylo::export]` on impl blocks whose trait you want `dylo-cli` to genaerte:
```rust
#[dylo::export]
impl Mod for ModImpl {
/// Parses command line arguments
fn parse(&self) -> Args {
Args::parse()
}
}
```
> **Warning**
> Only dyn-compatible traits can be marked with `#[dylo::export]` — dynamic dispatch
> is kinda the whole point.
Traits generated by `dylo` are `Send + Sync + 'static` by default. If you need a trait to be
not sync, you can pass `nonsync` as an arugment to `dylo::export`:
```rust
#[dylo::export]
impl Foo for FooImpl {}
// will generate:
// trait Foo: Send + Sync + 'static { }
#[dylo::export(nonsync)]
impl Bar for BarImpl {}
// will generate:
// trait Bar: Send + 'static { }
```
The `Mod` trait has special treatment: the concrete type `ModImpl` must implement `Default`,
because it must be able to be constructed dynamically when the mod is loaded, from no arguments.
If `Mod` or `ModImpl` are missing, the code geneated by dylo will not compile.
[dylo-cli](https://crates.io/crates/dylo-cli) will make sure that:
* In the `mod` crate, there's an exported function that returns a `Box<dyn ModImpl>`
* In the "consumer" crate, there is code (leveraging [dylo-runtime](https://crates.io/crates/dylo-runtime))
that knows how to build, load, and return a `Box<dyn Mod>`.
If you need your initialization to take arguments, you can simply export two interfaces:
```rust
#[dylo::export]
impl Mod for ModImpl {
type Error = anyhow::Error;
fn make_client(&self, endpoint: &str) -> Result<Box<dyn Client>, Self::Error> {
let client = ClientImpl::new(endpoint)?;
Ok(Box::new(client))
}
fn parse_args(&self) -> Args {
// ...
}
}
#[dylo::export]
impl Client for ClientImpl {
fn send_request(&self, request: Request) -> Result<Response, anyhow::Error> {
// ...
}
}
```
Note that you're not supposed to write the `trait` definition yourself — just the
`impl Blah for BlahImpl` block. It's [dylo-cli](https://crates.io/crates/dylo-cli)'s job
to generate the trait for you.
In concrete terms, it will add an `src/.dylo/spec.rs` file to your original crate, and
add an `include!(".dylo/spec.rs")` item to your `src/lib.rs`
> **Warning**:
>
> Other crate structures exist but aren't supported for now.
## Dependencies
Because the consumer crate is generated from the `mod-XXX` crate, it shares some dependencies
with it: any types that appear in the public API must be available to the `con-XXX` crate as well.
However, some types and functions and third-party crates are only used in the implemention. Those
can be feature-gated both in the `Cargo.toml` manifest:
```toml
[package]
name = "mod-cliargs"
version = "0.1.0"
edition = "2024"
[lib]
# mods are always cdylibs — this one will build into
# `target/debug/libmod_cliargs.so` or `target/debug/libmod_cliargs.dylib` on macOS.
crate-type = ["cdylib"]
[dependencies]
# camino types are used in the public API of this mod
camino = "1"
con = "1"
# impl deps are marked "optional"
clap = { version = "4.5.13", features = ["derive"], optional = true }
[features]
default = ["impl"]
# ... and they are enabled by the "impl" feature, which is itself enabled by default
impl = ["dep:clap"]
```
And in the `src/lib.rs` code itself:
```rust
#[cfg(feature = "impl")]
#[derive(Default)]
struct ModImpl;
#[cfg_attr(feature = "impl", derive(Parser))]
pub struct Args {
#[cfg_attr(feature = "impl", clap(default_value = "."))]
/// config file
pub path: Utf8PathBuf,
}
#[dylo::export]
impl Mod for ModImpl {
fn parse(&self) -> Args {
Args::parse()
}
}
```
In the consumer version of the crate, only the non-impl dependencies and items will remain:
```toml
# rough outline of what `dylo-runtime` would generate for the consumer `cliargs` crate
[package]
name = "cliargs"
version = "0.1.0"
edition = "2024"
[dependencies]
# only the dependencies used in the public API
camino = "1"
```
```rust
// generated code for the public API
pub struct Args {
/// config file
pub path: Utf8PathBuf,
}
pub trait Mod: Send + Sync + 'static {
fn parse(&self) -> Args;
}
```
Note that filtering out items with `#[cfg(feature = "impl")]` isn't done via something like
[-Zunpretty-expand](https://github.com/rust-lang/rust/issues/43364), for myriad reasons. It's
done by parsing the AST with [syn](https://crates.io/crates/syn), removing offending items and
attributes, then formatting the AST with rustfmt.
## Limitations
dylo will expect all your exported traits to be [`dyn`](https://doc.rust-lang.org/std/keyword.dyn.html)-compatible (this used to be call "object safe")
Here's a list of things you cannot do.
### Traits cannot be generic over types
```rust
// ❌ This won't work
#[dylo::export]
impl Parser<T> for JsonParser<T> {
fn parse(&self, input: &str) -> Result<T>;
}
```
### Function arguments or return types cannot be generic
Methods in exported traits cannot have generic type parameters.
```rust
// ❌ This won't work
#[dylo::export]
impl Parser for JsonParser {
fn parse<T: DeserializeOwned>(&self, input: &str) -> Result<T>;
}
```
### You cannot use `impl Trait`
Neither argument position nor return position `impl Trait` is supported — they're essentially
generic type parameters in disguise.
```rust
// ❌ This won't work
#[dylo::export]
impl Handler for MyHandler {
fn process(&self, transform: impl Fn(u32) -> u32) -> impl Iterator<Item = u32>;
}
```
### You can be generic over lifetimes
Unlike with type parameters, traits can be generic over lifetimes:
```rust
#[dylo::export]
impl Parser<'a> for MyParser {
fn parse(&self, input: &'a str) -> Result<&'a str>;
}
```
This works because lifetimes are erased at compile time and don't affect dynamic dispatch.
### You can (and should) use boxed trait objects
A surprising amount of things can be achieved through boxed trait objects if most of your traits are dyn-compatible:
```rust
// that's okay
#[dylo::export]
impl Handler for MyHandler {
fn process(&self, transform: Box<dyn Fn(u32) -> u32>) -> Box<dyn Iterator<Item = u32>>;
}
```
Note that if you don't need ownership of something, you can just take a reference to it, like the transform function here:
```rust
// that's okay too!
#[dylo::export]
impl Handler for MyHandler {
fn process(&self, transform: &dyn Fn(u32) -> u32) -> Box<dyn Iterator<Item = u32>>;
}
```
### Async functions are not supported yet
async fns in trait (AFIT) are supported by Rust as of 1.75, but as of 1.83, they are still
not dyn-compatible, so this won't work:
```rust
// ❌ This won't work yet
#[dylo::export]
impl Client for HttpClient {
async fn fetch(&self, url: &str) -> Result<Response> {
reqwest::get(url).await
}
}
```
However, you can return boxed futures:
```rust
// no need to pull in `futures-core` for this
pub type BoxFuture<'a, T> = std::pin::Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
#[dylo::export]
impl Client for HttpClient {
fn fetch(&self, url: &str) -> BoxFuture<'_, Result<Response>> {
Box::pin(async move {
reqwest::get(url).await
})
}
}
```
Sometimes you'll need to be a bit more explicit with lifetimes:
```rust
#[dylo::export]
impl Client for HttpClient {
fn fetch<'fut>(&'fut self, url: &'fut str) -> BoxFuture<'fut, Result<Response>> {
Box::pin(async move {
reqwest::get(url).await
})
}
}
```
Support for `dyn-compatible` async fn in traits is in the cards afaict, see the
[dynosaur](https://crates.io/crates/dynosaur) crate for a peek into the future (however you
cannot use it with dylo).
### Self type restrictions
You cannot take or return `self` by value, but you can use `Box<Self>` or `Arc<Self>` receivers:
```rust
#[dylo::export]
impl Builder for RequestBuilder {
// ❌ These won't work: we don't know the size of "Self"
fn with_header(mut self, name: &str) -> Self {
// etc.
}
fn build(self) -> Request {
// etc.
}
}
#[dylo::export]
impl Builder for RequestBuilder {
// ✅ These will work: a Box<Self> is the size of a pointer
fn with_header(mut self: Box<Self>, name: &str) -> Box<Self> {
// etc.
}
fn build(self: Box<Self>) -> Request {
// etc.
}
}
```
Essentially, as a consumer, we don't know the size of "Self" — so we need the indirection.
References (`&self`, `&mut self`) are always fine.
## Should `dylo` exist?
Not really — much like [rubicon](https://github.com/bearcove/rubicon), all that
should be possible in stable Rust, with support from the compiler, etc.
Half the reason to bother with an approach like dylo's is to avoid unnecessary
rebuilds. The _proper_ approach for that is being explored by other folks, see:
* [Downstream dependencies of a crate are rebuilt despite the changes not being public-facing #14604](https://github.com/rust-lang/cargo/issues/14604)
However, I live in the today, and for now I'll stick to my horrible codegen hacks.