/thinnable

Thin references to unsized types.

Primary LanguageRust

Thinnable

Standard Rust DST references comprise not only a pointer to the underlying object, but also some associated metadata by which the DST can be resolved at runtime. Both the pointer and the metadata reside together in a so-called "fat" or "wide" reference, which is typically what one wants: the referent can thus be reached with only direct memory accesses, and each reference can refer to a different DST (for example, slices or traits) of a single underlying object.

However, in order to store multiple copies of the same DST reference, one typically suffers either metadata duplication across each such reference or else some costly indirection; furthermore, in some situations Rust's metadata may be known to be excessive: some other (smaller) type may suffice instead (for example, with particular slices, we may know that their lengths will always fit in a u8 and hence using a usize for their metadata would be unnecessary; or, with particular trait objects, we may know that the underlying type will always be from some enum and hence using a vtable pointer for their metadata would be unnecessary). Addressing these concerns can save valuable memory, especially when there are a great many such references in simultaneous use.

A Thinnable stores the metadata together with the referent rather than the reference, and thereby enables one to obtain "thin" DST references to the (now metadata-decorated) object: namely, ThinRef and ThinMut for shared and exclusive references respectively. But this comes at the cost of an additional indirect memory access in order to fetch the metadata, rather than it being directly available on the stack; hence, as is so often the case, we are trading off between time and space. Furthermore, for any given Thinnable, its "thin" references can only refer to the one DST with which that Thinnable was instantiated (although regular "fat" references can still be obtained to any of its DSTs, if required).

One can specify a non-standard metadata type, e.g. to use a smaller integer such as u8 in place of the default usize when a slice fits within its bounds: a MetadataCreationFailure will arise if the metadata cannot be converted into the proposed type. Using such a non-standard metadata type may save some bytes of storage, but obviously adds additional conversion overhead on every dereference.

The ThinnableSlice<T> type alias is provided for more convenient use with [T] slices; and the ThinnableSliceU8<T>, ThinnableSliceU16<T> and ThinnableSliceU32<T> aliases provide the same convenient use with [T] slices but encoding the length metadata in a u8, u16 or u32 respectively.

This crate presently depends on Rust's unstable ptr_metadata and unsize features and, accordingly, requires a nightly toolchain.

Example

use core::{alloc::Layout, convert::TryFrom, fmt, mem};
use thinnable::*;

const THIN_SIZE: usize = mem::size_of::<&()>();

// Creating a thinnable slice for an array is straightforward.
let thinnable_slice = ThinnableSlice::new([1, 2, 3]);

// Given a thinnable, we can obtain a shared reference...
let r = thinnable_slice.as_thin_ref(); // ThinRef<[u16]>
// ...which is "thin"....
assert_eq!(mem::size_of_val(&r), THIN_SIZE);
// ...but which otherwise behaves just like a regular "fat" DST
// reference
assert_eq!(&r[..2], &[1, 2]);

// For types `M` where the metadata type implements `TryInto<M>`, we
// can use `Thinnable::try_new` to try creating a thinnable using
// `M` as its stored metadata type (dereferencing requires that
// `M: TryInto<R>` where `R` is the original metadata type).
//
// For slices, there's a slightly more ergonomic interface:
let size_default = mem::size_of_val(&thinnable_slice);
let mut thinnable_slice;
thinnable_slice = ThinnableSliceU8::try_slice([1, 2, 3]).unwrap();
let size_u8 = mem::size_of_val(&thinnable_slice);
assert!(size_u8 < size_default);

// We can also obtain a mutable reference...
let mut m = thinnable_slice.as_thin_mut(); // ThinMut<[u16], u8>
// ...which is also "thin"....
assert_eq!(mem::size_of_val(&m), THIN_SIZE);
// ...but which otherwise behaves just like a regular "fat" DST
// reference.
m[1] = 5;
assert_eq!(&m[1..], &[5, 3]);

// We can also have thinnable trait objects:
let thinnable_trait_object;
thinnable_trait_object = Thinnable::<_, dyn fmt::Display>::new(123);
let o = thinnable_trait_object.as_thin_ref();// ThinRef<dyn Display>
assert_eq!(mem::size_of_val(&o), THIN_SIZE);
assert_eq!(o.to_string(), "123");