Skip to main content

Crate plusplus

Crate plusplus 

Source
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, InConstruction, DowncastTo};

// define a class
class!{
    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 std::borrow::Cow;
    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
        pub class ObjectOne: super::ObjectZero {
            string_one: String;

            pub fn new() -> ObjectOne<InConstruction> {
                init_class! {
                    // initialize the superclass
                    superclass: super::ObjectZero::new(),

                    string_one: String::new(),
                }
            }

            pub fn print_example(&self) {
                println!("hi from ObjectOne");
            }

            // you can also write async functions!
            pub async fn async_example(&self) -> Cow<'_, str> {
                Cow::Borrowed(&self.string_one)
            }

            // 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 std::borrow::Cow;

class!{
    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 async_example(&self) -> Cow<'_, str> {
                let super_str = self.super_async_example().await;
                Cow::Owned(format!("{}-{}", self.string_two, super_str))
            }
        }

        // 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 visibilty 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);
}

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};

    // create a virtual function table (or, vtable) to store the overridable function pointers
    pub struct ObjectOneVtbl {
        pub fn_print_example: fn(this: &ObjectOne),
    }
    impl ObjectOneVtbl {
        // the vtable is initialized with our own implementations of the methods
        const BASE: Self = {
            Self { fn_print_example: ObjectOne::my_print_example }
        };
    }

    // 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)]
    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're rude and skipped halfway down the document 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,
        // 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,
    }

    impl ObjectOne {
        // the methods users use to call into the vtable
        pub fn print_example(&self) {
            (self.vtbl.fn_print_example)(self)
        }
        pub fn super_set_string(&mut self, str: String) {
            self.plusplus__super_mut().my_set_string(str)
        }
        // internal helper methods to get references typed as the superclass
        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 is mostly boilerplate...
        const TYPE_ID: &'static std::any::TypeId = &std::any::TypeId::of::<ObjectOne>();
        type RootClass = <ObjectZero as Class>::RootClass;
        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 }

        // ....except for this! 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 {
        //     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); }
        }
    }

    // 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) {
            // set the parent class's subclass type ID to this class's type ID
            unsafe { self.superclass.plusplus__set_subclass(ObjectOne::TYPE_ID) };

            // set the root class's manually_drop method to this class's manually_drop method
            {
                unsafe fn manually_drop(this: *mut ObjectZero) {
                    let ref_mut = unsafe { &mut *this };
                    let this = 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: &mut ObjectZero = ref_mut;
                            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 typers)
                            assert!(self_size >= target_size);
                            let array_size = 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 target_ptr = std::ptr::slice_from_raw_parts_mut(
                                t as *mut ObjectZero as *mut u8,
                                array_size
                            );
                            // ....then converts that into a fat pointer to the target type.
                            let target_ref = &mut *(target_ptr as *mut 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.
                        }
                    };
                    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;
            }
            // update `ObjectZero`'s vtable to point to our `set_string` function
            {
                // temporarily convert `self` to a non-`InConstruction` object so we can use
                // the full object's `Deref` impl to get a reference to the class whose vtable
                // we need to update
                let this: &mut ObjectZero = &mut *(unsafe { self.to_constructed() });
                fn fn_set_string(this: &mut ObjectZero, str: String) {
                    // we need to do that same conversion code to convert `&mut ObjectZero` to
                    // `&mut ObjectOne` here.
                    //
                    // why isn't this a function, you ask? it should be! i tried!!!! but the
                    // compiler needs to know that `*mut ObjectOne` and *mut [u8] have the same
                    // type of metadata field, and Rust's generics system isn't able to observe
                    // the type of a pointer metadata field!!!!! so this needs to be inlined
                    // everywhere!!!!!!!!!!
                    //
                    // i think this means that rust's fat pointer types are duck-typed, which is
                    // really funny to me. i suppose it says a lot about me that i find that
                    // sort of thing funny, but then again, i'm also the kind of person that's
                    // willing to write this sort of library, which should have said enough.
                    //
                    // welcome to my sick and twisted mind...... hehehehehehehehehe............
                    let this: &mut ObjectOne = unsafe {
                        let t: &mut ObjectZero = this;
                        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 *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
                    };
                    this.my_set_string(str)
                }
                unsafe { this.plusplus__vtbl_mut().fn_set_string = fn_set_string };
            }
        }
        // oh, we're back to normal code, thank god. thanks for putting up with me.
        //
        // this function just initializes the class structure from its `InitClass`
        // fields. pretty mechanical stuff really.
        pub(super) fn plusplus__new_from_init(init: PlusPlus__InitClass) -> Self {
            let mut this = Self {
                vtbl: ObjectOneVtbl::BASE,
                memory: InConstruction::default(),
                subclass_id: None,
                superclass: init.superclass,
                string_one: init.string_one
            };
            this.plusplus__set_vtbls();
            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
                )
            }
        }

        /// Finish constructing this by moving it to the heap placing it in a `ClassBox`.
        ///
        /// Downcasting, upcasting, and deref coersions will work properly after calling this!
        pub fn finish(self: ObjectOne<InConstruction>) -> ClassBox<ObjectOne> {
            let boxed = Box::new(self);
            let leaked = Box::leak(boxed);
            let constructed = unsafe { leaked.to_constructed() };
            unsafe { ClassBox::from_raw(constructed) }
        }
    }
}
// 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(),
        };
        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 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<plusplus::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<plusplus::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:

  1. If the type punning is invalid
  2. If the pointer provenance is handled incorrectly
  3. If there’s a way for a user to access unsafe class internals without using explicitly unsafe code
  4. 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§

ClassBox
A wrapper around polymorphic Class types.
Constructed
InConstruction

Traits§

Class
Any class type.
ClassMemory
Downcast
Any wrapper around a class that can be downcast into a child class.
DowncastTo
Convenience trait to make downcasting into a child class easier to write.