Arc<Mutex<Box<SharedStruct>>> downcast to Arc<Mutex<Box<dyn SomeTrait>>> and keep/preserve the same Mutex for both

Hello everyone,

I am searching for a neat way to implement the following design idea.

There is a struct that is shared in multi-threaded async code. Like:


#[derive(Default)]
struct Shared {
    val: usize,
}

fn main() {
    let shared = Arc::new(Mutex::new(Box::new(Shared::default())));
    // ..
}

Then i pass shared to tokio::spawn by using shared.clone() and whenever data is being modified, use shared.lock().await and stuff, so far so good.

Now the thing is that Shared must implement SomeTrait and trait implementation code should lock Shared before accessing it as well, so i would like to pass something like this to external library:

impl SomeTrait for Shared {}

let shared_dyn: Arc<Mutex<Box<dyn SomeTrait>>> = Arc::new(Mutex::new(Box::new(Shared::default())));
external_function_call(shared_dyn);

Then in shared library code could look like:

async fn external_function_call(shared_dyn: Arc<Mutex<Box<dyn SomeTrait>>>) {
    let shared = shared_dyn.lock().await;
    shared.some_trait_method();
}

While conceptually this seems fine, practically i can not call a function by passing Boxed struct instead of Boxed dyn Trait:

// This does not work because Box<Shared> "kind of" is not Box<dyn SomeTrait>, although it implements SomeTrait.
let shared = Arc::new(Mutex::new(Box::new(Shared::default())));
external_function_call(shared)

What i liked about this code idea is that, within library code it is explicitly written where locks are taken and freed, the main code that implements Shared struct can add it's own methods to it, maybe implement other traits.

The main problem with this is that the library code does not know the structure for Shared struct, thus it can not use Box downcast; but if i create new Mutex<Box<dyn Shared>>, then it is not locking the same struct.

I would like to avoid doing some trickery with unsafe code, or reimplement Mutex functionality in SomeTrait.

Does anyone know any idea of how this could be done in a nice way? Or what would be the best way to implement this?

It's okay to have some proxy objects or whatever, if it helps. The most important for me in this case is so that library implementation does not know internals of Shared struct, it must lock it while using trait methods. I would like so it has explicit locking code, not that trait method internally locks and library implementation does not know anything about it.

They are different types, yes. The reason the coercion doesn't work is because it's in a nested context.

You can (attempt to) downcast to &mut SharedStruct or &SharedStruct after locking the Mutex<dyn Shared>, if you add some boilerplate.


Edit: Oh yeah, and you don't need the Box since you're in an Arc<Mutex<_>>.

And you can coerce that one.

4 Likes

Thank you for the ideas. This really helps.

Valuable resource, link bookmarked. And thank you for contents there as well. Some of chapters i skimmed through seems a bit over my current level of understanding though :slight_smile: Must re-read multiple times so it sinks in.

To explore this a bit further, i modified the code you provided in playground to:

use std::sync::Arc;

// use std::sync::Mutex;
// use tokio::sync::Mutex;
use futures_locks::Mutex;

trait Shared: Send {}
struct SharedStruct {}

impl Shared for SharedStruct {}

fn used_as_struct(_shared: Arc<Mutex<SharedStruct>>){
    // do something with SharedStruct
}

fn use_trait(_shared: Arc<Mutex<dyn Shared>>) {
    // do something with SharedStruct that trait Shared implements
}

#[tokio::main]
async fn main() {
    let shared: Arc<Mutex<SharedStruct>> = Arc::new(Mutex::new(SharedStruct {}));
    use_trait(shared.clone());
    used_as_struct(shared);
}

The interesting thing is that with std::sync::Mutex and with tokio::sync::Mutex it compiles and works as intended (i tested with more code and locking in function bodies), but with futures_locks::Mutex compilation fails:

   Compiling test_mutex_trait v0.1.0 (/service/sandbox/rust/test_mutex_trait)
error[E0308]: mismatched types
  --> src/main.rs:24:15
   |
24 |     use_trait(shared.clone());
   |     --------- ^^^^^^^^^^^^^^ expected trait object `dyn Shared`, found struct `SharedStruct`
   |     |
   |     arguments to this function are incorrect
   |
   = note: expected struct `Arc<futures_locks::Mutex<(dyn Shared + 'static)>>`
              found struct `Arc<futures_locks::Mutex<SharedStruct>>`
note: function defined here
  --> src/main.rs:17:4
   |
17 | fn use_trait(_shared: Arc<Mutex<dyn Shared>>) {
   |    ^^^^^^^^^ -------------------------------

For more information about this error, try `rustc --explain E0308`.
error: could not compile `test_mutex_trait` due to previous error

What should have been added to futures_locks crate so this code can be compiled?

What i would like to improve for this code though is to allow to use any type of async Mutex. At the moment, if in the reusable library code i decide to use tokio Mutex, tokio must be used everywhere... so it is kind of a dependency-lock-in.

futures_lock::Mutex already wraps an Arc (which adds a layer of indirection like Box in the initial example), while std::sync::Mutex and tokio::sync::Mutex do not. Since it wraps an Arc you can just remove the outer Arc, which is useless. However you still can't coerce a futures_lock::Mutex<SharedStruct> to a futures_lock::Mutex<dyn Shared> because it doesn't implement the unstable CoerceUnsized trait (which Arc does implement)

2 Likes

This explains it. So CoerceUnsized is the trait that does the magic. Thanks.

What's more, this kind of highlights that code for libraries that one uses must be studied as well. I assumed that Mutex from futures_lock crate kind of behaves like std, so wrap it in Arc. My assumption was wrong. I guess i should make a habit to always skim the source for main struct (it's not too hard since standard docs have url to it anyways).

Thanks guys. This has helped me to move forward.

Yeah the source link is really really useful for quickly looking at the source code of a crate and understanding what it's doing under the hood. In the case of futures_lock's Mutex it is also documented that it wraps an Arc, though I would still consider it surprising due to the naming.

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.