Crate basic_oop

Source
Expand description

The crate provides basic tools for writing object-oriented code in Rust. Very basic: no multiply inheritance, no generics, no interfaces (but this can be added in future in a limited form), no incapsulation. All classes in hierarchy should have distinct names even if they are located in different modules.

§How to define a class

First you need to import the parent class. All classes should be derived from some parent class. The only class not having any parent is Obj defined in basic_oop::obj module. It contains no fields or methods. Lets import it:

import! { pub test_class:
    use [obj basic_oop::obj];
}

Here test_class correspond to our new class name. (Although it is possible to use different names in the import section and in the class definition, doing so is not recommended to avoid confusion among users of the class.)

The pub keyword means that our class will be public. In case of a private class, it should be omitted.

All types that we plan to use in the method signatures of our class should also be imported as unique names. For example:

import! { pub test_class:
    use [obj basic_oop::obj];
    use std::rc::Rc;
    use std::rc::Weak as rc_Weak;
}

Now we can start to define our class using class_unsafe macro.

Classes defined with the class_unsafe macro are intended for use either with the Rc smart pointer or with the Arc smart pointer.

Suppose we don’t need reference counter atomicity. Then our class definition will be the next:

#[class_unsafe(inherits_Obj)]
pub struct TestClass { }

Each class should have two constructors: one for creating this particular class and one for calling it from the constructor of the inheritor:

impl TestClass {
    pub fn new() -> Rc<dyn IsTestClass> {
        Rc::new(unsafe { Self::new_raw(TEST_CLASS_VTABLE.as_ptr()) })
    }

    pub unsafe fn new_raw(vtable: Vtable) -> Self {
        TestClass { obj: unsafe { Obj::new_raw(vtable) } }
    }
}

§Fields

To add a field to class, we just write it in ordinar way:

#[class_unsafe(inherits_Obj)]
pub struct TestClass {
    field: RefCell<Rc<String>>,
}

impl TestClass {
    pub fn new(field: Rc<String>) -> Rc<dyn IsTestClass> {
        Rc::new(unsafe { Self::new_raw(field, TEST_CLASS_VTABLE.as_ptr()) })
    }

    pub unsafe fn new_raw(field: Rc<String>, vtable: Vtable) -> Self {
        TestClass {
            obj: unsafe { Obj::new_raw(vtable) },
            field: RefCell::new(field),
        }
    }
}

§Non-virtual methods

To add a method, it is needed to specify a fictive field with #[non_virt] attribute and function type:

#[class_unsafe(inherits_Obj)]
pub struct TestClass {
    ...
    #[non_virt]
    get_field: fn() -> Rc<String>,
}

Then TestClassExt extension trait will be generated contained appropriate function calling TestClass::get_field_impl. We must provide this implementing function:

impl TestClass {
    fn get_field_impl(this: &Rc<dyn IsTestClass>) -> Rc<String> {
        this.test_class().field.borrow().clone()
    }
}

§Virtual methods

Adding a virtual method is no different from adding a non-virtual method only this time we use virt:

#[class_unsafe(inherits_Obj)]
pub struct TestClass {
    ...
    #[virt]
    set_field: fn(value: Rc<String>),
}

impl TestClass {
    fn set_field_impl(this: &Rc<dyn IsTestClass>, value: Rc<String>) {
        *this.test_class().field.borrow_mut() = value;
    }
}

§Derived class. Method overriding.

Lets import our class and derive another one from it:

import! { pub derived_class:
    use [test_class crate::test_class];
}

#[class_unsafe(inherits_TestClass)]
pub struct DerivedClass {
}

Now we wants to override set_field, how we do it? Simple:

#[class_unsafe(inherits_TestClass)]
pub struct DerivedClass {
    #[over]
    set_field: (),
}

impl DerivedClass {
    pub fn set_field_impl(this: &Rc<dyn IsTestClass>, value: Rc<String>) {
        let value = /* coerce value */;
        TestClass::set_field_impl(this, value);
    }
}

The type of the overridden function is already known from the base class definition, so there is no need to re-write it, which is why the type of the phony field is specified as ().

§Using the class.

We can create instance of derived class:

let class = DerivedClass::new(Rc::new("initial".to_string()));

We can cast it to base class:

let base_class: Rc<dyn IsTestClass> = dyn_cast_rc(class).unwrap();

We can call both virtual and non-virtual methods:

assert_eq!(base_class.get_field().as_ref(), "initial");
base_class.set_field(Rc::new("changed".to_string()));
assert_eq!(base_class.get_field().as_ref(), "changed coerced");

See full working example in README.md

§Arc-based classes

To get a class based on Arc instead of Rc, use use ObjSync instead of Obj:

import! { pub test_class:
    use [obj_sync basic_oop::obj_sync];
    ...
}
#[class_unsafe(inherits_ObjSync)]
pub struct TestClass { }

Modules§

obj
obj_sync

Macros§

import
Imports base class into the current scope so that it can be inherited from.

Type Aliases§

Vtable
The pointer to the table containing pointers to class virtual functions.

Attribute Macros§

class_unsafe
Generates class and appropriate helper types and traits.