Is transmuting PhantomData markers safe?
Asked Answered
O

1

8

This is taken out of context so it might seem a bit weird, but I have the following data structure:

use std::marker::PhantomData;

pub struct Map<T, M=()> {
    data: Vec<T>,
    _marker: PhantomData<fn(M) -> M>,
}

Map is an associative map where keys are "marked" to prevent using keys from one map on another unrelated map. Users can opt into this by passing some unique type they've made as M, for example:

struct PlayerMapMarker;
let mut player_map: Map<String, PlayerMapMarker> = Map::new();

This is all fine, but some iterators (e.g. the ones giving only values) I want to write for this map do not contain the marker in their type. Would the following transmute be safe to discard the marker?

fn discard_marker<T, M>(map: &Map<T, M>) -> &Map<T, ()> {
    unsafe { std::mem::transmute(map) }
}

So that I could write and use:

fn values(&self) -> Values<T> {
    Values { inner: discard_marker(self).iter() }
}

struct Values<'a, T> {
    inner: Iter<'a, T, ()>,
}
Ormiston answered 21/10, 2018 at 3:40 Comment(5)
Since fn(M) -> M has static lifetime and does not implement Drop, transmuting PhantomData<fn(M) -> M> to PhantomData<()> shouldn't have any observable effect except for type checking.Mardellmarden
Is there more than one non zero-sized field in the struct?Sade
@Sade In the real struct, yes.Ormiston
This doesn't answer the question, but in terms of design, I would rather seek to newtype those markers instead of making that a logic of the container. Thus, a value of struct PlayerMapMarker(String) could not be added to a Map<ItemMapMarker>.Smearcase
@Smearcase As I said, it doesn't make that much sense taken out of context. In the real data structure it is the keys that are marked, and the user doesn't get to choose their own key type, it is always the same.Ormiston
M
5

TL;DR: Add #[repr(C)] and you should be good.


There are two separate concerns here: Whether the transmute is valid in the sense of returning valid data at the return type, and whether the entire thing violates any higher-level invariants that might be attached to the involved types. (In the terminology of my blog post, you have to make sure that both validity and safety invariants are maintained.)

For the validity invariant, you are in uncharted territory. The compiler could decide to lay out Map<T, M> very differently from Map<T, ()>, i.e. the data field could be at a different offset and there could be spurious padding. It does not seem likely, but so far we are guaranteeing very little here. Discussion about what we can and want to guarantee there is happening right now. We purposefully want to avoid making too many guarantees about repr(Rust) to avoid painting ourselves into a corner.

What you could do is to add repr(C) to your struct, then I am fairly sure you can count on ZSTs not changing anything (but I asked for clarification just to be sure). For repr(C) we provide more guarantees about how the struct is laid out, which in fact is its entire purpose. If you want to play tricks with struct layout, you should probably add that attribute.

For the higher-level safety invariant, you must be careful not to create a broken Map and let that "leak" beyond the borders of your API (into the surrounding safe code), i.e. you shouldn't return an instance of Map that violates any invariants you might have put on it. Moreover, PhantomData has some effects on variance and the drop checker that you should be aware of. With the types that are being transmuted being so trivial (your marker types don't require dropping, i.e. them and their transitive fields all do not implement Drop) I do not think you have to expect any problem from this side.

To be clear, repr(Rust) (the default) might also be fine once we decide this is something we want to guarantee -- and ignoring size-0-align-1 types (like PhantomData) entirely seems like a pretty sensible guarantee to me. Personally though I'd still advise for using repr(C) unless that has a cost you are not willing to pay (e.g. because you lose the compilers automatic size-reduction-by-reordering and cannot replicate it manually).

Mimeograph answered 23/10, 2018 at 7:8 Comment(2)
repr(transparent) should also be valid, shouldn't it?Verbenia
@MarinVeršić yesMimeograph

© 2022 - 2024 — McMap. All rights reserved.