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 tool looks for when generating consumer crates.
Usage
Slap #[dylo::export] on impl blocks whose trait you want dylo-cli to generate:
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:
// will generate:
// trait Foo: Send + Sync + 'static { }
// 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 will make sure that:
- In the
modcrate, there's an exported function that returns aBox<dyn ModImpl> - In the "consumer" crate, there is code (leveraging 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:
Note that you're not supposed to write the trait definition yourself — just the
impl Blah for BlahImpl block. It's 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:
[]
= "mod-cliargs"
= "0.1.0"
= "2024"
[]
# mods are always cdylibs — this one will build into
# `target/debug/libmod_cliargs.so` or `target/debug/libmod_cliargs.dylib` on macOS.
= ["cdylib"]
[]
# camino types are used in the public API of this mod
= "1"
= "1"
# impl deps are marked "optional"
= { = "4.5.13", = ["derive"], = true }
[]
= ["impl"]
# ... and they are enabled by the "impl" feature, which is itself enabled by default
= ["dep:clap"]
And in the src/lib.rs code itself:
;
In the consumer version of the crate, only the non-impl dependencies and items will remain:
# rough outline of what `dylo-runtime` would generate for the consumer `cliargs` crate
[]
= "cliargs"
= "0.1.0"
= "2024"
[]
# only the dependencies used in the public API
= "1"
// generated code for the public API
Note that filtering out items with #[cfg(feature = "impl")] isn't done via something like
-Zunpretty-expand, for myriad reasons. It's
done by parsing the AST with syn, removing offending items and
attributes, then formatting the AST with rustfmt.
Limitations
dylo will expect all your exported traits to be dyn-compatible (this used to be call "object safe")
Here's a list of things you cannot do.
Traits cannot be generic over types
// ❌ This won't work
Function arguments or return types cannot be generic
Methods in exported traits cannot have generic type parameters.
// ❌ This won't work
You cannot use impl Trait
Neither argument position nor return position impl Trait is supported — they're essentially
generic type parameters in disguise.
// ❌ This won't work
You can be generic over lifetimes
Unlike with type parameters, traits can be generic over lifetimes:
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:
// that's okay
Note that if you don't need ownership of something, you can just take a reference to it, like the transform function here:
// that's okay too!
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:
// ❌ This won't work yet
However, you can return boxed futures:
// no need to pull in `futures-core` for this
pub type BoxFuture<'a, T> = Pin;
Sometimes you'll need to be a bit more explicit with lifetimes:
Support for dyn-compatible async fn in traits is in the cards afaict, see the
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:
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, 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:
However, I live in the today, and for now I'll stick to my horrible codegen hacks.