How to compare trait objects when user-defined types wrap

I’m working on a library where I need to compare if two wrapped trait objects actually point to the same underlying value.

The tricky part is that users of the library can supply their own types (e.g. Qux) that embed library types (like Bar) while implementing the same traits.

Here’s a reduced example:

use std::any::{Any};
use std::sync::{Arc, Mutex};

// types provided by the Library
pub trait IFoo: Any + Send + Sync {}
pub struct Foo {}
impl dyn IFoo {
    pub fn as_any(&self) -> &dyn Any {
        self
    }
}
impl IFoo for Foo {}

pub trait IBar: Any + Send + Sync {}
pub struct Bar {}
impl dyn IBar {
    pub fn as_any(&self) -> &dyn Any {
        self
    }
}
impl IBar for Bar {}

// Types from User of the Library (although defined in here to keep things simple)
struct Qux {
    bar: Bar, // to forward invocations
}
impl IBar for Qux {}

// Library
pub struct Comparator {
    left: Arc<dyn Any + Send + Sync>,
}

impl Comparator {
    pub fn compare(&self, right: &Arc<dyn Any + Send + Sync>) -> bool {
        if (*self.left).type_id() != (**right).type_id() {
            return false;
        }

        // Compare underlying pointers
        let left_ptr = Arc::as_ptr(&self.left) as *const ();
        let right_ptr = Arc::as_ptr(right) as *const ();
        left_ptr == right_ptr
    }
}

fn main() {
       let foo: Arc<dyn IFoo> = Arc::new(Foo {});
    let foo_any: Arc<dyn Any + Send + Sync> = Arc::new(foo.clone());
    let foo_any2: Arc<dyn Any + Send + Sync> = Arc::new(foo.clone());

    let comparator = Comparator { left: foo_any.clone() };
    println!("Compare foo -> foo: {}", comparator.compare(&foo_any)); // true
    println!("Compare foo -> foo2: {}", comparator.compare(&foo_any2)); 

    let bar: Arc<Mutex<dyn IBar>> = Arc::new(Mutex::new(Bar {}));
    let bar_any: Arc<dyn Any + Send + Sync> = Arc::new(bar.clone());
    let bar_any2: Arc<dyn Any + Send + Sync> = Arc::new(bar.clone());

    let comparator = Comparator { left: bar_any.clone() };
    println!("Compare bar -> bar: {}", comparator.compare(&bar_any)); // true
    println!("Compare bar -> bar2: {}", comparator.compare(&bar_any2));

    let qux: Arc<Mutex<dyn IBar>> = Arc::new(Mutex::new(Qux { bar: Bar {} }));
    let qux_any: Arc<dyn Any + Send + Sync> = Arc::new(qux.clone());

    let comparator = Comparator { left: qux_any.clone() };
    println!("Compare qux -> qux: {}", comparator.compare(&qux_any)); // true
    
    let qux_any2: Arc<dyn Any + Send + Sync> = Arc::new(qux.clone());
    println!("Compare qux -> qux2: {}", comparator.compare(&qux_any2));
}

Standard Output

Compare foo -> foo: true
Compare foo -> foo2: false
Compare bar -> bar: true
Compare bar -> bar2: false
Compare qux -> qux: true
Compare qux -> qux2: false

Expectation should return true for all comparisons

Playground: Rust Playground

You're taking your Arc<dyn IFoo>s and Arc<dyn IBar>s and wrapping them in newly created Arc<dyn Any>s — two levels of Arc. Is that really what you want to be able to compare, or is your goal to be able to compare an Arc<dyn IFoo> with an Arc<dyn IBar> and the Arc<dyn Any> is just your current attempt to get there?

Essentially comparing the underlying data of the same concrete type. wrappers can be arbitrary

Foo will be compared to Foo.
Bar will be compared to Bar
Qux will be compared to Qux.

This playground will better explain the intent. All of the comparisons are expected to return true.

It's impossible to define a (sound) comparison which will return true without changing something about the values you have. You have to use a different trait than Any, stop double-wrapping your Arcs, or both. So:

  • Do you need dyn Any in particular, or is it just how you are currently attempting to solve your problem?
  • Do you need to compare values that are inside doubly nested Arcs, or is it just how you are currently attempting to solve your problem?

Yes, this is an attempt. I’m using dyn Any because it can store arbitrary types, but I’m open to alternatives as long as they allow storing different types (Foo, Bar, Qux).

Initially, I had everything stored as trait objects, but by erasing them to Any before storing them inside the Comparator, I can get rid of the extra level of Arc.

Ultimately, what I need is a way to compare the underlying concrete types consistently: (Wrapping or Erasing types is your call)

  • IFoo against IFoo
  • IBar against IBar
  • and special cases like Qux, which implements IBar.

So yeah, my approach can be wrong, so any lateral thinking please.

If it helps, we should only store and compare either Arc types or Arc<Mutex types, let me know.

Worked out: Rust Playground

Please review.

If there are only a handful of dyn Trait + pointer (Arc<_>, Arc<Mutex<_>>) to support, why not use an enum?

If there's a solution using raw pointers, without hard coding types, that'd be elegant.

I'm still not sure whether you are intending to have the effect of the Arc::new(foo.clone()) or not — it is creating a new Arc allocation that wraps the Arc<Mutex<dyn IFoo>> inside, rather than casting the Arc<dyn IFoo> to Arc<dyn Any>. Your as_any() methods are going completely unused. (They're also unnecessary since Rust 1.86 stabilized trait upcasting.)

Assuming that your application has no other use for those wrappings and it is just serving the goal of comparison, here's how to do the comparison with no extra wrapping, making use of true trait upcasting. The Mutex makes things a tricky, but we can hide it away by erasing to a helper trait implemented for the Mutex:

use std::any::Any;
use std::sync::{Arc, Mutex};

pub trait IFoo: Any + Send + Sync {}
pub struct Foo {}
impl IFoo for Foo {}

pub trait IBar: Any + Send + Sync {}
pub struct Bar {}
impl IBar for Bar {}

pub trait IBarMutex: Any + Send + Sync {}
impl<T: IBar> IBarMutex for Mutex<T> {}

struct Qux {
    bar: Bar,
}
impl IBar for Qux {}

pub struct Comparator {
    left: Arc<dyn Any + Send + Sync>,
}

impl Comparator {
    pub fn compare(&self, right: &Arc<dyn Any + Send + Sync>) -> bool {
        <dyn Any>::type_id(&*self.left) == <dyn Any>::type_id(&**right)
            && Arc::as_ptr(&self.left).cast::<()>() == Arc::as_ptr(right).cast::<()>()
    }
}

fn main() {
    let foo: Arc<dyn IFoo> = Arc::new(Foo {});
    let foo_any: Arc<dyn Any + Send + Sync> = foo.clone();
    let foo_any2: Arc<dyn Any + Send + Sync> = foo.clone();

    let comparator = Comparator {
        left: foo_any.clone(),
    };
    println!("Compare foo -> foo: {}", comparator.compare(&foo_any));
    println!("Compare foo -> foo2: {}", comparator.compare(&foo_any2));

    let bar: Arc<dyn IBarMutex> = Arc::new(Mutex::new(Bar {}));
    let bar_any: Arc<dyn Any + Send + Sync> = bar.clone();
    let comparator = Comparator {
        left: bar_any.clone(),
    };
    println!("Compare bar -> bar: {}", comparator.compare(&bar_any));

    let bar2: Arc<dyn IBarMutex> = Arc::new(Mutex::new(Bar {}));
    let bar_any2: Arc<dyn Any + Send + Sync> = bar.clone();
    let comparator = Comparator {
        left: bar_any2.clone(),
    };
    println!("Compare bar2 -> bar2: {}", comparator.compare(&bar_any2));

    let qux: Arc<dyn IBarMutex> = Arc::new(Mutex::new(Qux { bar: Bar {} }));
    let qux_any: Arc<dyn Any + Send + Sync> = qux.clone();

    let comparator = Comparator {
        left: qux_any.clone(),
    };
    println!("Compare qux -> qux: {}", comparator.compare(&qux_any));

    let qux_any2: Arc<dyn Any + Send + Sync> = qux.clone();
    println!("Compare qux -> qux2: {}", comparator.compare(&qux_any2));

    let foo_any3: Arc<dyn Any + Send + Sync> = foo.clone();
    println!("Compare qux -> foo3: {}", comparator.compare(&foo_any3));
}

Thanks for the attempt. IBarMutex will be a problem, joining types for existing types will not be scalable.

Is there a type agnostic solution? if we decide to keep the Arc/Mutex type consistent for all types.

To be clear, you don't need a new trait like trait IBarMutex for every type, only every trait. But you do need it, I think — unfortunately, there's no way to get a TypeId for the contents of a Mutex<dyn Trait> without locking the mutex, even though that is possible in theory.

An alternative you should consider — which may or may not be applicable to your real use case since you haven't shown us what IFoo and IBar do — is keeping all Mutexes out of your library code, and making it the responsibility of the IBar implementor to use a mutex or other interior mutability in their own code. This has a number of advantages:

  • The TypeId is easily accessible and so no IBarMutex trait is needed for comparison.
  • If the trait has any operations that don't read or write the mutable data, the implementor can implement them without locking the mutex.
  • The implementor can use other interior mutability strategies to improve throughput and efficiency, such as plain atomics, or lock-free or sharded data structures, that are suited to the data they need to mutate.
  • Even if the implementor definitely needs a Mutex, they may be able to do some of their work before or after locking it, reducing the time it spends locked.

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.