Emulating OO inheritance

I have an application that depends on items of different types having both common methods and special methods for item-specific data. Although not overly pretty, it actually works.

However, the counterpart having common methods and common data became a real mess with unsafe pointers and duplicated code. So my question is simply: is there a reasonable way to do this in Rust?

// Rust does not support inheritance, how to proceed?
struct AContent {
    // Item-specific data supporting both common and specific methods. SOLVED

    // Common data used by common methods. UNRESOLVED
    common_data: bool // Just an example
}

struct BContent {
    // Item-specific data supporting both common and specific methods. SOLVED

    // Common data used by common methods. UNRESOLVED
    common_data: bool // Just an example
}

enum Item {
    A(AContent),
    B(BContent)
}

impl Item {
    fn set_common_data(&self, data: bool) -> Item {
        todo!();
        *self  // for "chaining" purposes
    }
    fn get_common_data(&self) -> bool {
        todo!()
    }
}

fn main() {
    let a = Item::A(AContent{common_data: false});
    let b = Item::B(BContent{common_data: false});
    // Wish:
    a.set_common_data(true).get_common_data();
    b.get_common_data();
}

Note the return of *self, it is the counterpart to return this in Java and JavaScript.

the "easy" method is to use Deref. although it is generally adviced against to emulate inheritance with Deref, but it works for many use cases, so it can be an acceptable solution when it makes sense. one example that heavily uses Deref for OO style inheritance is makepad.

I'll omit code snippets, use of Deref (and DerefMut) should be rather self explanatory.

another commonly used method to emulate inheritance is to use some form of "delegation" traits combined with blanket implementations. it needs more boilerplates though, compared to the previous "easy" method.

example:

/// the common data, i.e. base "class"
/// see below what the derive macro does
#[derive(oo::Class)]
struct Common {
    data: bool
}
/// these are the private implementation details
impl Common {
    fn set_data(&mut self, data: bool) { todo!() }
    fn get_data(&self) -> bool { todo!() }
}

/// "derived classes" of Common
#[derive(oo::Class)]
#[oo(inherit(Common))]
struct AContent {
    base: Common,
    a: i32,
}
#[derive(oo::Class)]
#[oo(inherit(Common))]
struct BContent {
    base: Common,
    b: String,
}

/// the delegation trait, defines what's considered the "public API" of the `Common` class
trait CommonMethods {
    fn set_common_data(&mut self, data: bool) -> &mut Self;
    fn get_common_data(&self) -> bool;
}

/// a marker trait for the "is-a" relation, can be automated to some extent using macros
/// alternatively, can use the standard library, such as `AsRef`, `AsMut`
trait IsA<Base> {
    fn upcast(&self) -> &Base;
    fn upcast_mut(&mut self) -> &mut Base;
    // may contains other methods, depending on what OOP features to emulate
}

/// blanket implementation of public API, delegates to `IsA<Common>`
impl<T> CommonMethod for T where T: IsA<Common> {
    //...
}

fn main() {
    let a = AContent {
        base: Common { data: false },
        a: 42,
    };
    let b = BContent {
        base: Common { data: false },
        b: String::from("hello"),
    };
    // needs to import the trait in case it's from external crates
    use crate::CommonMethods;
    a.set_common_data(true).get_common_data();
    b.get_common_data();
}

/// static dispatched polymorphism can be emulated with generics
fn toggle_common_data<T: CommonMethods>(obj: &mut T) {
    obj.set_common_data(!obj.get_common_data());
}

/// dynamic dispatched polymorphism uses trait objects:
fn toggle_common_data_dyn(obj: &mut dyn CommonMethods) {
    obj.set_common_data(!obj.get_common_data());
}

/// the derive macro generates the "is-a" markers such as following.
/// can generate additional boilerplates too, like constructors etc.
impl IsA<Common> for Common {...}
impl IsA<Common> for AContent {...}
impl IsA<AContent> for AContent {...}
impl IsA<Common> for BContent {...}
impl IsA<BContent> for AContent {...}

Thanx! Will try it out and see where it goes.

I'd like to also point out, modified or variated version of both methods are used in windows-rs, the rust bindings for windows APIs by microsoft.

note, since the windows crate is bindings to ffi APIs, and because the bindings are machine generated from metadata, the implementation details can be quite different from what typical rust code would do, but the ideas are the same.

as large portion of the windows API is COM, which is basically modeled after the C++ OOP mechanism, it is enevitable for the rust bindings to emulate OOP paradigms. IMO, microsoft did a very good job to keep the rust API feel natural and idiomatic.

just to pick some random examples I have personally encountered. if you ever used windows-rs, chances are you have seen similar constructs too.

examples of Deref emulated inheritance:

in windows::Win32::Graphics::Dxgi:

  • IDXGIDevice4: Deref<Target = IDXGIDevice3>
  • IDXGIDevice3: Deref<Target = IDXGIDevice2>
  • IDXGIDevice2: Deref<Target = IDXGIDevice1>
  • IDXGIDevice1: Deref<Target = IDXGIDevice>
  • IDXGIDevice: Deref<Target = IDXGIObject>

similarly in windows::Win32::Graphics::Direct3D11:

  • ID3D11Device5: Deref<Target = ID3D11Device4>
  • ID3D11Device4: Deref<Target = ID3D11Device3>
  • ID3D11Device3: Deref<Target = ID3D11Device2>
  • ID3D11Device2: Deref<Target = ID3D11Device1>
  • ID3D11Device1: Deref<Target = ID3D11Device>

and the windows_core crate has a sophisticated OOP system emulated with marker traits and blanket implementations, and procedural macros. example of the notable ones:

  • CanInto<T>
  • AsImpl<T>
  • Interface,
  • ComObject<T>

the procedural macros are re-exports of the windows_implement and windows_interface crates.