dtolnay / request-for-implementation

Crates that don't exist, but should
610 stars 6 forks source link

Attribute macro to make a trait boxed clonable #13

Closed dtolnay closed 5 years ago

dtolnay commented 5 years ago

The objekt crate provides a type-safe way to make trait objects clonable, but there are one too many steps involved.

trait MyTrait: objekt::Clone {
    /* ... */
}

clone_trait_object!(MyTrait);

// Now data structures containing Box<MyTrait> can derive Clone.
#[derive(Clone)]
struct Container {
    trait_object: Box<MyTrait>,
}

Ideally this would be taken care of by an attribute macro in one step that expands to the two steps above.

#[clonable]
trait MyTrait: Clone {
    /* ... */
}

The attribute macro would assert that Clone is present in the supertrait list (or else fail to compile with an error), substitute that Clone with objekt::Clone, and then make the correct call to clone_trait_object! to emit the trait object impl.

kardeiz commented 5 years ago

Thanks for putting this project together!

See here for my quick implementation (with example) of this: https://github.com/kardeiz/objekt-clonable.

Actually, it is short so I'll paste it below.

This does handle the case where the objekt crate has been renamed, but does require that the objekt macros are loaded elsewhere.

#[proc_macro_attribute]
pub fn clonable(_attrs: TokenStream, item: TokenStream) -> TokenStream {
    let mut item_trait = parse_macro_input!(item as ItemTrait);

    let item_trait_ident = &item_trait.ident;

    let random = rand::random::<u64>();

    let objekt_rename = &Ident::new(&format!("_objekt_{}", random), Span::call_site());

    let cloneish_paths: Vec<Path> = vec![
        parse_quote!(Clone),
        parse_quote!(std::clone::Clone),
        parse_quote!(::std::clone::Clone),
    ];

    if let Some(path) = item_trait
        .supertraits
        .iter_mut()
        .filter_map(|x| match x {
            TypeParamBound::Trait(ref mut y) => Some(y),
            _ => None
        })
        .map(|x| &mut x.path)
        .find(|x| cloneish_paths.iter().any(|y| &y == x))
    {
        *path = parse_quote!(#objekt_rename::Clone);
    } else {
        panic!("`Clone` must be present in trait supertrait list");
    }

    (quote!{
        extern crate objekt as #objekt_rename;
        #item_trait
        clone_trait_object!(#item_trait_ident);        
    })
    .into()
}
dtolnay commented 5 years ago

Thanks, this is terrific! Some comments, and then I'll check this item off the list:

dtolnay commented 5 years ago

Separately, I'm not all that happy with the "objekt" name. Go ahead and pick something better for your crate if you have ideas. Something straightforward like clone_trait::clonable could work, or maybe some simple memorable name.

kardeiz commented 5 years ago

Thanks for the feedback!

I got rid of the random, though I am still doing the rename (by appending the trait ident to the crate name).

I had to pub use objekt::__internal_clone_trait_object; in the reexport crate to get the internal macro into scope.

I kind of like the name, but I'll think about the name also.

See https://github.com/kardeiz/objekt-clonable.

dtolnay commented 5 years ago

Uh-oh, leaking the internal macro needs to be fixed in my crate. I hadn't updated it since 2018-style macro imports were stabilized. I published objekt 0.1.2 with the fix so that should no longer be needed. Please set your Cargo.toml to require objekt = "0.1.2".

In the generated code I would skip the extern crate entirely and directly invoke objekt_clonable::objekt::clone_trait_object!.

kardeiz commented 5 years ago

Oh, wow, okay that works!

I was worried about not being able to access the crate if it was renamed (in extern crate or Cargo.toml), but I guess that isn't an issue anymore with the 2018 edition. I haven't completely switched mentally over yet to the new edition.

I've updated to objekt 0.1.2.

dtolnay commented 5 years ago

Great. Please remove the dependency on rand and publish the crates to crates.io! Then I will add a link from the objekt readme and close out this issue.

kardeiz commented 5 years ago

Done: https://crates.io/crates/objekt-clonable!

By the way, thanks so much for serde, syn, and quote; they are fantastic! I think it is equally awesome that you've put this project together and are helping mentor people.

dtolnay commented 5 years ago

Added a link in the objekt readme! https://github.com/dtolnay/objekt/commit/f76af10143014365ec55c4cb8f77b86b5b6f45d1 Marked the idea as completed! https://github.com/dtolnay/request-for-implementation#procedural-macros

Nicely done.