Passing Weak<dyn trait> to a function

Consider the following program:

use std::sync::Arc;
use std::sync::Weak;

trait CallMe {
    fn cb(&self);
}

struct Caller {
    cb: Weak<dyn CallMe>,
}

impl Caller {
    fn do_call(&self) {
	match self.cb.upgrade() {
	    None => (),
	    Some(cb) => cb.cb()
	}
    }
}

fn new_caller(cb: Weak<dyn CallMe>) -> Caller {
    Caller { cb }
}

struct DoCallMe;

impl CallMe for DoCallMe {
    fn cb(&self) {
	println!("I was called!");
    }
}

fn main() {
    let cm = Arc::new(DoCallMe{});
    //let cmw = Arc::downgrade(&cm);
    //let c = new_caller(cmw);
    let c = new_caller(Arc::downgrade(&cm));
    c.do_call()
}

This fails to compile with:

error[E0308]: mismatched types
  --> src/main.rs:38:39
   |
38 |     let c = new_caller(Arc::downgrade(&cm));
   |                        -------------- ^^^ expected trait object `dyn CallMe`, found struct `DoCallMe`
   |                        |
   |                        arguments to this function are incorrect
   |
   = note: expected reference `&Arc<dyn CallMe>`
              found reference `&Arc<DoCallMe>`

However, if I uncomment the commented lines

    let cmw = Arc::downgrade(&cm);
    let c = new_caller(cmw);

and remove

let c = new_caller(Arc::downgrade(&cm));

it works fine. This seems strange to me. It's not a big deal, but a little annoying. Why does this happen? I'm worried that I'm missing something bigger here. Those seem functionally equivalent to me.

For background, I am creating a binding for a library that does callbacks, at GitHub - cminyard/rust-gensio: Rust bindings to the gensio library

I am using weak references to avoid circular references were a user might install a callback that then references the struct that's holding the callback. That does put the onus on the user to keep the struct around, as if it gets automatically freed the callback will stop working. But it seems better than the circular references.

-corey

it seems a compiler bug to me, but I'm not sure. I don't know all the rules regarding the unsize coercion, why CoerceUnsized is not automatically applied if don't use named variable?. but if you do the coercion explicitly, it compiles, like this:

    let c = new_caller(Arc::downgrade(&cm) as Weak<dyn CallMe>);

bug or not, I'm not sure, but this behavior is definitely surprising to me.

I think what's happening is:

  • the compiler sees Arc::downgrade and tries to infer the type parameter T of the Arc (this is missing when you use a binding)
  • it sees it produces a Weak<T> and a Weak<dyn CallMe> is expected, hence T=CallMe (this is the problematic step, here T should be DoCallMe and then the Weak<DoCallMe> should be coerced)
  • then downgrade takes a &Arc<dyn CallMe>
  • but you're passing a &Arc<DoCallMe>
  • unsize coercion cannot be applied through references, so it doesn't work here
  • hence you get an error due to types not matching

The compiler is probably too eager to fix the type of T, but I'm pretty sure there are situations where being less eager would cause the same issue, so I don't think there's an easy fix to this problem.

3 Likes

There's a general principle here – type inference works best when there are no subtypes and no coercions, and every expression has a uniquely defined type (possibly containing type variables). In that case, there are no priority decisions to make and it doesn't matter what order the types are resolved in, because there are never multiple possible solutions. But Rust is not such a language, so sometimes not specifying types doesn't work even though it “could”.

Ok, I understand that, but it was just strange that I could assign it to
a variable and then it worked. In my mind, the same type would be
assigned to the variable and to the expression. The same type solutions
should still exist.

-corey

You can be less explicit when nudging the compiler to see where the coercion should be:

    let cm = Arc::new(DoCallMe {});
    let c = new_caller(Arc::downgrade(&cm) as _);
    //                                    ^^^^^

Or perhaps

    let cm = Arc::new(DoCallMe {}) as _;
    //                            ^^^^^
    let c = new_caller(Arc::downgrade(&cm));

(Note that the latter one causes cm to be unsize coerced (changes its type) and not just the Weak.)

1 Like