I'd like to present an alternative to the proposed safe-transmute MCP.
It's centered around three problems I think the current proposal has:
- The
Contextparameter ofBikeshedIntrinsicFromintroduces new, non-obvious semantics. This is a problem both for implementing it in the compiler, and teaching it to new users. - Because
Contextis automatically checked, type authors lose some expressivity; by default the ability to transmute a type is tied to its fields' visibility. This has implications for stability (this argument isn't as strong as I though it would be; Jack Wrenn makes some good cases as to how specific use-cases could be covered; still, I think a principled approach would be better) - Overall, use-cases proposed by the proposal author seem to require
unsafeeven in places where the semantics should be provably safe. (though maybe some of the examples could be rewritten without unsafe)
#[lang = "transmutability_trait"]
#[marker]
pub unsafe trait CanTransmuteFrom<Src, Token, const ASSUME: Assume>
where
Src: ?Sized
{}
#[lang = "transmutability_fields_trait"]
pub unsafe trait CanTransmuteFromFields<Token>
where
Src: ?Sized
{}
// pub unsafe trait CanTransmuteFromFields
#[lang = "transmutability_opts"]
#[derive(PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub struct Assume {
pub alignment : bool,
pub lifetimes : bool,
pub validity : bool,
}
impl Assume {
pub const NOTHING: Self = Self {
alignment : false,
lifetimes : false,
validity : false,
};
pub const ALIGNMENT: Self = Self {alignment: true, ..Self::NOTHING};
pub const LIFETIMES: Self = Self {lifetimes: true, ..Self::NOTHING};
pub const VALIDITY: Self = Self {validity: true, ..Self::NOTHING};
}
// ...The main changes from the core proposal are:
- The
visibilityfield is removed fromAssume. - A new
CanTransmuteFromFieldstraits is created.
This new trait replaces Context in the original proposal. This trait essentially means "You can transmute me to a tuple of my fields only if you give me the right Token type parameter".
Types that are trivially transmutable to their fields (eg (Foo, Bar, i32)) will implement CanTransmuteFromFields<(), ...>, meaning that any context can provide a token enabling that transmutation.
By default, types with private fields and #[non_exhaustive] types don't implement CanTransmuteFromFields.
However, if a module wants to declare a type Foobar, and be able to transmute that type (for instance, to build it in a zero-copy serialization crate), but doesn't want external code to transmute that type; then it can declare a private token:
pub struct Foobar {
foo: Foo,
bar: Bar,
// ...
};
struct FoobarTransmuteToken;
unsafe impl CanTransmuteFromFields<FoobarTransmuteToken> for Foobar;The intrinsic CanTransmuteFrom trait is only implemented for types that implement CanTransmuteFromFields. This means the only code that can transmute Foobar to access its fields, is code that can access the FoobarTransmuteToken type. Since that type is private, it means transmutation is restricted to the current module, which can enforce Foobar's invariants.
Safe APIs can be built on top of these traits:
pub fn safe_transmute<Dst, Src, Token>(src: Src) -> Dst
where
Dst: CanTransmuteFrom<Src, Token, Assume::NOTHING>
{
unsafe { mem::transmute(src) }
}
pub fn safe_transmute_ref<Dst, Src, Token>(src: &Src) -> &Dst
where
Dst: CanTransmuteFrom<Src, Token, Assume::NOTHING>
{
unsafe { mem::transmute(src) }
}
pub fn safe_transmute_unaligned_ref<Dst, Src, Token>(src: &Src) -> &Dst
where
Dst: CanTransmuteFrom<Src, Token, Assume::ALIGNMENT>
{
assert!((src as *Src as usize) % mem::align_of::<Dst>() == 0);
unsafe { mem::transmute(src) }
}
// --- ZERO-COPY ---
pub trait FromZeros<Token, const ASSUME: Assume> {}
#[repr(u8)]
pub enum Zero {
Zero = 0u8
}
impl<Dst, Token, const ASSUME: Assume> FromZeros<Token, ASSUME> for Dst
where
Dst: CanTransmuteFrom<[Zero; usize::MAX], Token, Assume::NOTHING>
{}
pub fn zeroed<Dst, Token>() -> Dst
where
Dst: FromZeros<Token, Assume::NOTHING>,
{
safe_transmute::<_, Token>([Zero; size_of::<Self>])
}Something to notice is that these APIs use very little unsafe code. The implementation of zeroed uses safe_transmute, and doesn't require the library writer to take responsibility for an invariant; the compiler takes care of making sure the type can indeed be transmuted from zeros.
A port of the MCP example use-case would look like:
mod a {
use super::*;
mod npc {
#[repr(C)]
pub struct NoPublicConstructor(u32);
struct PrivateToken;
impl NoPublicConstructor {
pub(super) fn new(v: u32) -> Self {
assert!(v % 2 == 0);
core::mem::safe_transmute::<_, PrivateToken>(v)
}
pub fn method(self) {
if self.0 % 2 == 1 {
// totally unreachable, thanks to assert in `Self::new`
unsafe { *std::ptr::null() }
}
}
}
}
use npc::NoPublicConstructor;
}
mod b {
use super::*;
fn new(v: u32) -> a::NoPublicConstructor {
assert_not_impl!(NoPublicConstructor: BikeshedIntrinsicFrom<u32, B>);
core::mem::safe_transmute::<_, PrivateToken>(v) // ERROR: Cannot access PrivateToken
}- How to export the ability to do only certain transmutations. Eg only allow transmutations from zeros.
- How to deal with recursive visibility, eg private fields of private fields; this is something the current MCP doesn't really explain either.