2 stable releases
new 1.0.1 | Dec 6, 2024 |
---|---|
1.0.0 | Dec 5, 2024 |
#250 in Procedural macros
252 downloads per month
12KB
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 genaerte:
#[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
:
#[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 will make sure that:
- In the
mod
crate, 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:
#[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'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:
[package]
name = "mod-cliargs"
version = "0.1.0"
edition = "2021"
[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:
#[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:
# rough outline of what `dylo-runtime` would generate for the consumer `cliargs` crate
[package]
name = "cliargs"
version = "0.1.0"
edition = "2021"
[dependencies]
# only the dependencies used in the public API
camino = "1"
// 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, 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
#[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.
// ❌ 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.
// ❌ 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:
#[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:
// 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:
// 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:
// ❌ 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:
// 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:
#[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 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:
#[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, 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.