Skip to content

Instantly share code, notes, and snippets.

@PoignardAzur
Last active April 21, 2021 09:28
Show Gist options
  • Select an option

  • Save PoignardAzur/7c4eda06bf129df0a42cc9f55def4d3c to your computer and use it in GitHub Desktop.

Select an option

Save PoignardAzur/7c4eda06bf129df0a42cc9f55def4d3c to your computer and use it in GitHub Desktop.
Proposed alternative to Safe Transmute MCP - Visibility token

Safe transmute - visibility token

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 Context parameter of BikeshedIntrinsicFrom introduces new, non-obvious semantics. This is a problem both for implementing it in the compiler, and teaching it to new users.
  • Because Context is 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 unsafe even in places where the semantics should be provably safe. (though maybe some of the examples could be rewritten without unsafe)

Alternate proposal

#[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 visibility field is removed from Assume.
  • A new CanTransmuteFromFields traits 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
    }

Unanswered questions

  • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment