Recently, I've been rewriting some C++ open-source libraries in Rust. Due to the inheritance relationships present in the original C++ code, I need to simulate inheritance for a small number of structs in Rust. I cannot fully rely on trait + struct implementations (as this would require library users to write a significant amount of boilerplate code manually). Therefore, I have no choice but to attempt simulating C++ inheritance in Rust, though this simulation doesn't need to be overly complex.
I’m seeking help here and would like to ask: Can procedural macros be used to simulate C++ inheritance mechanisms? If procedural macros can achieve this simulated inheritance, please provide some rough implementation ideas. Thank you in advance!
Additional note: I prefer not to use the newtype or delegate mechanisms, as I’m concerned they might introduce slight runtime performance overhead. Moreover, they could negatively impact code readability, especially since this is not a greenfield project but a rewrite of an existing C++ codebase.
Here’s a rough idea of what I’d like to achieve using procedural macros:
Define a public interface using trait xxx, and also define a struct named struct xxx_impl_boilerplate that implements trait xxx and contains some fields. struct xxx_impl_boilerplate will act as the base class in C++. For example:
Define a derived struct struct yyy that "inherits" trait xxx. With the help of a procedural macro, the implementation code and fields from struct xxx_impl_boilerplate should be copied into struct yyy. The resulting code should resemble the following handwritten version:
pub struct yyy {
name: String,
// Other fields...
}
// The procedural macro should automatically generate this code...
impl xxx for yyy {
fn name(&self) -> &str {
&self.name
}
}
If a method with the same name (e.g., fn name(&self) -> &str) appears in the impl block of struct yyy, the procedural macro should replace the method in impl xxx for yyy with the one from struct yyy's impl block. This mimics C++’s override behavior. The syntax sugar might look like this:
We don't do that here. Don't try force other language's construction.
Make sure what you need is inheritance, not composition. It's rarely the case.
I prefer not to use the newtype or delegate mechanisms, as I’m concerned they might introduce slight runtime performance overhead.
Do measure. It should be zero-overhead if you have no type erasure. And it should not introduce more overhead than vtable in c++. I strongly recommend the delegate route if you have to do this.
in C++, the term "inheritance" may mean different concepts -- what's commonly referred to as "extends" and "implements" in other OOP languages [1].
for "implements", i.e. the base class is a pure virtual class, the rust equivalence is trait objects. [2]
for "extends", i.e. you want to call methods defined in the base class, the closest analog in rust is the Deref trait, but such usage is NOT the intended use of the trait, and is commonly deemed an anti-pattern by the community. in most cases, composition is a better choice.
although rust supports the OOP paradigm quite well, it is generally adviced to prefer more idomatic alternative designs when they exist.
personally, I mostly avoid OOP designs all together, unless absolutely necessary, e.g. there's a hard requirements for the design, e.g. to interop with foreign languages, or it is in the transitioning process from existing code base, etc.
for an example of traiditional OOP patterns brought to rust, check out the gtk-rs project, although it's a binding, not an re-implementation.
or the mix of them, since C++ allows complicated mutiple inheritance ↩︎
The main issue with this approach is that the macros cannot "see" what the referenced traits or impls contain. You should think of macros as strictly string processing code. So in this code:
#[inherit(xxx from xxx_impl_boilerplate)]
pub struct yyy {
name: String,
// Other fields...
}
Whatever inherit does, all it sees are the arguments to the macro "xxx from xxx_impl_boilerplate" and the token stream corresponding to the struct declaration. There is no way for this macro to see that xxx has a method called name, the signature of the method, or that the type xxx_impl_boilerplate implements that trait (or indeed, that the name "xxx_impl_boilerplate" event names a type).
Your best bet would be to manage the vtable yourself, and have macros which implement the "inheritence" logic. For example a hypothetical macro like:
impl yyy
{
fn _private_name(&self) -> &str {
&self.name
}
}
impl has_xxx_vtable for yyy
{
fn _get_vtable() -> xxx_vtable {
let mut result = <xxx_impl_boilerplate as has_xxx_vtable>::_get_vtable();
rest.name = _private_name;
return result;
}
}
Note that in this macro expansion, all the information needed by the macro is available to it - it doesn't need to know which methods are provided by the "base class", instead it uses the fact that it it has been given those methods which are being "overriden", and keeps everything else.
You can do as it is done for C : have closure or function fields on the structure that can substitute for virtual methods. For more ergonomics, you can also write a normal method calling such a virtual function.
I think to get the fields embedded directly into the struct you would have to generate a decl macro in the library to add those fields into a struct (prob easier if it just calls a proc macro with its fields and the child struct) and then emit an invocation of that macro for the child struct. Not sure if that will compose well with derive and such though.
For the overriding behavior, the best way is probably to again have a macro be generated in the library. A macro on the child struct's impl block should call that generated macro, which can pass its own methods and the overriding methods to a proc macro that can easily perform the logic to merge both sets of method.