Expand description
§Rust++: Object-Oriented Programming for Rust!!
This crate allows you to create polymorphic objects in Rust, with overridable functions, with behavior and syntax that should be familiar to users of object-oriented languages like C++, C#, or Java.
As an example:
use plusplus::{class, ClassInConstruction, Downcast, DowncastTo, InConstruction};
// define a class
class!{
// classes can derive traits (though right now only `Clone` is supported)
#[derive(Clone)]
pub class ObjectZero {
string: String;
// a class constructor. Classes returned from Rust++ class constructors must be
// parameterized with the `<InConstruction>` type parameter.
pub fn new() -> ObjectZero<InConstruction> {
// classes are initialized in the constructor with the `init_class!` syntax.
//
// this takes all the class's fields as an argument, plus a `superclass` field for
// initializing the superclass (when present)
init_class! {
string: "hello".into(),
another_field: 26,
}
}
// fields can have normal Rust visibility modifiers and be defined
// anywhere in the class
pub(crate) another_field: i32;
pub fn set_string(&mut self, str: String) {
self.string = str;
}
}
}
// object inheritance works across module and crate boundaries
mod object_one {
use plusplus::{class, InConstruction};
use reqwest::Response;
class!{
// ObjectOne is a subclass of ObjectZero. it inherits all of ObjectZero's methods and
// fields, and can extend ObjectZero's methods with new behavior
//
// A subclass must derive all traits the parent class derives
#[derive(Clone)]
pub class ObjectOne: super::ObjectZero {
string_one: String;
reqwest_url: String;
pub fn new() -> ObjectOne<InConstruction> {
init_class! {
// initialize the superclass
superclass: super::ObjectZero::new(),
string_one: String::new(),
reqwest_url: "https://crouton.net/".into(),
}
}
pub fn print_example(&self) {
println!("hi from ObjectOne");
}
// you can also write async functions!
pub async fn get_url(&self) -> reqwest::Result<Response> {
reqwest::get(&self.reqwest_url).await
}
// you override a superclass's methods by declaring an Override block with the name
// of the class you're overriding
override super::ObjectZero {
pub fn set_string(&mut self, str: String) {
// you prefix method invocations with the `super_` prefix to call the
// parent's implementation of the method. this allows you to extend methods
// with new behaviors!
self.super_set_string(str.clone());
self.string_one = str;
}
}
}
}
// if you want to define methods that can't be overridden, just put them in a normal
// impl block
impl ObjectOne {
pub fn string_one(&self) -> &str {
&self.string_one
}
}
}
use object_one::ObjectOne;
use reqwest::Response;
class!{
#[derive(Clone)]
pub class ObjectTwo: ObjectOne {
string_two: String;
pub fn new(two: impl Into<String>) -> ObjectTwo<InConstruction> {
init_class! {
superclass: object_one::ObjectOne::new(),
string_two: two.into(),
}
}
// in order to override a class's method, you must create an override block
// for the class that declared that method
override ObjectOne {
pub fn print_example(&self) {
self.super_print_example();
println!("hello from ObjectTwo");
}
pub async fn get_url(&self) -> reqwest::Result<Response> {
let response = self.super_get_url().await?;
println!("url get!!!");
Ok(response)
}
}
// you can override methods from any parent class in the class hierarchy
override ObjectZero {
pub fn set_string(&mut self, str: String) {
self.super_set_string(str.clone());
self.string_two = str;
}
}
}
}
fn main() {
// initialize an object. the `finish` method wraps the class in a `ClassBox`
// and allows it to behave polymorphically.
let object_two = ObjectTwo::new("hello!").finish();
// prints:
// ```
// hi from ObjectOne
// hello from ObjectTwo
// ```
object_two.print_example();
println!();
let object_as_one = object_two.upcast(); // is ClassBox<ObjectOne>
// calling a class method from anywhere in the class hierarchy will always result in the
// deepest implementation of that method getting executed. so, this also prints:
// ```
// hi from ObjectOne
// hello from ObjectTwo
// ```
object_as_one.print_example();
println!();
// you can call a method with the `my_` prefix in order to bypass the method overload and
// call that class's own implementation of the method. so this prints:
// ```
// hi from ObjectOne
// ```
object_as_one.my_print_example();
println!();
let mut object_as_base = object_as_one.upcast(); // is ClassBox<ObjectZero>
// this call results in all three `set_string` methods getting invoked
let new_str = "sets all strings!!";
object_as_base.set_string(new_str.to_string());
// you can't directly access a subclass's fields from a superclass. so, the following line
// would not compile:
// println!("{}" object_as_base.string_two);
let object_as_two = object_as_base
.downcast_to::<ObjectOne>().unwrap()
.downcast_to::<ObjectTwo>().unwrap();
// but, you can access a superclass's fields, within the limits set by visibility rules
assert_eq!(object_as_two.string, new_str);
assert_eq!(object_as_two.string_one(), new_str);
assert_eq!(object_as_two.string_two, new_str);
let tokio_rt = tokio::runtime::Builder::new_current_thread().enable_io().build().unwrap();
tokio_rt.block_on(async {
let object_as_one = object_as_two.upcast();
// method overloads work on async methods, too. this prints:
// ```
// url get!!!
// ```
let response = object_as_one.get_url().await.unwrap();
// prints:
// ```
// <html>
// <title> Crouton
// </title>
// <body bgcolor="white" text="black">
// <img src="crouton.png" alt="Crouton">
// </body>
// </html>
// ```
// delightful....
println!("{}", response.text().await.unwrap());
});
}How? Why?? Is that safe??!? These are questions that I often get asked in my day-to-day life. Fortunately, in this case I have good answers to all three.
§How?
First, know that Constructed and InConstruction are defined as follows:
use std::mem::MaybeUninit;
#[repr(transparent)]
pub struct Constructed([MaybeUninit<u8>]);
#[repr(transparent)]
pub struct InConstruction([MaybeUninit<u8>; 0]);Note that InConstruction is a zero-sized type, and Constructed is an unsized type. This
will be important later.
Now, let’s look at how ObjectOne gets expanded:
// we create an inner module for the class internals to make sure nobody
// *untoward* fucks around with the vtable
pub use plusplus__class_objectone::ObjectOne;
mod plusplus__class_objectone {
use super::*;
use plusplus::{
Class, ClassBox, InConstruction, Constructed,
ClassMemory, ClassInConstruction, ClassClone
};
use std::pin::Pin;
// create a virtual function table (or, vtable) to store the overridable function pointers
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct ObjectOneVtbl {
pub fn_print_example: fn(this: &ObjectOne),
// the futures returned by async virtual functions are automatically boxed
pub fn_get_url: for<'rpp_future>
fn(this: &'rpp_future ObjectOne)
-> Pin<Box<dyn 'rpp_future + Future<Output=reqwest::Result<Response>>>>,
}
impl ObjectOneVtbl {
// the vtable is initialized with our own implementations of the methods
const BASE: Self = {
fn my_get_url<'rpp_future>(this: &'rpp_future ObjectOne)
-> Pin<Box<dyn 'rpp_future + Future<Output=reqwest::Result<Response>>>>
{
Box::pin(ObjectOne::my_get_url(this))
}
Self {
fn_print_example: ObjectOne::my_print_example,
fn_get_url: my_get_url,
}
};
}
// define the actual class struct.
//
// the class struct is #[repr(C)] because we need precise control over the type's memory
// layout and *my god* is there going to be a lot of type punning. type puns are this
// crate's meat + potatoes. they're essential.
//
// we'll get to the generic parameter in a moment.
#[repr(C)]
#[derive(Clone)]
pub struct ObjectOne<C: ?Sized + ClassMemory = Constructed>
where
ObjectZero: Class,
{
// we implement inheritance by nesting the class's superclass in the first field of the
// class struct. that way, a reference to an `ObjectOne` is also very nearly a valid
// reference to an `ObjectZero` - you (mostly) just need to do a simple pointer cast.
superclass: ObjectZero<InConstruction>,
// unless you skipped, you know this one
vtbl: ObjectOneVtbl,
// the type ID of the immediate subclass of this type. this is necessary if you want
// downcasting to work
//
// we store an Option<&TypeId> instead of an Option<TypeId> as a size optimization; an
// Option<&TypeId> is 8 bytes, while an Option<TypeId> is 24 bytes
subclass_id: Option<&'static std::any::TypeId>,
// the user-defined type fields.
//
// note that in order to match the apparent private visibility this field was defined
// with we need to add a `pub(super)` to this field, so it's visible outside the
// `plusplus__class_objectone` module
pub(super) string_one: String,
pub reqwest_url: String,
// plusplus class memory. ahh, plusplus class memory.
//
// i said earlier that converting a class reference to its superclass's reference is
// *mostly* a simple pointer cast. however, if class references were just normal thin
// references, calling an overriden method that accesses one of that superclass's
// subclasses would read past the end of that type, and thus (I think???) invoke
// undefined behavior.
//
// so we have this memory field to fix that. the memory field contains a
// `[MaybeUninit<u8>]` buffer, and the size of this buffer is always set to the size of
// the rest of all of the subclasses combined (the buffer size is stored in the metadata
// field of the fat pointer to this object). as a result, doing object casts doesn't
// change the object's observed size, and references to any class in an object's class
// hierarchy will always be references to the whole object in memory.
memory: C,
}
// helper struct that contains the fields the user needs to initialize. if we didn't have
// this struct, then the user's constructor methods would have to live inside the
// `plusplus__class_objectone` module, and they could futz with the class struct internals
// in a potentially unsafe way.
pub struct PlusPlus__InitClass {
pub superclass: ObjectZero<InConstruction>,
pub string_one: String,
pub reqwest_url: String,
}
impl ObjectOne {
// the methods users use to call into the vtable
pub fn print_example(&self) {
(self.vtbl.fn_print_example)(self)
}
// the boxed futures in virtual async fns get converted back to normal async calls
pub async fn get_url(&self) -> reqwest::Result<Response> {
(self.vtbl.fn_get_url)(self).await
}
pub fn super_set_string(&mut self, str: String) {
self.plusplus__super_mut().my_set_string(str)
}
fn plusplus__super_ref(&self) -> &ObjectZero {
self
}
fn plusplus__super_mut(&mut self) -> &mut ObjectZero {
self
}
// this method is used by subclasses to modify their superclass's vtables and insert
// method overrides. this is unsafe because if you put an incorrectly-implemented
// function into the vtable, Bad Things Will Happen
#[doc(hidden)]
pub unsafe fn plusplus__vtbl_mut(&mut self) -> &mut ObjectOneVtbl { &mut self.vtbl }
}
unsafe impl Class for ObjectOne {
// implement the Class trait for this class. this starts with boilerplate...
const TYPE_ID: &'static std::any::TypeId = &std::any::TypeId::of::<ObjectOne>();
type RootClass = <ObjectZero as Class>::RootClass;
type InConstruction = ObjectOne<InConstruction>;
fn subclass_id(&self) -> Option<&'static std::any::TypeId> { self.subclass_id }
fn root_class(&self) -> &Self::RootClass { self }
fn root_class_mut(&mut self) -> &mut Self::RootClass { self }
unsafe fn as_in_construction(&self) -> &ObjectOne<InConstruction> {
unsafe { &*(self as *const Self as *const ObjectOne<InConstruction>) }
}
unsafe fn as_in_construction_mut(&mut self) -> &mut ObjectOne<InConstruction> {
unsafe { &mut *(self as *mut Self as *mut ObjectOne<InConstruction>) }
}
// ....but this is really interesting!!
//
// plusplus needs to provide explicit drop handling. if it didn't, then if you dropped a
// a superclass that's been subclassed, then none of the subclass destructor code would
// be run, because the superclass sees its subclasses as opaque blobs of memory.
//
// let's look at `ObjectZero`'s vtable:
//
// ```
// pub struct ObjectZeroVtbl {
// class_clone: fn(&ObjectZero) -> plusplus::ClassBox<ObjectZero>,
// pub manually_drop: unsafe fn(*mut ObjectZero),
// pub fn_set_string: fn(this: &mut ObjectZero, str: String),
// }
// ```
//
// notice that it has an extra `manually_drop` function pointer. whenever a subclass is
// initialized, it overrides the root `manually_drop` function with its own. then, at
// the end of initialization, `manually_drop` will contain a drop function for the leaf
// class, which can see every superclass in class hierarchy, and then it will drop
// all the data successfully.
//
// we wrap classes with `ClassBox` instead of `Box` for this reason. `ClassBox` knows
// about the special class drop logic; `Box` does not.
unsafe fn manually_drop(slot: &mut std::mem::ManuallyDrop<Self>) {
let as_root_class = slot.root_class_mut();
let manual_drop_fn = unsafe { as_root_class.plusplus__vtbl_mut().manually_drop };
unsafe { manual_drop_fn(as_root_class); }
}
// helper methods that implement
unsafe fn from_root_class_ref(root: &Self::RootClass) -> &Self {
unsafe {
// ah. um. asdfuwef heheheh don'- don't worry about.. this......
// ...............................................................
// .....*sigh*. okay, there's no getting around the fact that this
// looks really really ugly. i'm sorry. i swear its less bad than it
// seems.
//
// this code first computes the size difference between the source
// dynamically-sized class and the target class with no padding...
let t: &<ObjectZero as Class>::RootClass = root;
let self_size = std::mem::size_of_val(t);
let target_size = std::mem::size_of::<ObjectOne<InConstruction>>();
// (if this assertion fails then this code has been called on the
// wrong types)
assert!(self_size >= target_size);
// uses slice_from_raw_parts_mut to construct a fat pointer whose
// metadata contains the size of the `memory` buffer for the target
// type....
let array_size = self_size - target_size;
// ....then converts that into a fat pointer to the target type.
let target_ptr = std::ptr::slice_from_raw_parts(
t as *const <ObjectZero as Class>::RootClass as *const u8,
array_size
);
let target_ref = &*(target_ptr as *const ObjectOne);
// we do an assertion for good measure to make sure nothing's
// gone wrong.
assert_eq!(self_size, std::mem::size_of_val(target_ref));
target_ref
// okay, maybe using `slice_from_raw_parts_mut` is ugly as sin.
// ideally we'd use `std::ptr::from_raw_parts_mut` to construct a
// pointer with the right metadata correctly, but as of when I'm
// writing this (June 2nd 2026) its been five years since the issue
// for that function was opened and frankly i don't want to wait an
// indeterminate amount of time for someone to remember to stabilize
// that! i got burned once before by trying to write a library built
// around specialization (don't ask) and i'm not going to get burned
// again.
}
}
// essentially the same code as above, but for mutable references
unsafe fn from_root_class_mut(root: &mut Self::RootClass) -> &mut Self {
unsafe {
let t: &mut <ObjectZero as Class>::RootClass = root;
let self_size = std::mem::size_of_val(t);
let target_size = std::mem::size_of::<ObjectOne<InConstruction>>();
assert!(self_size >= target_size);
let array_size = self_size - target_size;
let target_ptr = std::ptr::slice_from_raw_parts_mut(
t as *mut <ObjectZero as Class>::RootClass as *mut u8,
array_size
);
let target_ref = &mut *(target_ptr as *mut ObjectOne);
assert_eq!(self_size, std::mem::size_of_val(target_ref));
target_ref
}
}
}
impl ClassInConstruction for ObjectOne<InConstruction> {
type Class = ObjectOne;
/// Finish constructing this by moving it to the heap placing it in a `ClassBox`.
///
/// Downcasting, upcasting, and deref coercions will work properly after calling this!
fn finish(self) -> ClassBox<ObjectOne> {
let boxed = Box::new(self);
let leaked = Box::leak(boxed);
let constructed = unsafe { leaked.to_constructed() };
unsafe { ClassBox::from_raw(constructed) }
}
}
// the class construction helper methods
impl ObjectOne<InConstruction> {
// update the parent class's vtable to call the subclass's overridden methods
fn plusplus__set_vtbls(&mut self) {
use Class as _;
// set the parent class's subclass type ID to this class's type ID
unsafe { self.superclass.plusplus__set_subclass(<ObjectOne as Class>::TYPE_ID) };
{
// the subclass implementation of manually_drop
unsafe fn manually_drop(this: *mut <ObjectOne as Class>::RootClass) {
let this = unsafe { ObjectOne::from_root_class_mut(&mut *this) };
unsafe { std::ptr::drop_in_place(this) };
}
let root_vtbl = unsafe {
<ObjectOne as Class>::root_class_mut(self.to_constructed())
.plusplus__vtbl_mut()
};
root_vtbl.manually_drop = manually_drop;
}
{
let this: &mut ObjectZero = &mut *(unsafe { self.to_constructed() });
fn fn_set_string(this: &mut ObjectZero, str: String) {
let this: &mut ObjectOne = unsafe { ObjectOne::from_root_class_mut(this.root_class_mut()) };
this.my_set_string(str)
}
unsafe { this.plusplus__vtbl_mut().fn_set_string = fn_set_string };
}
}
// this function just initializes the class structure from its `InitClass` fields.
pub(super) fn plusplus__new_from_init(init: PlusPlus__InitClass) -> Self {
use Class;
let mut this = Self {
vtbl: ObjectOneVtbl::BASE,
memory: InConstruction::default(),
subclass_id: None,
superclass: init.superclass,
string_one: init.string_one,
reqwest_url: init.reqwest_url
};
this.plusplus__set_vtbls();
// set the derived trait vtables for the parent class
let root_in_construction = unsafe {
this.to_constructed().root_class_mut().as_in_construction_mut()
};
root_in_construction.plusplus__set_trait_vtbls::<ObjectOne>();
// the implementation for this function in `ObjectZero` looks like this:
//
// pub fn plusplus__set_trait_vtbls<Class>(&mut self)
// where
// Class: ?Sized + plusplus::Class<RootClass=ObjectZero>,
// Class::InConstruction: Clone,
// {
// let class_clone = |this: &ObjectZero| -> plusplus::ClassBox<ObjectZero> {
// use plusplus::{Class as _, ClassBox};
// let child_class = unsafe {
// Class::from_root_class_ref(this).as_in_construction()
// };
// let cloned = child_class.clone().finish();
// unsafe { ClassBox::from_raw(ClassBox::leak(cloned).root_class_mut()) }
// };
// self.vtbl.class_clone = class_clone;
// }
//
// notice the `Class::InConstruction: Clone` bound. implementing traits as a generic
// method on the superclass allows us to use this method's generic bounds to check at
// compiletime whether all the superclass's traits are implemented on the child class.
this
}
// exposed so that subclasses can set the subclass_id field in their superclass. unsafe
// because downcasts will go wrong if this is set to the wrong type id
pub unsafe fn plusplus__set_subclass(&mut self, subclass_id: &'static std::any::TypeId) {
self.subclass_id = Some(subclass_id);
}
/// Unsafe because caller must guarantee that vtbl doesn't contain any subclass methods
pub unsafe fn to_constructed(&mut self) -> &mut ObjectOne {
unsafe {
&mut *(
std::ptr::slice_from_raw_parts_mut::<u8>(
self as *mut _ as *mut u8,
0
) as *mut ObjectOne
)
}
}
}
// `ClassBox` uses this trait for its own `Clone` implementation
impl ClassClone for ObjectOne {
fn class_clone(&self) -> ClassBox<Self> {
use {ClassBox, Class, ClassClone};
let root_ref: &mut _ = ClassBox::leak(self.root_class().class_clone());
let self_ref = unsafe { Self::from_root_class_mut(root_ref) };
unsafe { ClassBox::from_raw(self_ref) }
}
}
}
// the user-defined functions
impl ObjectOne {
pub fn new() -> ObjectOne<InConstruction> {
// init_class! gets expanded into this
let init_class = plusplus__class_objectone::PlusPlus__InitClass {
superclass: ObjectZero::new(),
string_one: String::new(),
reqwest_url: "https://crouton.net/".into(),
};
ObjectOne::<InConstruction>::plusplus__new_from_init(init_class)
}
// the user-defined methods. these versions don't get overridden by subclasses,
// so they have the `my_` prefix added.
pub fn my_print_example(&self) {
println!("hi from ObjectOne");
}
pub async fn my_get_url(&self) -> reqwest::Result<Response> {
reqwest::get(&self.reqwest_url).await
}
pub fn my_set_string(&mut self, str: String) {
self.super_set_string(str.clone());
self.string_one = str;
}
}
// deref coercions to the parent class. the compiler will auto-insert multiple deref coercions
// to travel multiple layers up the class hierarchy.
impl std::ops::Deref for ObjectOne {
type Target = ObjectZero;
fn deref(&self) -> &Self::Target {
unsafe {
// see above for why this code is Like This
let t: &ObjectOne = self;
let self_size = std::mem::size_of_val(t);
let target_size = std::mem::size_of::<ObjectZero<InConstruction>>();
assert!(self_size >= target_size);
let array_size = self_size - target_size;
let target_ptr = std::ptr::slice_from_raw_parts(
t as *const ObjectOne as *const u8,
array_size,
);
let target_ref = &*(target_ptr as *const ObjectZero);
assert_eq!(self_size, std::mem::size_of_val(target_ref));
target_ref
}
}
}
impl std::ops::DerefMut for ObjectOne {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe {
// ditto
let t: &mut ObjectOne = self;
let self_size = std::mem::size_of_val(t);
let target_size = std::mem::size_of::<ObjectZero<InConstruction>>();
assert!(self_size >= target_size);
let array_size = self_size - target_size;
let target_ptr = std::ptr::slice_from_raw_parts_mut(
t as *mut ObjectOne as *mut u8,
array_size,
);
let target_ref = &mut *(target_ptr as *mut ObjectZero);
assert_eq!(self_size, std::mem::size_of_val(target_ref));
target_ref
}
}
}And that’s how this works! Thank you for sticking with me.
§Why?
Some would say that it is best that Rust is not an object-oriented programming language. I fully agree! Objects have a relatively high amount of overhead, and are conceptually very limited. For most code, traits are a strictly better way of expressing genericism than objects.
You usually should not have to use this. I’ll plead with you right now to really, really think about whether you need to reach for object-oriented programming to solve your problem.
But, sometimes, a problem is very well suited to an object-oriented implementation and very poorly suited to any other implementation. And it’s useful to have objects as an option whenever those problems come up. Of course, you could rewrite your code in a different language, but there are a lot of good reasons to use Rust even despite that! What other language has so robust an ecosystem, and also runs on desktops, web browsers, servers, smartphones, and embedded devices?
(i also think its really subversive to take such an explicitly not-object-oriented language and force it to be object-oriented. the fact that this is possible at all is wildly funny to me. i was laughing like a madwoman when i was writing this. i had a lot of fun. and isn't that its own reward?)§Is that safe??!?
I think so! The ways I can think of that this theoretically could go wrong are:
- If the type punning is invalid
- If the pointer provenance is handled incorrectly
- If there’s a way for a user to access unsafe class internals without using explicitly
unsafecode - If there’s a way for to swap superclasses, and thus swap vtables, of a fully-constructed class
For #1, we’re using #[repr(C)] carefully, and the subclass/superclass type layouts line up.
We’re teaching Rust about our provenance tricks with pointer metadata, so unless I’m critically
misusing the pointer functions, #2 should be fine. No user-defined code is placed in the
protected class module, so #3 isn’t a factor. And for #4, as far as I can tell, there is no way
to swap superclass vtables once they’re constructed with safe code.
That said, I may have overlooked something! I am not an expert on unsafe Rust. So if you find something, do open an issue.
Also, Miri hasn’t complained in any of the tests I’ve run on the latest code. Absence of evidence isn’t evidence of absence, but like, it’s at least a datapoint.
This was proudly made without any assistance from AI tools.
Macros§
- class
- The whole point.
Structs§
- Class
Box - A wrapper around polymorphic
Classtypes. - Constructed
- The extra memory of a class that has been constructed.
- InConstruction
- The extra memory for a class in construction.
Traits§
- Class
- Any class type.
- Class
Clone - Class
InConstruction - Class
Memory - The extra memory for a class in which subclasses are stored.
- Downcast
- Any wrapper around a class that can be downcast into a child class.
- Downcast
To - Convenience trait to make downcasting into a child class easier to write.