[![Crates.io](https://img.shields.io/crates/v/vptr.svg)](https://crates.io/crates/vptr) [![Documentation](https://docs.rs/vptr/badge.svg)](https://docs.rs/vptr/) # vptr Enable thin references to trait ## Intro ### What are trait object and virtual table ? In rust, you can have dynamic dispatch with the so-called Trait object. Here is a typical example ```rust trait Shape { fn area(&self) -> f32; } struct Rectangle { w: f32, h : f32 } impl Shape for Rectangle { fn area(&self) -> f32 { self.w * self.h } } struct Circle { r: f32 } impl Shape for Circle { fn area(&self) -> f32 { 3.14 * self.r * self.r } } // Given an array of Shape, compute the sum of their area fn total_area(list: &[&dyn Shape]) -> f32 { list.iter().map(|x| x.area()).fold(0., |a, b| a+b) } ``` In this example the function `total_area` takes a reference of trait objects that implement the `Shape` trait. Internally, this `&dyn Shape` reference is composed of two pointer: a pointer to the object, and a pointer to a virtual table. The virtual table is a static structure containing the function pointer to the `area` function. Such virtual table exist for each type that implements the trait, but each instance of the same type share the same virtual table. Having only a pointer to the struct itself would not be enough as the `total_area` does not know the exact type of what it is pointed to, so it would not know from which `impl` to call the `area` function. This box diagram shows a simplified representation of the memory layout ```ascii &dyn Shape ╭──────> Rectangle ╭─> vtable of Shape for Rectangle ┏━━━━━━━━━━━━━┓ │ ┏━━━━━━━━━┓ │ ┏━━━━━━━━━┓ ┃ data ┠───╯ ┃ w ┃ │ ┃ area() ┃ ┣━━━━━━━━━━━━━┫ ┣━━━━━━━━━┫ │ ┣━━━━━━━━━┫ ┃ vtable ptr ┠─────╮ ┃ h ┃ │ ┃ drop() ┃ ┗━━━━━━━━━━━━━┛ │ ┗━━━━━━━━━┛ │ ┣━━━━━━━━━┫ ╰────────────────────╯ ┃ size ┃ ╏ ╏ ``` Other languages such as C++ implements that differently: in C++, each instance of a dynamic class has a pointer to the virtual table, inside of the class. So just a normal pointer to the base class is enough to do dynamic dispatch Both approaches have pros and cons: in Rust, the object themselves are a bit smaller as they do not have a pointer to the virtual table. They can also implement trait from other crates which would not work in C++ as it would have to somehow put the pointer to the virtual table inside the object. But rust pointer to trait are twice as big as normal pointer. Which is usually not a problem. Unless of course you want to pack many trait object reference in a vector in constrained memory, or pass them through ffi to C function that only handle pointer as data. That's where this crate comes in! ### Thin references This crates allows to easily opt in to thin references to trait for a type, by having pointers to the virtual table within the object. ```rust use vptr::vptr; trait Shape { fn area(&self) -> f32; } #[vptr(Shape)] struct Rectangle { w: f32, h : f32 } impl Shape for Rectangle { fn area(&self) -> f32 { self.w * self.h } } #[vptr(Shape)] struct Circle { r: f32 } impl Shape for Circle { fn area(&self) -> f32 { 3.14 * self.r * self.r } } // Given an array of Shape, compute the sum of their area fn total_area(list: &[vptr::ThinRef]) -> f32 { list.iter().map(|x| x.area()).fold(0., |a, b| a+b) } ``` Same as before, but we added `#[vptr(Shape)]` and are now using `ThinRef` instead of `&dyn Shape`. The difference is that the ThinRef has only the size of one pointer ```ascii ThinRef Rectangle ╭─>VTableData ╭─>vtable of Shape for Rectangle ┏━━━━━━━━━━━━━┓ ┏━━━━━━━━━━━━┓ ╮ │ ┏━━━━━━━━┓ │ ┏━━━━━━━━━┓ ┃ ptr ┠──╮ ┃ w ┃ │ ╭──│──┨ offset ┃ │ ┃ area() ┃ ┗━━━━━━━━━━━━━┛ │ ┣━━━━━━━━━━━━┫ ⎬─╯ │ ┣━━━━━━━━┫ │ ┣━━━━━━━━━┫ │ ┃ h ┃ │ │ ┃ vtable ┠──╯ ┃ drop() ┃ │ ┣━━━━━━━━━━━━┫ ╯ │ ┗━━━━━━━━┛ ┣━━━━━━━━━┫ ╰──>┃ vptr_Shape ┠──────╯ ┃ size ┃ ┗━━━━━━━━━━━━┛ ╏ ╏ ``` ## The `#[vptr]` macro The `#[vptr(Trait)]` macro can be applied to a struct and it adds members to the struct with pointer to the vtable, these members are of type VPtr, where S is the struct. The macro also implements the `HasVPtr` trait which allow the creation of `ThinRef` for this You probably want to derive from `Default`, otherwise, the extra fields needs to be initialized manually (with `Default::default()` or `VPtr::new()`) ```rust trait Shape { fn area(&self) -> f32; } #[vptr(Shape, ToString)] // There can be several traits #[derive(Default)] struct Rectangle { w: f32, h : f32 } // The traits within #[vptr(...)] need to be implemented for that type impl Shape for Rectangle { fn area(&self) -> f32 { self.w * self.h } } impl Display for Rectangle { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { write!(fmt, "Rectangle ({} x {})", self.w, self.h) } } // [...] let mut r1 = Rectangle::default(); r1.w = 10.; r1.h = 5.; let ref1 = ThinRef::::from(&r1); assert_eq!(mem::size_of::>(), mem::size_of::()); assert_eq!(ref1.area(), 50.); // When not initializing with default, you must initialize the vptr's manually let r2 = Rectangle{ w: 1., h: 2., ..Default::default() }; let r3 = Rectangle{ w: 1., h: 2., vptr_Shape: VPtr::new(), vptr_ToString: VPtr::new() }; // Also work with tuple struct #[vptr(Shape)] struct Point(u32, u32); impl Shape for Point { fn area(&self) -> f32 { 0. } } let p = Point(1, 2, VPtr::new()); let pointref = ThinRef::from(&p); assert_eq!(pointref.area(), 0.); // The trait can be put in quote if it is too complex for a meta attribute #[vptr("PartialEq")] #[derive(Default)] struct MyString(String); impl PartialEq for MyString { fn eq(&self, other: &str) -> bool { self.0 == other } } let mystr = MyString("Hi".to_string(), VPtr::new()); let mystring_ref = ThinRef::from(&mystr); assert!(*mystring_ref == *"Hi"); ``` ## License MIT