Handling multiple types which share a non-object-safe trait

I need to deal with several different types of object, all of which share a trait, but that trait's not object-safe. I want to pass them around my program, but I can't find any way to do that?

Ideally I'd return them from a pure fn, to a place that will deal with their i/o, but the following doesn't compile due to the object-unsafety of Serialize. I obviously can't return Vec<impl Serialize> because there's multiple types in the Vec. Nor can I use Any, because it can only downcast to concrete types, not interfaces.

  use k8s_openapi::api::core::v1::{Pod, Service};
  
  fn main() {
      make_resources().iter().for_each(
          |r| println!("{}", serde_yaml::to_string(&r).unwrap())
      );
  }
  
  fn make_resources() -> Vec<Box<dyn serde::Serialize>> {
      let a = Pod { ..Pod::default() };
      let b = Service { ..Service::default() };
      vec![a, b]
  }
error[E0038]: the trait `Serialize` cannot be made into an object
   --> src/main.rs:7:32
    |                                                                                              
7   | fn make_resources() -> Vec<Box<dyn serde::Serialize>> {                  
    |                                ^^^^^^^^^^^^^^^^^^^^ `Serialize` cannot be made into an object
    |     

Nor can I even give that code the i/o fn, as impl Foo isn't allowed in arg lists (I really wouldn't mind if printer() got templated out once for each concrete type, but I can't seem to tell the compiler to do that):

  use k8s_openapi::api::core::v1::{Pod, Service};
  
  fn main() {
      make_resources(|r| println!("{}", serde_yaml::to_string(&r).unwrap()));
  }
  
  fn make_resources<F>(printer: F)
      where F: FnOnce(impl serde::Serialize)
  {
      let a = Pod { ..Pod::default() };
      printer(a);
      let b = Service { ..Service::default() };
      printer(b);
  }
error[E0562]: `impl Trait` only allowed in function and inherent method argument and return types, not in `Fn` trait params
 --> src/main.rs:8:21
  |
8 |     where F: FnOnce(impl serde::Serialize)
  |                     ^^^^^^^^^^^^^^^^^^^^^

How do I solve this problem? At the moment I can't even work out how to do basic factoring of my code into >1 fn.

A typical approach is to write a different, object-safe trait, that offers all the functionality you actually need, implement that trait generically for all T: OriginalNonObjectSafeTrait, and make an object of that trait then.

Like for example, if all you need to be able to is to call serde_yaml::to_string, you can make a trait that hasonly a fn(&self) -> Result<String, serde_yaml::Error> method with that functionality and is object safe.

use serde::Serialize;
use serde_yaml;

trait SerializeYamlString {
    fn serde_yaml_to_string(&self) -> Result<String, serde_yaml::Error>;
}
impl<T: Serialize> SerializeYamlString for T {
    fn serde_yaml_to_string(&self) -> Result<String, serde_yaml::Error> {
        serde_yaml::to_string(self)
    }
}

use k8s_openapi::api::core::v1::{Pod, Service};

fn main() {
    make_resources()
        .iter()
        .for_each(|r| println!("{}", r.serde_yaml_to_string().unwrap()));
}

fn make_resources() -> Vec<Box<dyn SerializeYamlString>> {
    let a = Pod { ..Pod::default() };
    let b = Service {
        ..Service::default()
    };
    vec![a, b] // <- TODO, probably needs some `Box::new` wrapping
}

Sometimes, such a object-safe helper trait can even offer (essentially) the full functionality of the original trait, perhaps just a little less efficiently. E.g. if your ordinary trait has a generic method fn foo_with_callback<F: FnOnce(u8)>(&self, x: F)>, then you could make an object safe trait with a method fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>) to capture essentially the full API of foo_with_callback, but less efficiently, as new trait objects are involved.


trait OriginalNonObjectSafeTrait {
    fn foo_with_callback<F: FnOnce(u8)>(&self, x: F);
}

trait WrappingObjectSafeTrait {
    fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>);
}

impl<T: OriginalNonObjectSafeTrait> WrappingObjectSafeTrait for T {
    fn foo_with_dyn_callback<'a>(&self, x: Box<dyn FnOnce(u8) + 'a>) {
        self.foo_with_callback(x)
    }
}

// for convenience we can even go back e.g. for a `Box<dyn …>`
impl OriginalNonObjectSafeTrait for Box<dyn WrappingObjectSafeTrait> {
    fn foo_with_callback<F: FnOnce(u8)>(&self, x: F) {
        (&**self).foo_with_dyn_callback(Box::new(x))
    }
}

In the case of serde::Serialize, there’s also pre-existing object safe traits of this kind, you can find in erased-serde.

(Compared to above approach, that features some improvements, such as avoiding introduction of Boxes by using &mut … references; and implementing the original serde::Serialize for the dyn erased_serde::Serialize type itself. Also for Serialize, the approach had to be taken in multiple steps, recursively also providing object-safe versions of all traits that were used for generic parameters.)

10 Likes

I had a look at the src of fn erase and noticed it was inside a impl dyn Serializer block: ser.rs - source

What is that? I'm not familiar with the impl dyn construct.

Unless it's not so much an "impl dyn construct" as a regular old impl block for the type dyn Serializer.

I thought I'd quickly test that theory but haven't had much joy: playground

I think I'm missing the right way to actually create a dyn Foo. &dyn Foo isn't the same thing.

It's exactly that, yes.

In this playground, you don't use the created dyn Foo in any way. Of course, Fooable doesn't have the method, since it's not dyn Foo. If you actually use the dyn_foo you've created, this compiles - playground.

You don't need a dyn Serializer in this case, since erase is not a method (it doesn't take self as an argument in any way), it's an associated function. <dyn Serializer>::erase(something) is what you're expected to use, IIUC.

4 Likes

Just so I understand all the tools at my disposal, why would I want a method on just the trait-object? I assume that these are, ironically, not virtually dispatched?

1 Like

It's useful when you need to operate on the trait object itself, and not the implementing base type. Rust's "downcasting" is one example.

It's not ironic once you internalize that dyn Trait + '_ is it's own concrete, compile-time type. Instead it's consistent.[1]


  1. And the exceptions more annoying, IMO. ↩︎

4 Likes

You can never create in one step a dyn Foo. dyn Foo is a type that can be given to any Foo implementor; so the process is always to create a value of some implementing type and then coerce that type to dyn Foo.

But that doesn't necessarily matter — the type in the impl block doesn't matter — because what actually controls method lookup is the receiver type: when you write

impl dyn Foo {
    fn method(&self) {

the receiver type is &Self = &dyn Foo, and method dispatch cares only about &dyn Foo, not how the impl block was declared. You aren't allowed to move the & from method to impl and write

impl &dyn Foo {
    fn method(self) {

but if you could, it would behave exactly the same way. (Though, for trait impl blocks impl Foo for Bar, it does matter what you're implementing on because the type has to meet trait bounds written by the callers of the trait.)

2 Likes

Perfect answer btw, thank you!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.