&mut dyn Trait in static variable

I have a structure that represents a cache and requires a reference to a MemoryBackend trait object for work.

I accept the trait-object reference and it works fine in normal situations.

#[no_std] // Important
pub struct Cache<'a, T> {
    memory_backend: &'a mut dyn MemoryBackend,
    phantom_data: core::marker::PhantomData<T>,
}

pub trait MemoryBackend {}

However, I needed to store the Cache object as a static variable:

use spin::{Mutex, Once};

static SLAB_INFO_CACHE: Mutex<Once<Cache<SlabInfo>>> = Mutex::new(Once::new());

And I'm having problems:

error[E0277]: `(dyn MemoryBackend + 'static)` cannot be sent between threads safely
5  | static SLAB_INFO_CACHE: Mutex<Once<Cache<SlabInfo>>> = Mutex::new(Once::new());
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn MemoryBackend + 'static)` cannot be sent between threads safely
   |
   = help: within `slab_allocator::Cache<'static, SlabInfo>`, the trait `Send` is not implemented for `(dyn MemoryBackend + 'static)`
   = note: required because it appears within the type `&'static mut (dyn MemoryBackend + 'static)`

16 | pub struct Cache<'a, T> {
   |            ^^^^^
   = note: required for `spin::once::Once<slab_allocator::Cache<'static, SlabInfo>>` to implement `Send`
   = note: required for `Mutex<spin::once::Once<slab_allocator::Cache<'static, SlabInfo>>>` to implement `Sync`
   = note: shared static variables must have a type that implements `Sync`

I don't quite understand this error, I guess it is related to the fact that the trait object is only MemoryBackend and not Sync + Send.
I can't just go ahead and change the memory_backend: &‘a mut dyn MemoryBackend field to memory_backend: &’a mut (dyn MemoryBackend + Send + Sync), as that would force these traits to be implemented when not required.
I could try to force Cache to own MemoryBackend, but since I'm in a no_std environment, I can't use Box.
It would be cool to have something in the style
if lifetime is not static: &'a mut (dyn MemoryBackend)
if lifetime is static: &'a mut (dyn MemoryBackend + Send + Sync)

How can I solve this problem?

Rust doesn't need &mut for mutating things. Caches can mutate through & by using interior mutability.

&mut is primarily a guarantee that this is exclusive access, and only the current exclusive owner of this object/variable can use it.

Global variables mean that anyone from anywhere can access it, which is in direct conflict with exclusive references.

Rust will force you to wrap all global mutable/exclsuive state, absolutely anything that can have more than one potential user, to be inside some kind of concurrency control - a mutex, lazy init, atomic write. Or inside UnsafeCell with some kind of control implement by you.

2 Likes

It would be wrong to accept MemoryBackend by shared reference, because then it can be used by another cache and its internal state can be broken.
I suppose it is worth taking ownership of the MemoryBackend trait object, but I don't know how to implement it in no_std.

There are some heapless/fixed-size Box crates (but I don't know enough to suggest a particular one).

On this topic:

You could use a generic T: ?Sized instead and add + MemoryBackend [+ Send + Sync] on implementations to support both use cases.

(Or perhaps impl<U: ?Sized + MemoryBackend> MemoryBackend for &mut U,[1] and just store a T in the field instead of &mut T.[2])


  1. and Box<U> etc ↩︎

  2. or Box<T> ↩︎

3 Likes

This doesn't make any difference. Note by the way, that due to usage of Mutex, the compiler was actually only asking for Send, not Send + Sync, and it would also ask for that if the trait object was owned; the thread-safety story of T and &mut T is identical.

Also, Box isn't uncommon in no_std; you "just" need an allocator, not full std. OTOH, I'm confused you mention Mutex since that should be limited to std?

They’re using the spin versions of these.

1 Like

Ah, should have checked the imports.

It’s always a bit of an annoying problem when you want “just a handful” of versions of something in Rust. Either it’s code duplication – which can be annoying to maintain, but relevant when two APIs are only very similar on the surface (e.g. Rc vs Arc)… Or you use a macro, but that can be annoying, too, because of tooling[1] and the resulting APIs can’t be abstracted over, either![2] Or you use generics; but that can make the API surface seem much more complicated [look at examples like e.g. arc-swap or ndarray].

In order to use generics, you’ll have to identify, or define, the right trait (or traits) to encapsulate the differences of your list of “versions of something”. If such traits seem useful to generalize, you could keep them as part of your API, making your types even more extensible – if it’s rather just an internal interface, then using a sealed trait pattern, and hiding the internals from rustdoc, can be a reasonable approach. And you can use type aliases to offer a “simplified” way of using your API.

For this last point, actually… not so long ago, the ergonomics of using generics was helped out a little bit by rustdoc becoming able to generate more complete pages (including all the methods and such) for type aliases. (You can see this well in the same two crates again.)[3]

For your particular use-case, generics are probably the easiest choice. (As @quinedot already suggested.) This is because the trait MemoryBackend itself already provides the interface over which you should (probably?) be able to abstract.

#![no_std]

pub struct Cache<T, B: MemoryBackend> {
    memory_backend: B,
    phantom_data: core::marker::PhantomData<T>,
}

pub trait MemoryBackend {
    fn some_apy(&mut self, x: usize, y: usize);
}

// lift the implementation to &mut T, which allows users to also supply
// concrete, owned, implementations to be passed
impl<B: ?Sized> MemoryBackend for &mut B where B: MemoryBackend {
    fn some_apy(&mut self, x: usize, y: usize) {
        B::some_apy(self, x, y) // delegate method to `&mut **self`
                                // (but the dereferencing can be left implicit)
    }
}

// finally, define aliases for the main intended use-cases:

pub type StaticCache<T> = Cache<T, &'static (dyn MemoryBackend + Send)>;

pub type NonStaticCache<'a, T> = Cache<T, &'a dyn MemoryBackend>;

That being said, if this whole codebase is very much unfinished yet, it can be a good idea to start out with something simpler (e.g. just unconditionally add the + Send to the dyn MemoryBackend for now), and then refactor to introduce the generic afterwards.[4]


  1. IDE support is worse with code in a macro; rustdoc can’t link an interested reader to the correct source code – yes, I’m always annoyed when I try to look as any particular functionality of our i…/u… integer types… ↩︎

  2. Once again, Rc/`Arc, or Rust’s number types, are good examples; they don’t come with a trait-or-something to abstract over them. ↩︎

  3. Ultimately though, I believe there’s still room for improvement. For example, with inspiration from ml-style modules ~ or perhaps also from C++ templates, for a less unstructured-raw-syntax-level alternative for macros – though that’s probably too huge a language feature.

    Or, if we start from other end, of using generics, and declaring type aliases for conveninence: with a good system for delegations, you could perhaps implement using generics, but then wrap a handful of concrete instantiations into newtype-style structs (instead of mere type aliases), and then lift all the relevant methods and trait implementations.
    But such a feature is also pretty hard to design… ↩︎

  4. As soon as you add it to the Cache type itself, compiler errors should quickly point to all the right places you need to touch next, until your done. ↩︎

3 Likes