genericsrustthread-safetyphantom-types

How do I share a struct containing a phantom pointer among threads?


I have a structure that needs to be generic over a type, yet the type is not actually contained in the structure: it's used in methods of this structure, not in the structure itself. And so, the structure includes a PhantomData member:

pub struct Map<T> {
    filename: String,
    phantom: PhantomData<*const T>,
}

The phantom member is defined as a pointer because the structure does not actually own data of type T. This is per advice in the documentation of std::marker::PhantomData:

Adding a field of type PhantomData<T> indicates that your type owns data of type T. This in turn implies that when your type is dropped, it may drop one or more instances of the type T. This has bearing on the Rust compiler's drop check analysis.

If your struct does not in fact own the data of type T, it is better to use a reference type, like PhantomData<&'a T> (ideally) or PhantomData<*const T> (if no lifetime applies), so as not to indicate ownership.

So the pointer seems to be the right choice here. This, however, causes the structure to no longer be Send nor Sync, because PhantomData is only Send and Sync if its type parameter is, and since pointers are neither, the whole thing isn't either. And so, code like this

// Given a master_map of type Arc<Map<Region>> ...
let map = Arc::clone(&master_map);

thread::spawn(move || {
    map.do_stuff();
});

fails to compile even though no Region values or even pointers are being moved:

error[E0277]: the trait bound `*const Region: std::marker::Send` is not satisfied in `Map<Region>`
  --> src/main.rs:57:9
   |
57 |         thread::spawn(move || {
   |         ^^^^^^^^^^^^^ `*const Region` cannot be sent between threads safely
   |
   = help: within `Map<Region>`, the trait `std::marker::Send` is not implemented for `*const Region`
   = note: required because it appears within the type `std::marker::PhantomData<*const Region>`
   = note: required because it appears within the type `Map<Region>`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<Map<Region>>`
   = note: required because it appears within the type `[closure@src/main.rs:57:23: 60:10 map:std::sync::Arc<Map<Region>>]`
   = note: required by `std::thread::spawn`

error[E0277]: the trait bound `*const Region: std::marker::Sync` is not satisfied in `Map<Region>`
  --> src/main.rs:57:9
   |
57 |         thread::spawn(move || {
   |         ^^^^^^^^^^^^^ `*const Region` cannot be shared between threads safely
   |
   = help: within `Map<Region>`, the trait `std::marker::Sync` is not implemented for `*const Region`
   = note: required because it appears within the type `std::marker::PhantomData<*const Region>`
   = note: required because it appears within the type `Map<Region>`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<Map<Region>>`
   = note: required because it appears within the type `[closure@src/main.rs:57:23: 60:10 map:std::sync::Arc<Map<Region>>]`
   = note: required by `std::thread::spawn`

Here's a complete test case in the playground that exhibits this issue:

use std::fmt::Debug;
use std::marker::PhantomData;
use std::sync::Arc;
use std::thread;

#[derive(Debug)]
struct Region {
    width: usize,
    height: usize,
    // ... more stuff that would be read from a file
}

#[derive(Debug)]
struct Map<T> {
    filename: String,
    phantom: PhantomData<*const T>,
}

// General Map methods
impl<T> Map<T>
where
    T: Debug,
{
    pub fn new<S>(filename: S) -> Self
    where
        S: Into<String>,
    {
        Map {
            filename: filename.into(),
            phantom: PhantomData,
        }
    }

    pub fn do_stuff(&self) {
        println!("doing stuff {:?}", self);
    }
}

// Methods specific to Map<Region>
impl Map<Region> {
    pub fn get_region(&self) -> Region {
        Region {
            width: 10,
            height: 20,
        }
    }
}

fn main() {
    let master_map = Arc::new(Map::<Region>::new("mapfile"));
    master_map.do_stuff();
    let region = master_map.get_region();
    println!("{:?}", region);

    let join_handle = {
        let map = Arc::clone(&master_map);
        thread::spawn(move || {
            println!("In subthread...");
            map.do_stuff();
        })
    };

    join_handle.join().unwrap();
}

What is the best way to deal with this? This is what I've tried:

Defining the phantom field as PhantomData<T>. A proper value instead of a pointer. This works, but I'm wary of it because I've no idea what effect it has, if any, on Rust compiler's "drop check analysis", as per the docs quoted above.

Defining the phantom field as PhantomData<&'a T>. A reference. This should work, but it forces the structure to take an unneeded lifetime parameter, which propagates through my code. I'd rather not do this.

Forcing the structure to implement Send and Sync. This is what I'm actually doing at the moment:

unsafe impl<T> Sync for Map<T> {}
unsafe impl<T> Send for Map<T> {}

It seems to work, but those unsafe impls are ugly and make me nervous.

To clarify what T is used for: It doesn't matter, really. It may not even be used, just provided as a marker for the type system. E.g. only needed so that Map<T> has a type parameter so different impl blocks can be provided:

impl<T> struct Map<T> {
    // common methods of all Maps
}

impl struct Map<Region> {
    // additional methods available when T is Region
}

impl struct Map<Whatever> {
    // additional methods available when T is Whatever, etc.
}

Solution

  • There's another option: PhantomData<fn() -> T>. fn() -> T has the same variance as T and *const T, but unlike *const T, it implements both Send and Sync. It also makes it clear that your struct only ever produces instances of T. (If some methods take T as input, then PhantomData<fn(T) -> T> might be more appropriate).

    #[derive(Debug)]
    struct Map<T> {
        filename: String,
        phantom: PhantomData<fn() -> T>,
    }