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);
}
}
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:
- 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.
- 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).
- 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.
- Is it worth it to use
SmallRef
as a parameter to non-inlined functions?
Perhaps, need to find a use case and measure it.
- 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.