Lifetime transmutation of Generic Associated Types does not work

Writing a Rust crate for LMDB, I came across the following problem:

impl<'a> TxnRo<'a> {
    /// Get value from database
    pub fn get<'b, K, V>(
        &'b self,
        db: &Db<K, V>,
        key: &K,
    ) -> Result<Option<V::AlignedRef<'b>>, io::Error>
    where
        K: ?Sized + Storable,
        V: ?Sized + Storable,
    {
        let mut backend = self.backend.lock();
        unsafe {
            let result = backend.get::<_, K, V>(self.env, db, key)?;
            Ok(std::mem::transmute::<
                Option<V::AlignedRef<'_>>,
                Option<V::AlignedRef<'b>>,
            >(result))
        }
    }
}

Error:

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
   --> src/lib.rs:930:16
    |
930 |               Ok(std::mem::transmute::<
    |  ________________^
931 | |                 Option<V::AlignedRef<'_>>,
932 | |                 Option<V::AlignedRef<'b>>,
933 | |             >(result))
    | |_____________^
    |
    = note: `Option<<V as Storable>::AlignedRef>` does not have a fixed size

For more information about this error, try `rustc --explain E0512`.

However, I would argue that the two types V::AlignedRef<'lt> and V::AlignedRef<'b> would always have the same size.

Is this a bug?

Maybe I can workaround it by avoiding the transmutation (by modifying my backend.get method), but I still think the error is unjustified?


I can indeed work around this problem as follows:

impl TxnBackend {
    unsafe fn get_unsafe<'a, E, K, V>(
        &mut self,
        env: &E,
        db: &Db<K, V>,
        key: &K,
    ) -> Result<Option<V::AlignedRef<'a>>, io::Error>
    where
        E: Env,
        K: ?Sized + Storable,
        V: ?Sized + Storable,
    {
        /* … */
    }
    /* … */
}

impl<'a> TxnRo<'a> {
    /// Get value from database
    pub fn get<'b, K, V>(
        &'b self,
        db: &Db<K, V>,
        key: &K,
    ) -> Result<Option<V::AlignedRef<'b>>, io::Error>
    where
        K: ?Sized + Storable,
        V: ?Sized + Storable,
    {
        let mut backend = self.backend.lock();
        unsafe { Ok(backend.get_unsafe::<_, K, V>(self.env, db, key)?) }
    }
}

But I still think the compiler error E0512 is not correct in the above case.

Yeah, transmute sanity checks use some heuristics which are not exhaustive, so may have false positives.

In that case you could the following helper:

use ::core::mem;

trait AssertSameSize {
    const MONOMORPHIZATION_CHECK: ();
}
impl<T, U> AssertSameSize for (T, U) {
    const MONOMORPHIZATION_CHECK: () = {
        if mem::size_of::<T>() != mem::size_of::<U>() {
            panic!("Not the same size!");
        }
    };
}

unsafe
fn transmute<T, U> (src: T)
  -> U
{
    let _check: () = <(T, U) as AssertSameSize>::MONOMORPHIZATION_CHECK;
    mem::transmute_copy(&*mem::ManuallyDrop::new(src))
}

it uses a monomorphization check, which means that once that generic function is compiled downto actual machine code (codegen phase), it has 100% accuracy for the check, so that, if it compiles, you have the peace of mind that no reachable runtime code will be able to involve a transmute with a size-mismatch.

  • The issue being that not all Rust code makes it to codegen (mainly, generic code), and codegen itself isn't triggered on cargo check, so that you may think your code is fine only to stumble upon a compilation error later on. This is the main drawback of post-monomorphization errors, which makes them a poor solution for libraries.

  • Libraries could thus simply use a good old assert! rather than a "const assertion", so that at least the library is guaranteed to compile; and the "unlikely scenario" (size mismatch) replaces UB with a panic.

  • In this specific scenario where a type differs in only one lifetime, you technically don't need the checks since it is known to be equally-sized.

1 Like

Thank you for your response and the idea with the monomorphization check. I'm not sure if mem::transmute_copy(&*mem::ManuallyDrop::new(src)) would be zero-cost though?

For me, the best solution probably is to not use transmute and instead (implicitly) pass a lifetime to get_unsafe as in my last code block.

Interestingly, I wasn't able to pass the lifetime explicitly to get_unsafe:

warning: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
   --> src/lib.rs:914:42
    |
861 |         &mut self,
    |         - the late bound lifetime parameter is introduced here
...
914 |         unsafe { Ok(backend.get_unsafe::<'b, _, K, V>(self.env, db, key)?) }
    |                                          ^^
    |
    = note: `#[warn(late_bound_lifetime_arguments)]` on by default
    = warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
    = note: for more information, see issue #42868 <https://github.com/rust-lang/rust/issues/42868>

But if I omit the 'b from the turbofish it works fine.

Now I totally forgot again what late-boundedness was. It had to do something with passing lifetimes explicitly. What was this rhyme again? … Late bound, un-sound, spinning around… :face_with_spiral_eyes:

No, it was:

Yeah, you'd need to change the generic <'b> parameter in the definition of get_unsafe to become 'b : 'b. This makes it so that parameter has "a (phony) bound" associated to it, which makes it "non-trivial" for rust's heuristics, which in turn makes it (early-bound, and thus) turbofishable (otherwise a "trivial" lifetime parameter is late-bound / higher-order, and thus, non-turbofishable…).

In the case of unsafe functions, I personally find it to be a good thing to do, precisely because turbofishing lifetimes reduces the leeway that lifetimes and type inference have :100:

1 Like