References to values smaller than references

Lets say you are working with some library that gives you a u32, representing a handle to some resource. You decide that you want to share an immutable reference and store it in a number of places in your code. Then you realize the references are taking up 64 bits (assuming thats the pointer width) but the value is only 32 bits of data. We are wasting space!

Here is an attempt to solve this. It implements SmallRef, a struct which contains a bitwise copy of the value that it "references". SmallRef also holds a borrow of the original value through PhantomData.

Edit: Added NoInteriorMutability trait.

extern crate core;

/// Represents a handle/key/name/id to some resource and ownership of it. 
/// Implements dropped and therefore cannot be copied. Used for illustration.
pub struct Resource(u32);

impl Resource {
    pub fn new(value: u32) -> Self {
        println!("Resource {} created.", value);
        Resource(value)
    }
    
    #[inline]
    pub fn as_u32(&self) -> u32 {
        self.0
    }
}

impl Drop for Resource {
    fn drop(&mut self) {
        println!("Resource {} dropped.", self.0);
    }
}

unsafe impl NoInteriorMutability for Resource {}

// ----------------------------------------

use core::marker::PhantomData;
use std::mem::ManuallyDrop;
use std::ptr;

// Make it less error prone to create PhantomData by having it consume the
// value.
fn phantom_data<T>(_: T) -> PhantomData<T> {
    PhantomData
}

/// Implement this when you can promise that the type does not use interior
/// mutability.
pub unsafe trait NoInteriorMutability {
}

/// NOTE: Don't expose this! It implements an unsafe trait!
macro_rules! impl_no_interior_mutability {
    ($($T:ty),+) => {
        $(
            unsafe impl NoInteriorMutability for $T {}
        )+
    };
}

/// Implement it for all elegible primitive types and  
impl_no_interior_mutability!(u8, u16, u32, u64, i8, i16, i32, i64, bool);

/// A type that acts like an immutable reference but holds a copy instead of an
/// actual pointer. Only makes sense for types that are not already copyable, do
/// not use interior mutability and are smaller than a pointer.
pub struct SmallRef<'a, T: 'a> where T: NoInteriorMutability {
    // Make sure we don't drop the value, we don't own it.
    value: ManuallyDrop<T>,
    // Use &'a T as the PhantomData type because we don't own the value.
    _borrow: PhantomData<&'a T>,
}

impl<'a, T: 'a> SmallRef<'a, T> where T: NoInteriorMutability {
    pub fn new(value: &'a T) -> Self {
        // Safe because the original value can not be modified while we hold the
        // borrow and because we don't drop the value.
        unsafe {
            SmallRef {
                value: ManuallyDrop::new(ptr::read(value)),
                _borrow: phantom_data(value),
            }
            // NOTE: Just ptr::read(value) *should* work too. This looks more
            // like I know what I'm doing though.
        }
    }
}

// NOTE: Unfortunately Rust prevents us from doing this even though it is safe.
// It looks like https://doc.rust-lang.org/std/cell/struct.Ref.html could use
// a Copy implementation if it were possible.
// impl<'a, T> Copy for SmallRef<'a, T> {
// }

/// Force a bitwise copy.
impl<'a, T: 'a> Clone for SmallRef<'a, T> where T: NoInteriorMutability {
    fn clone(&self) -> Self {
        // Safe because immutable references are Copy? For better argumentation
        // see `SmallRef::new`.
        unsafe {
            SmallRef {
                value: ManuallyDrop::new(ptr::read(&*self.value)),
                _borrow: self._borrow,
            }
            // NOTE: Just ptr::read(value) *should* work too. This looks more
            // like I know what I'm doing though.
        }
    }
}

impl<'a, T: 'a> std::ops::Deref for SmallRef<'a, T> where T: NoInteriorMutability {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &*self.value
    }
}

// ----------------------------------------

#[cfg(test)]
mod tests {
    use super::Resource;
    use super::SmallRef;

    #[test]
    fn resource_ref_is_size_of_resource() {
        use std::mem::size_of;
        assert!(size_of::<Resource>() == size_of::<SmallRef<Resource>>());
        assert!(size_of::<SmallRef<Resource>>() < size_of::<&Resource>());
    }
    
    #[test]
    fn deref_works() {
        let v = Resource::new(13);
        
        // NOTE: Ideally r2 implements Copy but havent't figured out how to do
        // that yet for arbitrarily sized T.
        let r1 = SmallRef::new(&v);
        let r2 = r1.clone();
        
        // Deref called implicitly.
        assert_eq!(r1.as_u32(), 13);
        assert_eq!(r2.as_u32(), 13);
    }
}

Playground

Does it work? Yes. Is it ergonomic? Meh, I would like to implement Copy but I haven't figured out how. I'd like to not resort to implementing all possible sizes smaller than the maximum pointer width we can expect in the nearby future.

Implementation wise it would be nice if we could do static assertions to ensure sizeof T < sizeof &T and not T: Copy. The first one is possible but the second one not yet (negative trait bounds or specialization would help).

Some questions:

  1. Is there a crate that implements this, I haven't been able to find one.

Doesn't look like there is. Implementing this turns out to be cumbersome so that might be a reason. Another reason is that it is uncommon to have multiple immutable references to non-copy values smaller than a reference.

  1. Can we implement Copy?

The only way I see is by storing the value in a data type that is Copy. Unfortunately it is not possible to do this automatically (#43408).

  1. Are the unsafe abstractions actually safe?

@cuviper mentioned that interior mutability violates the assumption that if we have an immutable reference to a value, the value cannot change.

We solved this by adding an unsafe trait called NoInteriorMutability and requiring everyone to manually implement the trait for their types if it holds.

  1. Is it worth it to use SmallRef as a parameter to non-inlined functions?

Perhaps, need to find a use case and measure it.

  1. Can we deal with interior mutability in a way that does not require everyone in the world to implement an unsafe trait?

@scottmcm points us to unsafe auto trait Freeze {}, a compiler internal trait that does exactly what we need but is currently private.

1 Like

What if T is something like a Cell? Then you'll have lost the appropriate interior mutability.

4 Likes

I think it's an interesting concept, a "reference" but not at run time but purely as compile-time hint regarding the life-span of the object represented by the handle.

1 Like

Good point! Interior mutability does indeed violate my assumption that if I hold a reference, the value cannot be changed.

One, not so ergonomic, way I see of working around this is adding an unsafe marker trait NoInteriorMutability that would have to be defined by users of SmallRef.

3 Likes

Ah! Is there any way to use that currently?

Also I'm really curious if anyone can provide ideas/guidance to force a copy implementation Copy, other than changing the type we hold and doing a transmute.

Updated the open questions, if you have something to share on any of these please do :)!

  1. Is there a crate that implements this, I haven't been able to find one.

Doesn't look like there is. Implementing this turns out to be cumbersome so that might be a reason. Another reason is that it is uncommon to have multiple immutable references to non-copy values smaller than a reference.

  1. Can we implement Copy?

The only way I see is by storing the value in a data type that is Copy. Unfortunately it is not possible to do this automatically (#43408).

  1. Are the unsafe abstractions actually safe?

@cuviper mentioned that interior mutability violates the assumption that if we have an immutable reference to a value, the value cannot change.

We solved this by adding an unsafe trait called NoInteriorMutability and requiring everyone to manually implement the trait for their types if it holds.

  1. Is it worth it to use SmallRef as a parameter to non-inlined functions?

Perhaps, need to find a use case and measure it.

  1. Can we deal with interior mutability in a way that does not require everyone in the world to implement an unsafe trait?

@scottmcm points us to unsafe auto trait Freeze {}, a compiler internal trait that does exactly what we need but is currently private.