Search code examples
rustconcurrencythread-safetystatic-variables

In Rust, how to create a globally shared singleton with `OnceLock`?


Assuming that we need to define a singleton with global read access with thread safety, the canonical method is to use OnceLock:

/// A synchronization primitive which can be written to only once.
///
/// This type is a thread-safe [`OnceCell`], and can be used in statics.

static i1: OnceLock<Box<u64>> = OnceLock::new(); // this works
static i2: OnceLock<Box<dyn Sized>> = OnceLock::new(); // this doesnt:

the second line throw several errors that doesn't make sense:

error[E0038]: the trait `Sized` cannot be made into an object
   --> src/lib.rs:118:25
    |
118 | static i2: OnceLock<Box<dyn Sized>> = OnceLock::new();
    |                         ^^^^^^^^^ `Sized` cannot be made into an object
    |
    = note: the trait cannot be made into an object because it requires `Self: Sized`
    = note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>

Sized is a trait that has dynamic size, but Box is just a smart reference that points to a dynamically allocated struct later, and it is not even initialised in this definition, so why do I need make it into an object?

error[E0277]: `(dyn Sized + 'static)` cannot be shared between threads safely
   --> src/lib.rs:118:12
    |
118 | static i2: OnceLock<Box<dyn Sized>> = OnceLock::new();
    |            ^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Sized + 'static)` cannot be shared between threads safely
    |
    = help: the trait `Sync` is not implemented for `(dyn Sized + 'static)`
    = note: required for `Unique<(dyn Sized + 'static)>` to implement `Sync`
note: required because it appears within the type `Box<(dyn Sized + 'static)>`
   --> /home/peng/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:195:12
    |
195 | pub struct Box<
    |            ^^^
    = note: required for `OnceLock<Box<(dyn Sized + 'static)>>` to implement `Sync`
    = note: shared static variables must have a type that implements `Sync`

"Thread Safety" & "static" are literally in the documentation of "OnceLock", how could it be considered unsafe?

error[E0277]: `(dyn Sized + 'static)` cannot be sent between threads safely
   --> src/lib.rs:118:12
    |
118 | static i2: OnceLock<Box<dyn Sized>> = OnceLock::new();
    |            ^^^^^^^^^^^^^^^^^^^^^^^^ `(dyn Sized + 'static)` cannot be sent between threads safely
    |
    = help: the trait `Send` is not implemented for `(dyn Sized + 'static)`
    = note: required for `Unique<(dyn Sized + 'static)>` to implement `Send`
note: required because it appears within the type `Box<(dyn Sized + 'static)>`
   --> /home/peng/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:195:12
    |
195 | pub struct Box<
    |            ^^^
    = note: required for `OnceLock<Box<(dyn Sized + 'static)>>` to implement `Sync`
    = note: shared static variables must have a type that implements `Sync`

any static binding only has 1 replica in memory and is used by all threads in a process, there is no need to send them between threads like some distributed computing framework. How could the compiler never realised that?

So these 3 errors all appears to be clueless, how can I bypass these errors and make the static binding work with minimal overhead?


Solution

  • dyn Sized contradicts itself, Sized means, all items of a type have a statically known size, but Sized is implemented for a variety of types, (), u8, u16, … all with a different size, that means dyn Sized could have a size of 0 bytes, 1 byte, 2 bytes, … and you can change the underlying type at runtime so it is not statically known. That means a trait object (because it can be any type it's trait is implemented for) cannot implement Sized, but implementing the named trait is all we know a trait object does. In other words dyn Sized does not implement Sized but must implement Sized at the same time.

    The second and third error boil down to statics needing to be Sync (they can be accessed from any thread and Sync is the trait that tells the compiler it's safe to do so) but OnceCell only implements Sync if it's contents are Sync + Send. The source of OnceCell explains*:

    // Why do we need `T: Send`?
    // Thread A creates a `OnceLock` and shares it with
    // scoped thread B, which fills the cell, which is
    // then destroyed by A. That is, destructor observes
    // a sent value.
    

    A trait object however only implements the traits it lists, so dyn Trait does not implement either Send or Sync.

    You can achive what you seem to want simply by specifying an object safe trait and add the Send and Sync bounds to the trait object as well. For example with std::any::Any for any T: 'static type:

    static i2: OnceLock<Box<dyn Any + Send + Sync>> = OnceLock::new(); 
    

    *) That doesn't apply for your specific case, but does explain why OnceCell needs it's content to implement Send, to get around it one would need a distinct LeakingOnceCell which does not drop it's contents, it's usefulness seems questionable to me.