hobofan / ambassador

Delegation of trait implementations via procedural macros
Apache License 2.0
251 stars 13 forks source link

Permit delegating to a more complicated expression (via macro call) #32

Closed ijackson closed 2 years ago

ijackson commented 2 years ago

In my application, I want to delegate to a more compliated expression than simply a single field. So I have done this:

impl Piece {
  fn inner(&self) -> &dyn PieceTrait {
    self.ipc
      .as_ref()
      .expect("attempted to invoke unresolved fastsplit::Piece")
      .p.direct_trait_access()
  }
}

impl PieceTrait for Piece {
  ambassador_impl_PieceTrait_body_single_struct!{ inner() }
}

This works great. But I had to alter ambassador to permit more than just a simple identifier as the delegate.

I pasted the whole of Piece::inner() to show that it can do arbitrary stuff without having to complicate the macrology. The only requirement for the delegate argument is that appending it to self. produces a trait object expression.

(I hope this passes the github CI. I tried to run the tests locally but they don't work on master. I think I am probably running them with the wrong rust version or something.)

dewert99 commented 2 years ago

One potential issue with this solution would be if Piece borrowed self mutably (or took ownership if we weren't using dyn) the macro would still try to use inner() leading to a type error. A potentially more general solution would be to allow the user to choose three different delegates (one for self, &self, and &mut self). It would also probably be useful to be able to access such a feature without using the generated macros directly, probably an attribute macro applied to the impl block similar to #[Derive(Delegate)]. Doing this we could also sanity check the signatures of the chosen delegates. Here is one idea of the end result:

#[delegate_to_methods()]
#[delegate(PieceTrait, target_ref = "inner", target_mut = "inner_mut")]
impl Piece {
  fn inner(&self) -> &dyn PieceTrait {
    self.ipc
      .as_ref()
      .expect("attempted to invoke unresolved fastsplit::Piece")
      .p.direct_trait_access()
  }
  fn inner_mut(&mut self) -> &mut dyn PieceTrait {
    self.ipc
      .as_mut()
      .expect("attempted to invoke unresolved fastsplit::Piece")
      .p.direct_trait_access_mut()
  }
}
ijackson commented 2 years ago

One potential issue with this solution would be if Piece borrowed self mutably (or took ownership if we weren't using dyn) the macro would still try to use inner() leading to a type error

Indeed.

A potentially more general solution would be to allow the user to choose three different delegates (one for self, &self, and &mut self)

Yes. That woudl be great.

It would also probably be useful to be able to access such a feature without using the generated macros directly,

Well, maybe. Personally I like using the generated macro. In my application I need to apply bunches of magical attributes to the traits and structs and impls. My actual code looks like this:

#[ambassador::delegatable_trait]
#[enum_dispatch]
#[dyn_upcast]
pub trait OutlineTrait: Debug + Sync + Send + 'static {

[ambassador::delegatable_trait]
#[dyn_upcast]
pub trait PieceBaseTrait: OutlineTrait + Send + Debug + 'static {

#[derive(Serialize,Deserialize,Debug)]
struct Piece {

[dyn_upcast]
impl OutlineTrait for Piece {
  ambassador_impl_OutlineTrait_body_single_struct!{ inner() }
}
#[dyn_upcast]
impl PieceBaseTrait for Piece {
  ambassador_impl_PieceBaseTrait_body_single_struct!{ inner() }
}
#[typetag::serde(name="FastSplit")]
impl PieceTrait for Piece {
  ambassador_impl_PieceTrait_body_single_struct!{ inner() }
}

To my mind, feeding all those attrs on the impls through a derive macro on Piece would be really quite unpleasant and opaque.

If I could have any API I liked, I think the best for me would be an autogenerated macro that generates the whole impl block so one can do something like this:

#[dyn_upcast]
ambassador_impl_OutlineTrait!{ for Piece }

or this:

#[cfg(feature = "crazy")]
#[dyn_upcast]
ambassador_impl_ExcitingTrait!{
    for Crazy<T> where T:Clone,
    inner(),
    inner_mut(),
    inner_owned(),
}
dewert99 commented 2 years ago

I still think it would be nice to have a way of avoiding the generated macros (which would probably give better error messages), but I can understand why you find them useful. I was working on this and right now have it so

use ambassador::delegatable_trait;

#[delegatable_trait]
trait PieceTrait {
    fn get(&self) -> u32;
    fn change(&mut self);
}

struct Piece {
    inner: Option<Box<dyn PieceTrait>>,
}

impl Piece {
    fn inner(&self) -> &dyn PieceTrait {
        // I wanted a self contained example
        self.inner
            .as_ref()
            .expect("attempted to invoke unresolved fastsplit::Piece")
            .as_ref()
    }

    fn inner_mut(&mut self) -> &mut dyn PieceTrait {
        self.inner
            .as_mut()
            .expect("attempted to invoke unresolved fastsplit::Piece")
            .as_mut()
    }
}

impl PieceTrait for Piece {
    ambassador_impl_PieceTrait! {body_struct(<>, dyn PieceTrait, (compile_error!("owned receiver")), (inner()), (inner_mut()))}
}

would work. (The ambassador_impl_PieceBaseTrait_body_single_struct! macro is deprecated and only is generated when using the "backward_compatible" feature). I'm still planning to add a macro you could apply to the impl block but you wouldn't have to use it. Does this seem reasonable to you?

dewert99 commented 2 years ago

After working on things a bit more I think the best way to use the macro directly would be

ambassador_impl_PieceTrait! {body_struct(<>, dyn PieceTrait, (), (inner()), (inner_mut()))}

since the macro emits a special error message when using () if the corresponding receiver type is used

ijackson commented 2 years ago

Cool, thanks! I will update my project to use this new syntax.

ijackson commented 2 years ago

FYI I ended up with this:

impl_via_ambassador!{
  #[dyn_upcast]
  impl OutlineTrait for Piece { inner() } 

  #[dyn_upcast]
  impl PieceBaseTrait for Piece { inner() }

  #[typetag::serde(name="FastSplit")]
  impl PieceTrait for Piece { inner() }
}

via this

#[macro_export]
macro_rules! impl_via_ambassador{
  { 
    $(
      $( #[ $attr:meta ] )*
      impl $Trait:ident for $Type:ty
      { $($how_immut:tt)* }
    )*
  } => { $( paste!{
    $( #[ $attr ] )*
    impl $Trait for $Type {
      [< ambassador_impl_ $Trait >]!{ body_struct( <>, dyn $Trait,
          (),
          ($($how_immut)*),
          ()
      ) }
    }
  } )* }
}

which I am quite happy with. So thank you :-).