dylo 1.0.2

Generate dyn-compatible traits with procedural macros
Documentation
[![license: MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT)
[![crates.io](https://img.shields.io/crates/v/con.svg)](https://crates.io/crates/dylo)
[![docs.rs](https://docs.rs/dylo/badge.svg)](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.