Pre-RFC: Disjoint Polymorphism

This Pre-RFC now lives on github, where it easier to make it evolve: rust-poly/doc/0000-disjoint-polymorphism.md at master · matthieu-m/rust-poly · GitHub

Last edited 2015-05-15 (9 posts in the thread), see Change Log at the end

I have followed the various RFCs for adding "inheritance" in the language (as defined by Servo's extended requirements), but their invasive nature always bugged me.

I very much appreciate the disjoint nature of Rust's trait and struct, which neatly separate payload from behaviour. It is ultimately more flexible than the traditional OO-like code. Servo's requirements add a new dimension usage, but unfortunately most existing RFCs (as referenced in Summary of efficient inheritance RFCs) seem to end up mixing those properties. A notable exception is Fat Object, and this RFC turned out somewhat similar to it.

I have thus tried to take a stab at it, and I am satisfied enough with the current production that here I am solliciting feedback.

Alright, enough talking.

Note: if you are looking for a revolution, you are going to be thoroughly disappointed; an explicit goal was to integrate seamlessly in the language and after several iterations I ended up with a mostly-library solution. I believe it to be a good thing.

Note: this RFC is fairly long, at over 35,000 characters, the impatient can look for the DOM word to get a quick feel of it.


  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • RFC PR: (leave this empty)
  • Rust Issue: (leave this empty)

Summary

Solving the Servo DOM design requirements (and beyond), while integrating smoothly with the already existing trait mechanism.

This RFC provides:

  • Data Polymorphism
  • Trait Polymorphism
  • Thin Polymorphic Pointer/References Support
  • Runtime Type Information (to handle safe down-cast)

Motivation

Rust currently supports polymorphism through its traits, however the experience in Servo has raised a number of requirements which are not fulfilled.

A summary of those requirements is given here:

  • cheap field access from internal methods;
  • cheap dynamic dispatch of methods;
  • cheap down-casting;
  • thin pointers;
  • sharing of fields and methods between definitions;
  • safe, i.e., doesn't require a bunch of transmutes or other unsafe code to be usable;
  • syntactically lightweight or implicit upcasting;
  • calling functions through smartpointers, e.g. fn foo(JSRef, ...);
  • static dispatch of methods.

There have already been a number of proposals (see Summary of Efficient Inheritance RFCs).

This RFC is similar in nature to Fat Objects and tries not to focus on building independent bricks, but instead focus on maximizing integration with the existing code and avoid splitting the Rust landscape into two incompatible run-time polymorphism paradigms, which would hurt re-usability.

This RFC thus designs two disjoint polymorphism paths (one for data, one for interfaces), and a couple core structs to bridge the two, giving ad-hoc inheritance to those who wishes it in manner that let them use all pre-existing structs, traits and functions defined around them. Likewise, the structs and traits they create can be used without "inheritance".

It manages to do so with minimal compiler support, thus opening the door to other library schemes.

Detailed design

This RFC is rather long, as there is a lot to cover, and therefore it is separated in multiple subsections, each building (potentially) upon the precedent. Some sections (or parts) could potentially be used independently of the others.

In keeping with Rust tradition, this RFC preserves the orthogonality of defining data structures (in struct) and defining interfaces (in trait). By doing so, it maximizes the opportunity to mix and match "object-like" polymorphism and trait polymorphism.

As usual, all names are subject to discussion.

Transmutable, Coercible, Convertible

First things first, let us introduce a couple terms.

Transmutable

A type U is said to be transmutable to a type T if the compiler allows std::mem::transmute to convert from U to T. Doing so might lead to undefined results, of course, which is why the method is unsafe.

As an example, Box<T> is transmutable to *const T, irrespectively of whether it is sensible to do so or not.

This relationship is (probably) symmetric.

Coercible

A type U is said to be coercible to a type T when transmuting U to T "works". This is voluntarily vague, and sufficient for this RFC.

As an example, &str is coercible to &[u8], however the reverse is not true since not every byte sequence is a valid UTF-8 sequence.

This relationship is therefore asymmetric. It is also transitive.

The following intrinsic traits core::mem::{Coerce<T>,CoerceRef<T>,CoerceRefMut<T>} are introduced to allow querying this relationship:

unsafe trait Coerce<Target> {
    fn coerce(self) -> Target { mem::transmute(self) }
}

unsafe trait CoerceRef<Target> {
    fn coerce_ref(&self) -> &Target { mem::transmute(&self) }
}

trait CoerceRefMut<Target>: CoerceRef<Target> {
    fn coerce_ref_mut(&mut self) -> &mut Target { mem::transmute(&mut self) }
}

The following blanket implementation is provided too:

unsafe impl<A, B> CoerceRef for A
    where A: Coerce<B>
{
}

It is expected that the compiler then allows implicit coercion from A to B when A: Coerce<B>, from &A to &B when A: CoerceRef<B> and from &mut A to &mut B when A: CoerceRefMut<B>.

Note: if the compiler chases down coercions (as they are transitive) it must take care to avoid running in circles.

Note: mutability references allow modifying the inner part and therefore while str: CoerceRef<[u8]> is viable, str: CoerceRefMut<[u8]> is not because it would allow mutating individual bytes and the resulting str would not necessarily hold a valid UTF-8 sequence.

Convertible

A type U is said to be convertible to a type T if there exists a valid transformation from U to T.

As an example, 'a String is convertible to &'a str, however the reverse is not true as while a String could be produced it would not necessarily have the lifetime 'a.

This relationship is therefore asymmetric. It is also transitive.

Data Polymorphism

Data Polymorphism concerns itself in making it possible to safely access part of a &U via a &T.

Syntax

Grammar:

struct StructName: [pub] StructName (+ [pub] StructName)* {
    // other attributes
}

Example:

struct FirstParent;

struct SecondParent {
    a: int
}

impl SecondParent() {
    fn increment(&mut self) { self.a += 1; }
}

struct Child: FirstParent + pub SecondParent {
    // other attributes
}

fn usage(d: &Child) {
    d.increment();          // calls SecondParent::increment()
    println!("{}", d.a);
}

Semantics

By deriving from another struct, the derived struct embeds its parents' fields. However, due to encapsulation, it can only access fields that it could access if the parent was an attribute.

In essence, the previous example could be rewritten:

struct Child {
    _super_first: FirstParent,      // 0 bytes, but mentioned anyway
    pub _super_second: SecondParent,
    // other attributes
}

fn usage(child: &Child) {
    d._super_second.increment();
    println!("{}", child._super_second.a);
}

Guarantees

In addition to the syntactic sugar, a number of guarantees are made:

  • Child is said to derive from Parent if Parent is an immediate of Child or if any base of Child derives from Parent.
  • &Child is convertible to &Parent, if Child derives from Parent.
  • &Child is coercible to &Parent, if Child derives from Parent and Parent is an empty struct.
  • &Child is coercible to &Parent, if Child derives from Parent and Parent is the first non-empty base of Child.
  • &Child is coercible to &Parent, if, T being the first non-empty base of Child, &T is coercible to &Parent.

For ease of use, the compiler should perform implicit upcasts as necessary, either to access the fields or methods of the Parent. Down-casts will be dealt with in a later chapter.

As shown in the semantics section, pub affects whether a parent is publicly exposed or not. &Child is only safely coercible to &Parent in a given lexical scope if that relationship is accessible from this scope.

Note: the compiler should automatically implement the Coerce and CoerceRef traits, when it makes sense, between Parent and Child. It should never, however, implement the CoerceRefMut trait on its own as it might allow violating some of the child invariants (which need be stricter than the parent per the Liskov's substitution principle); this implementation is left to the user.

Common Ancestor

This RFC defines that () (the unit type) is a common ancestor to all struct:

  • For any T, &T is coercible to &().
  • () being an empty struct, it does not prevent coercibility to any other parent.

Disambiguation

If an ambiguity arises due to conflicting field names or method names, disambiguation requires explicitly casting to the appropriate reference type or simply naming the parent:

fn usage(child: &Child) {
    SecondParent::increment(child);
    println!("{}", (child as &SecondParent).a);
}

Trait Polymorphism

Trait Polymorphism concerns itself in making it possible to safely access part of the interface of a &U via a &T.

State of the art

This is already possible in Rust today:

trait FirstBase {
    fn first_method(&self);
}

trait SecondBase {
    fn second_method(&self);
}

trait Derived: FirstBase + SecondBase {
    fn derived_method(&self);
}

Static Dispatch

FIXME (2015-05-15): this section may still require a review

It is possible to guarantee static dispatch of a trait method, by explicitly selecting the enum or struct for which it was implemented.

For reminder:

struct Child: FirstParent, SecondParent { ... }
impl FirstBase for FirstParent { ... }
impl Child {
    fn first_method(&self);
}

fn doit(child: &Child) {
    <Child as FirstBase>::first_method(child);
}

A more complicated case may imply also having a struct ambiguity, it is resolved similarly:

impl SecondBase for FirstParent { ... }
impl SecondBase for SecondParent { ... }
// Note: SecondBase not implemented for Child

fn doit(child: &Child) {
    <SecondParent as SecondBase>::second_method(child);
}

Trait implementation re-use

Much like a struct may access its parent's field or method unambiguously, so may a trait.

By default, should a parent of a struct implement a trait, it is not necessary for the struct to implement any method, though it still has to explicitly declare the trait implementation:

impl FirstBase for FirstParent {
    //
}

impl FirstBase for Child {} // automatic, re-use FirstParent's implementation

In the case that multiple parents of a struct could provide the trait implementation, then the implementation requires disambiguation. A manual implementation must be provided, it may delegate using Static Dispatch as appropriate:

impl FirstBase for Child {
    fn first_method(&self) {
        <SecondParent as FirstBase>::first_method(self);
    }
}

A macro could be provide to perform the boring job of forwarding the arguments appropriately, although this case should probably be rare enough in practice that it might just no be worth it.

core::rtti

In order to provide all the necessary functionality, a new module is introduced: core::rtti, re-exported as std::rtti as part of the std facade.

TypeInfo, VTable and VRef

The intrinsic types core::rtti::{TypeInfo,VTable} and the library type core::rtti::VRef<Trait> are introduced:

struct TypeInfo {
    size: usize,
    align: usize,       // Note: log2(align) could be packed in unused bytes of size.
    trait_id: TypeId,
    struct_id: TypeId,
    //  ... others ?
    down_cast_trait: fn (TypeId) -> Option<&'static VTable>,
    down_cast_struct: fn (TypeId) -> bool,
}

struct VTable {
    type_info: &'static TypeInfo,
}

struct VRef<Trait> {
    vtable: &'static VTable,
}

impl<Trait> VRef<Trait> {
    fn get_type_info(&self) -> &'static TypeInfo;

    fn drop(&self, it: &mut ()) {
        unsafe {
            // only valid for below layout on x86/x86_64 
            let vptr = self.vtable as *const isize;
            let drop = vptr.offset(1);
            let drop: fn (&mut ()) -> () = mem::transmute(*drop);
            drop(it)
        }
    }

    fn get_vtable(&self) -> &'static VTable;

    // ...
}

The methods of the VTable are not explicit mentioned because their number depends on the particular trait being implemented, so instead a raw memory layout similar to the following is expected:

trait A: Clone + B + E {}
trait B { fn bar(&self); }
trait E {}

//
//  Memory Layout of the VTable of A
//
//  Each cell [n] represent a pointer-sized memory cell (on x86/x86_64)
//  where n is the compile-time index giving access to its content.
//
//                       VRef<A>     VRef<Clone>     VRef<B>     VRef<E>
//                          |           |               |           |
//                         +-+         +-+              |          +-+
//  [0] VTable (A)         + +         + +              |          + +
//                         + +         + +              |          + +
//  [1] Drop::drop         + +         + +              |          + +
//                         + +         + +              |          +-+
//  [2] Clone::clone       + +         + +              |
//                         + +         + +              |
//  [3] Clone::clone_from  + +         + +              |
//                         + +         +-+             +-+
//  [4] VTable (B)         + +                         + +
//                         + +                         + +
//  [5] Drop::drop         + +                         + +
//                         + +                         + +
//  [6] B::bar             + +                         + +
//                         +-+                         +-+
//
//  Both VTable (A) and VTable (B) refer to TypeInfo (A).
//

The following guaranteed are made:

  • VRef<Derived> is convertible to VRef<Base> if Base is a super-trait of Derived.
  • VRef<Base> is dynamically convertible to VRef<Derived> if Base is a super-trait of Derived, the conversion may fail if the current object does not implement Derived though.
  • VRef<Derived> is coercible to VRef<Base> if Base is an empty trait.
  • VRef<Derived> is coercible to VRef<Base> if Base is the first non-empty super-trait of Derived.
  • VRef<Derived> is coercible to VRef<Base> if, T being the first non-empty super-trait of Derived, VRef<T> is coercible to VRef<Base>.

The design of TypeInfo and VTable was done with simplicity in mind, as a proof of concept. Their final implementation might varied wildly depending on target architecture, compilation options and benchmark results.

Class and DynClass

The library types core::rtti::{Class<Trait, Struct>, DynClass<Trait, Struct>} are introduced:

#[repr(C)]      // Need to ensure layout compatibility with DynClass
struct Class<T, S>
    where S: T
{
    vref: VRef<T>,
    data: S,
}

#[repr(C)]      // Need to ensure layout compatibility with Class
struct DynClass<T, S> {
    vref: VRef<T>,
    data: S,
    _: [u8],    // Attempt to prevent `DynClass` from being accidentally used by value
}

impl<T, S> DynClass {
    fn as_trait(&self) -> &T;
    fn as_trait_mut(&mut self) -> &mut T;
    fn as_struct(&self) -> &S;
    fn as_struct_mut(&mut self) -> &mut S;
}

It should be noted that a Class is just a package and does not in itself allow any polymorphism. Polymorphism is delegated to DynClass, which can be created from a Class:

  • Class<T, S> is convertible to DynClass<T, S>
  • &Class<T, S> is coercible to &DynClass<T, S>
  • &mut Class<T, S> is coercible to &mut DynClass<T, S>`

DynClass itself implements type erasure and thus offers polymorphic behaviour. Based on the coercible properties of VRef and struct, we get:

  • &DynClass<D, C> is coercible to &DynClass<B, C> if VRef<D> is coercible to VRef<B>.
  • &mut DynClass<D, C> is coercible to &mut DynClass<B, C> if VRef<D> is coercible to VRef<B>.
  • &DynClass<D, C> is coercible to &DynClass<D, P> if &C is coercible to &P.
  • &mut DynClass<D, C> is coercible to &mut DynClass<D, P> if &mut C is coercible to &mut P.

Note: unlike Class, DynClass does not require that S implements T. This is intentional, it allows flexibility in the coercion of references. It is still safe because it can only be created from Class so that the actual type stored (most-derived type) does implement T.

Dyn

While one can always use () as a common ancestor in the above types, this RFC introduces a few type aliases core::rtti::Dyn<Trait> as a short-hand.

type Dyn<T> = DynClass<T, ()>;

DownCast, DownCastRef and DownCastRefMut

In order to control down-casts, the traits core::rtti::{DownCast<Target>, DownCastRef<Target>, DownCastRefMut<Target>} are introduced.

trait DownCast<Target> {
    fn down_cast(Self) -> Result<Target, Self>;

    unsafe fn unchecked_down_cast(Self) -> Target;
}

trait DownCastRef<Target> {
    fn down_cast_ref(&Self) -> Option<&Target>;

    unsafe fn unchecked_down_cast_ref(&Self) -> &Target;
}

trait DownCastRefMut<Target> {
    fn down_cast_ref_mut(&mut Self) -> Option<&mut Target>;

    unsafe fn unchecked_down_cast_ref_mut(&mut Self) -> &mut Target;
}

The unsafe functions are provided for speed, they performs the necessary runtime adjustments (if any) without checking whether the down-cast is valid or not.

On the contrary, the safe functions perform the check, and indicate the cast failure thanks to their more elaborate return type.

Note: there is no blanket implementation of DownCastRef or DownCastRefMut based on DownCast because the latter allow more operations (offset adjustments) than the former.

Casting

It would be better if the compiler could perform the up-casts implicitly, much like with Deref, there seems to be little point in having the user fiddling with as over and over.

The type DynClass implement the DownCast and Coerce traits as necessary.

impl<T, S> Drop for DynClass<T, S> {
    fn drop(&mut self) {
        unsafe {
            let data: &mut () = core::mem::transmute(&mut self.data);
            self.vref.get_type_info().drop(data);
        }
    }
}

unsafe impl<T, S, B, P> Coerce<Box<DynClass<B, P>>> for Box<DynClass<T, S>>
    where VRef<T>: Coerce<VRef<B>>,
          S: P, S: CoerceRef<P>
{
}

unsafe impl<T, S, B, P> CoerceRef<DynClass<B, P>> for DynClass<T, S>
    where VRef<T>: Coerce<VRef<B>>,
          S: P, S: CoerceRef<P>
{
}

impl<T, S, B, P> CoerceRefMut<DynClass<B, P>> for DynClass<T, S>
    where VRef<T>: Coerce<VRef<B>>,
          S: P, S: CoerceRefMut<P>
{
}

impl<T, S, D, C> DownCast<Box<DynClass<D, C>>> for Box<DynClass<T, S>>
    where D: T,
          C: S, C: CoerceRef<S>
{
    // ...
}

impl<T, S, D, C> DownCastRef<DynClass<D, C>> for DynClass<T, S>
    where VRef<D>: Coerce<VRef<T>>,
          C: S, C: CoerceRef<S>
{
    // ...
}

impl<T, S, D, C> DownCastRefMut<DynClass<D, C>> for DynClass<T, S>
    where VRef<D>: Coerce<VRef<T>>,
          C: S, C: CoerceRefMut<S>
{
    // ...
}

Interaction with Box, Rc, ...

There is no built-in way in Box::new to select the desired size and alignment, so dedicated helper functions should be provided to convert in and out of DynClass:

use core::mem;

fn make_dynamic<T, S>(original: Box<Class<T, S>>) -> Box<DynClass<T, S>> {
    unsafe { mem::transmute(original) }
}

fn make_static<T, S>(original: Box<DynClass<T, S>>) -> Result<Box<Class<T, S>, Box<DynClass<T, S>>> {
    let allowed = original.vref.get_type_info().trait_id == TypeId::of::<T>() &&
                  original.vref.get_type_info().struct_id == TypeId::of::<S>();

    if (allowed) { Ok(mem::transmute(original)) }
    else         { Err(original) }
}

fn up_cast<T, S, B, P>(original: Box<DynClass<T, S>>) -> Box<DynClass<B, P>>
    where T: B, S: P, S: CoerceRef<P>;

Sanity Check

Let us verify how each of the requirements were addressed in this RFC.

  • cheap field access from internal methods
    • provided via monomorphization (of trait methods) or via &Struct conversions.
  • cheap dynamic dispatch of methods
    • the call is performed like with regular traits, this RFC only changes the packaging.
  • cheap down-casting
    • down-casting to the most-derived trait/struct may be cheap, a simple comparison of TypeId.
    • down-casting to an intermediate trait/struct may be slightly more costly, as a few more comparisons will be necessary.
  • thin pointers
    • provided, though lack of integrated support make them slightly awkward.
  • sharing of fields and methods between definitions
    • provided by struct and trait polymorphism.
  • safe, i.e., doesn't require a bunch of transmutes or other unsafe code to be usable
    • unsafe will be present in the core library, however user code should not need it, though some checks bypass are so provided.
  • syntactically lightweight or implicit upcasting
    • it is expected that the compiler will perform the upcasting by itself (through tight integration of Coerce and CoerceRef), otherwise calling the methods will be necessary.
  • calling functions through smart pointers, e.g. fn foo(JSRef, ...)
    • through Deref
  • static dispatch of methods
    • provided by the regular procedural syntax call (see Static Dispatch)

Implementing the DOM according to requirements

Let us now how an example of a simple DOM would look like given those facilities, as it is the reference example used by the existing RFCs.

trait Node {}

struct NodeData {
    parent: Rc<DynClass<Node, NodeData>>,
    first_child: Rc<DynClass<Node, NodeData>>,
}

impl Node for NodeData {}

struct TextNode: NodeData {
}

impl Node for TextNode {}

trait Element: Node {
    fn before_set_attr(&mut self, key: &str, val: &str) { ... }
    fn after_set_attr(&mut self, key: &str, val: &str) { ... }
}

struct ElementData: NodeData {
    attrs: HashMap<String, String>,
}

// Note: private access to ElementData::data, ensuring invariants;
//       also, this method is always statically dispatched and thus inlinable.
impl ElementData {
    fn set_attribute(&mut self, key: &str, value: &str) {
        self.before_set_attr(key, value);
        // update
        self.after_set_attr(key, value);
    }
}

impl Element for ElementData {}

struct HTMLImageElement: ElementData {
}

impl Node for HTMLImageElement {}

impl Element for HTMLImageElement {
    fn before_set_attr(&mut self, key: &str, val: &str) {
        if key == "src" {
            // remove cached image
        }
        <ElementData as Element>::before_set_attr(key, value);
    }
}

struct HTMLVideoElement: ElementData {
    cross_origin: bool,
}

impl Node for HTMLVideoElement {}

impl Element for HTMLVideoElement {
    fn after_set_attr(&mut self, key: &str, value: &str) {
        if key == "crossOrigin" {
            self.cross_origin = (value == "true");
        }
        <ElementData as Element>::after_set_attr(key, value);
    }
}

fn process_any_element<'a>(element: &'a DynClass<Element, ElementData>) { ... }

fn main() {
    let video_element: Rc<Class<Element, HTMLVideoElement>> =
        Rc::new(Class::new(HTMLVideoElement::new(...)));

    process_any_element(&*video_element);

    let node = video_element.first_child.clone();

    if let Some(element): Option<&DynClass<Element, ElementData>> = node.down_cast_ref() {
        // ...
    } else if let Some(text): Option<&DynClass<Element, TextNode>> = node.down_cast_ref() {
        // unreachable, because TextNode derives from ElementData ...
        // ...
    } else {
        // ...
    }
}

Wrapping up

This RFC proposes an extremely lightweight way to add polymorphism. In terms of compiler integration. Only 2 intrinsic types (TypeInfo and VTable) and 2 intrinsic traits (Coerce, CoerceRef) are necessary, with all the functionality then being implemented in library code.

Furthermore, it supplements already existing Rust polymorphism without duplicating its functionality, therefore guaranteeing to the user that libraries using this functionality or not can still be hooked up together with little to no boilerplate.

Yet, despite being lightweight and rusty, it is quite possible to translate traditional objects hierarchy with a one-to-one mapping, use type aliases to mask the novelty and type inference to avoid ever mentioning it directly.

Drawbacks

  • no sugar around box: unfortunately, the story about box is unclear yet, so we can only provide a helper method, this makes creating/cloning smart pointers awkward.
  • heavier syntax (&T vs &Dyn<T>): it is expected that the need for such bundling be rare, and it is possible to convert to &T immediately (so that only struct code and not fn code be affected).
  • opinionated: not many building blocks here.

Alternatives

Comparison to existing RFCs

There are many other RFCs, as already mentioned:

  • #9: Fat Objects
  • #11: Extending Enums
  • #223: Trait Based Inheritance
  • #250: Associated Field Inheritance

This RFC emphasizes flexibility and a clean separation of concern between payload (struct), behaviour (trait) and usage (&T or &Dyn<T>). The same struct or trait can freely be shared in situations where thin pointers are desirable or not.

This RFC can be seen as a refined version of Fat Objects (#9), proposing a more fully fleshed out implementation and simplifying the implementation of non-virtual methods by simply adding them to the struct rather than creating an extraneous trait. It could actually benefit from a better integration of custom DSTs in the language, as DynClass is not too well integrated.

Compared to ... (#11), this RFC does not require distinguishing between enum that can be extended and enum that cannot (mixes payload and usage). This distinction already exists today in C++ (inheriting a class without a virtual destructor) and has proven to be a pain point of the language; it introduces a split in the language ecosystem between those struct that can be extended and those that cannot. On the contrary, this RFC emphasizes that every existing trait and struct can be reused, and no foresight is necessary when designing new ones. It is also significantly less ambitious, and does not attempt any large scale changes to the language beyond fulfilling the given requirements.

Compared to Extend (#223) or associated fields (#250), this RFC does not inject data in traits (mixes payload and behaviour). I consider this its main advantage. It neatly sidesteps the issue of splitting the ecosystem into stateful traits and stateless traits, and therefore guarantees that traits can be shared between any library, in any direction.

Compared to the associated fields (#250), this RFC's approach to fields is both cheaper than the indirect fields approach (with its required offset in v-table per field) and less constrained than the #[repr(fixed)] approach (which precludes implementing two fixed traits with contradicting requirements). It also does not require the compiler to try and guarantee the non-aliasing of fields. On the other hand, it is obviously less flexible given its conservative choice (no renaming/re-arrangement).

Compared to the associated fields (#250), this RFC's approach does not require that common fields be public, which is a violation of encapsulation. The struct can define methods with exclusive access to its fields, guaranteeing the invariants of its choice, and because those methods are not polymorphic they can be easily inlined. Still, if desired, its fields can be public.

Compared to the Internal Vtable (#250), this RFC once again avoids enforcing that a struct or trait only be usable in a particular way (mixes payload and usage). This once again allows using either the struct or trait in other contexts, where this particular representation would be less attractive (it is known that LLVM has issues with devirtualizing calls through internal v-pointers, for example).

Explicit Coercions

It is possible to require explicit coercions, however it seems strange in light of the Deref precedent and may lead to a usability hit because of the extra verbosity. Still, inference might make it sufficiently lightweight.

No disambiguation in Data Polymorphism

It is possible to remove the necessity for disambiguation in parent's field or method selection by simply forbidding ambiguities, after all this can be checked easily enough.

Given that the necessity for disambiguation already arises when a single struct implements multiple trait with the same method name, it seems little effort to allow it.

On the other hand, the potential usability blow in forbidding ambiguities would be important. There is a reason it is allowed for traits: requiring two 3rd party libraries not to impinge on each other names is reminiscent of the C days, where there were no namespace.

Alternative Syntax for Data Polymorphism and disambiguation

Another syntax could be considered to provide data polymorphism, for example by using an attribute on a data-member (as #230).

Similarly, another syntax could be considered to provide disambiguation. C++ for example uses child.Parent::method().

The current syntaxes, however, were selected by mirroring the syntax of trait polymorphism and trait disambiguation (respectively), increasing their accessibility and trying to avoid making the learning curve even steeper than it already is.

Still, as with naming, this is open to debate.

Public/Private Data Polymorphism

As proposed, a struct may derive from another either publicly struct A: pub B or privately struct A: B with the latter being the default (as is usual in Rust: more capability is opt-in).

This RFC would be pretty much as useful without this distinction, in which case there are two alternatives:

  • only public polymorphism: the diamond inheritance problem becomes bothersome, as the work-around today relies on privacy to enforce data coherence.
  • only private polymorphism: in which case the user can publicize the relationship by implementing Coerce and CoerceRef manually; those traits are unsafe though.

I would lean toward the latter solution as it offers more encapsulation. It is in line with fields (private by default) and not with traits (extended traits are public), and therefore since it adds fields it seems logical to follow their precedent.

Dyn ?

The Dyn alias is optional. It is purely added for convenience.

No common ancestor ()

There does not seem to be any disadvantage in having () be a common ancestor to all struct given that it has not capability and no overhead.

Still, if it were rejected, the Dyn alias could still be implemented though as full-blown struct instead.

DynMultiClass ?

In order to navigate through an inheritance tree, more information might be required than just a thin pointer, so as to be able to point within the data.

It is possible to arrange the VTable structure such that navigating is easy, as demonstrated, by encoding a back pointer to the most derived type TypeInfo to the start of the VTable before each subtype's inlined list of methods. It might or might not be desirable, the cost being relatively minor as a single instance of a given VTable exists, and this instance is tightly packed in ROM.

This strategy, however, is probably less desirable for struct, since those are destined to be massively instantiated. In this case, a dedicated struct DynMultiClass could be added to core::rtti, which would maintain an third field (compared to DynClass): an offset in the struct.

This RFC seems complicated enough as it is, without inviting more trouble, so this is tabled for now.

Note: the signature of down_cast_struct would become fn (TypeId, isize) -> Option<isize> for example.

Drop

In this proposal, when rtti is enabled and a trait has multiple non-empty bases, the drop glue is duplicated at the start of the VTable of each non-empty base.

An alternative would be, in this case, to hoist it inside TypeInfo. Of course, it would then require two dereferences instead of one to access drop, which might cause performance penalties.

However, one more pointer in the VTable is not that costly (there are only so many of them), therefore this RFC promotes duplication, at least as the default case.

down_cast_trait/down_cast_struct unification?

For clarity, two methods were provided.

Having two methods can be beneficial, as each is significantly simpler, however it can also lead to extra overhead (two indirect calls) when down-casting on both the trait and struct axes. A unified method could be implemented, either as replacement or in addition.

Diamond inheritance handling

Diamond inheritance in traits is already handled: the caller must disambiguate which super-trait she wishes to call.

Diamond inheritance in structs is however much more complicated, there is a significant difference between having one field or two. A cursory search reveals that few languages have attempted to provide a method to share fields, most being content enough to be able to disambiguate.

Furthermore, a user-land solution to inconsistency exists: if parents are private (as the default), then it is up to the child struct to re-export their methods, and doing so it can easily enough ensure the consistency of both data. It is, of course, somewhat onerous and error-prone.

It seems that any language solution to this problem could be added backward compatibly.

This RFC seems complicated enough as it is, without inviting more trouble, so this is tabled for now.

Unresolved questions

  • trait impl cannot be private today, should we try to keep the Child/Parent relationship secret and if so how? Wiring privacy into C: P checks?
  • How to guarantee that VRef only take a trait parameter? The answer is probably linked to how to get VRef to obtain its &'static VTable pointer.

Change Log

2015-05-15

  • Introduce CoerceRefMut: &str is coercible to &[u8] but making &mut str coercible to &mut [u8] may violate its soundness. Thus CoerceRefMut is now opt-in.
  • Introduce DownCastRefMut for the same reason.
  • Refactor Coerce and co to avoid associated types, as it seems that this prevents them from being implemented multiple times (with different targets) for the same time. In hindsight, it kind of feel obvious.
  • Refactor DownCast and co for the same reasons.
  • Remove any talk about tail-padding, as per cmr comment; it's left to an implementation detail.
  • Remove any talk about cfg attributes, it's distracting.
  • Expand Static Dispatch section
  • DynClass cannot implement multiple Deref, so move to explicit methods
7 Likes

The proposal looks really nice. There are two things I don’t understand, although this may be due to me having a lack of understanding of rust in general. The proposal proposes to add a cfg attribute to control the emission of vtables–why can you not have thin pointers which can be cast to fat pointers? My other question is that rtti appears to duplicate Any–would it be possible to adapt Any and have greater control over whether or not run-time type information is included?

Trait impls cannot be private. You either have the impl, or you don't.

What is the motivation for this?

This isn't strictly true, because of the repr attribute.

This is probably fine, but needs the additional restriction that the offset from the base of the parent is of a suitable alignment for the whole struct, and that it (minus any of its "tail padding") fits in the tail padding of the parent. Unless you're proposing individual fields get packed in the padding, instead of the whole thing, and that it gets extended as necessary? I think this section should be removed from the RFC. It's an implementation detail. I foresee complications arising when &mut gets involved (for example, *a = b might blast the padding. We don't define what happens to the padding, which is sometimes beneficial as it enables simpler codegen at times).

Is it not always possible? Today, if you don't have a trait object, you always get static dispatch. This section isn't very clear to me, I'm not sure what it's trying to point out. I have no idea what Child is or its relation to any other structs or traits.

There's no way to ensure that the type parameter is only substituted with a trait.

Does DynClass actually need to be unsized? Isn't it enough that it not be Copy? (Since it's only ever behind a & (unless it can be behind a &mut? You never mention &mut here), you cannot move out of the &).


Ok, so finishing the RFC I see you address some of this in the "Alternatives". But, you claim that these polymorphism traits can be implemented in a library. I agree! Considering how complex this system is, I would like to see an out-of-tree implementation of just the library bits that uses hand-rolled vtables as necessary. Compiler magic is not necessary to validate the idea.

I'm very uncomfortable with this proposal. Something about it sits wrong with me. That's about all the concrete feedback I have now, though.

1 Like

One aspect of this I really like is how you sperated data and trait polymorphism. On mobile I will try and add to this when I get access to a computer.

What sits wrong with you specifically? It actually doesn’t seem like a terribly complex proposal to me–that a lot of the stuff it’s describing can be done in-language strikes me as only a good thing.

My main concern was that despite the length and the in-depth nature of the description, I still felt pretty uncertain about some of the technical details. I don’t think this is due to complexity, but rather that a lot of the stuff just doesn’t seem necessary (to me, anyway).

For starters, I absolutely love the idea of Transmutable, Coercible and Conertible. I think these will be very important towards making a broad range of patterns safe, inheritance being only one of them. If the compiler can work these out automatically, or they can inform layout, so much the better.

I do not think Coerce is a good idea. There’s a reason we don’t have DerefMove yet. In particular, making Coerce happen automatically sounds like a recipe for a lot of either bugs or surprising behavior, since it can chop off part of a structure!

I like the idea of inheritance being sugar for deriving a ton of traits. In fact, I would much rather do it as a deriving / glorified macro (perhaps a deriving with arguments–this is something I’ve actually wanted for a while, though I don’t know if it’s feasible in the grammar) rather than as a more heavyweight language feature. Because this only affects data layout, concerns about how it works with generic / trait subtyping don’t apply.

A fair amount of the padding section seems not too interesting to me, though I can’t speak for others. I think people would probably be okay with requiring repr(C) if you’re relying on shared layout between two structs (it’s currently required for transmute to be well-defined).

I’m much more interested in how all of this stuff applies to enums–does it? If not, it seems like a wasted opportunity, since one case that I frequently want is to be able to turn a set of enums into a larger or smaller set of enums that share the same base (as long as I’ve verified beforehand that all the necessary variants are shared between them).

I’m also interested in how generics fit in with all this. They aren’t really discussed, so I’m not sure if they present any additional challenges when it comes to dealing with alignment and so on.

I am not really sure about the part where you have to specify which implementations from the parent you are using. What does that buy you, exactly? What if it’s a new trait that’s defined in your own crate; do you have to define it for both the parent and any children you’re using it for there, too?

In general I’m not a fan of structural inheritance also implying implementation inheritance–the structural sharing feels like it should be able to stand on its own, and the trait inheritance for structures could just as easily be replaced with macros in the rare cases where it’s actually desired.

All the cfg stuff sounds pretty bad to me. Why do we need it for every trait? We already have a Reflect OIBIT, which seems like a perfect candidate for deciding whether or not to expose RTTI information, and trait objects, which expose vtable information; neither option splits the language based on whether you want to generate vtables or not.

I flat out don’t understand why you need this new and elaborate rtti model for downcasts. Maybe there’s something I’m missing, but what is wrong with just using the type ID that we already have (which, again, is already bounded by Reflect)? This is how Any does it and it seems to work fine. Also, I don’t know Servo’s precise requirements, but downcasting to an intermediate trait may be what they’re interested in, since Rust can already downcast just fine to an intermediate type (well… I’m not actually exactly sure why they need cheap downcasting at all, so I probably shouldn’t presume that).

Thin pointers are probably the most important thing Servo needs, and also probably the most useful thing I expect to come out of all these discussions, so it’s a bit weird that they receive so little attention here.

() as a common ancestor really makes no sense to me. What about, say, [T ; 0], or PhantomData? I think the proposal would be better without this.

2 Likes

I don't quite get what you mean. From my understanding the proposal seems to suggest adding Coerce to add the concept of something being Transmutable, Coercible and Convertible.

So, to answer your questions:

  • vtable: the thin pointers can be cast to fat pointers (the reverse is not necessarily true), actually, using a single brand of vtable is the goal and this attribute just controls whether one wishes for dynamic polymorphism or not; as discussed in the alternatives it could be left out easily.

  • rtti: I believe Any could be optimized to take advantage of the newly emitted RTTI information to navigate the type hierarchy. As far as I know today you can only get the “most-derived-type” out of Any whereas with RTTI information you could down-cast to any type in the hierarchy.

My main concern was that despite the length and the in-depth nature of the description, I still felt pretty uncertain about some of the technical details. I don't think this is due to complexity, but rather that a lot of the stuff just doesn't seem necessary (to me, anyway).

I'll put up a revision which purges out all talk about tail-padding and cfg configurability.

I do not think Coerce is a good idea. There's a reason we don't have DerefMove yet. In particular, making Coerce happen automatically sounds like a recipe for a lot of either bugs or surprising behavior, since it can chop off part of a structure!

Actually, std::mem::transmute refuses to transmute between data of difference sizes. I would also note the trait is unsafe precisely because you are toying with complications here.

A fair amount of the padding section seems not too interesting to me, though I can't speak for others. I think people would probably be okay with requiring repr(C) if you're relying on shared layout between two structs (it's currently required for transmute to be well-defined).

I totally scrapped it out, it was distracting.

I'm much more interested in how all of this stuff applies to enums--does it? If not, it seems like a wasted opportunity, since one case that I frequently want is to be able to turn a set of enums into a larger or smaller set of enums that share the same base (as long as I've verified beforehand that all the necessary variants are shared between them).

No, not one bit.

It is easy for struct to share fields, but for enum to share variants brings complications:

  • if you extend the enum (add more variants), then matches on the parent enum do not cover the extra variants
  • if you refine the enum (remove some variants), then by seeing a mutable reference to the parent, you can add those variants

As a result, it seems that with enum the number of variants must be fixed. I cannot see how to deal with that.

I'm also interested in how generics fit in with all this. They aren't really discussed, so I'm not sure if they present any additional challenges when it comes to dealing with alignment and so on.

I do not think so; they were not discussed because I did not see any additional challenges. I may just be short-sighted, of course.

I am not really sure about the part where you have to specify which implementations from the parent you are using. What does that buy you, exactly? What if it's a new trait that's defined in your own crate; do you have to define it for both the parent and any children you're using it for there, too?

Rust tries to shy away as much as possible from implicit assumptions; I was just trying to play it safe.

All the cfg stuff sounds pretty bad to me. Why do we need it for every trait? We already have a Reflect OIBIT, which seems like a perfect candidate for deciding whether or not to expose RTTI information, and trait objects, which expose vtable information; neither option splits the language based on whether you want to generate vtables or not.

I flat out don't understand why you need this new and elaborate rtti model for downcasts. Maybe there's something I'm missing, but what is wrong with just using the type ID that we already have (which, again, is already bounded by Reflect)? This is how Any does it and it seems to work fine. Also, I don't know Servo's precise requirements, but downcasting to an intermediate trait may be what they're interested in, since Rust can already downcast just fine to an intermediate type (well... I'm not actually exactly sure why they need cheap downcasting at all, so I probably shouldn't presume that).

I must admit having some difficulties in using Any, so it may well be that it can accomplish much more than I credit it for.

As far as I could see in my exploration, though, Any seemed to suffer from two issues:

  • it could only be build from a concrete type
  • it could only down-cast to that same concrete type

I've tried to coax it to down-cast to traits, but was left quite frustrated by the experience (downcast_mut only targets Sized types, for example).

Thin pointers are probably the most important thing Servo needs, and also probably the most useful thing I expect to come out of all these discussions, so it's a bit weird that they receive so little attention here.

Yes, all of that to enable thin pointers... Box<DynClass<_, _>> is a thin pointer, and so is Box<Dyn<_>>.

() as a common ancestor really makes no sense to me. What about, say, [T ; 0], or PhantomData? I think the proposal would be better without this.

Actually, it seems the de-facto common ancestor already; I trudge through a few standard libraries which use an unknown type (in unsafe code) and it seems to always end up being either *const () or *mut ().

I thought about leaving it out, however I really wanted to present Dyn<T> as a thin pointer alternative to &T and the cheapest option was for it to be an alias of DynClass<T, ()>. An earlier version of the RFC had a whole new lot type, with conversions back and forth, it felt quite complicated for not that much.

1 Like

Thanks a lot for the detailed answer.

Trait impls cannot be private. You either have the impl, or you don't.

This begs the question of whether advertising the relationship between child and parent is mandatory / not important / harmful.

Another solution would be to pin the condition for checking whether two struct are in a relationship hinge on Child: Parent syntax (like for traits) and align this check on the privacy setting selected.

Do you think it would work better?

What is the motivation for [defining () as common ancestor]?

It is a de-facto ancestor today in the standard libraries, it seems, as *const () and *mut () are used a lot.

I was hoping to build on this to allow a slick definition of Dyn<T>.

I would find it unfortunate for multiple developers to come up with incompatible empty structs just to have a "data-less" thin pointers as they would not be compatible with each others.

I think this section should be removed from the RFC. It's an implementation detail.

Agreed, I am culling it. The RFC is big enough as it is, and such guarantees are easier to add than remove.

Is [static dispatch] not always possible?

I need to rework this section.

There's no way to ensure that the type parameter is only substituted with a trait.

Quite annoying; indeed. There is also a big "blank" here in how the VRef instance is supposed to get a hold of the &'static VTable corresponding to its Trait parameter...

The parameter today is mostly used in where clause; to ensure that two traits can share the same v-pointer.

I need to sleep on this.

Does DynClass actually need to be unsized?

My apologies here, DynClass can indeed be used behind &mut much like a trait can; it would otherwise be too restrictive. On top of that, being able to return a DynClass by value would lead to slicing.

DynClass should never be stack-allocated; nor should it be passed (or returned) by value. It's a view into a bigger object.

And I currently see no way how to ensure those restrictions are followed (short of the _: [u8] trick, which will fall short whenever the compiler is upgraded to be able to return DST).

That's about all the concrete feedback I have now, though.

It was helpful, thank you for your time.

I'm very uncomfortable with this proposal. Something about it sits wrong with me.

This sounds ominous. Without imposing, I would really like if you could find out what is causing this bad intuition.

But, you claim that these polymorphism traits can be implemented in a library.

Yes, good idea. It will also help me sort out the bugs that slipped through the crack; I've got some time on my hands in the coming week, so I'll try to hack it.

1 Like

So you would like Coerce to be applied whenever applicable and necessary? I'm not sure if the rewrite system implied by this and Deref (and Convertible?) is terminating or confluent. I'd need to sit down and think about it some more. No longer using an associated type worries me. A precise algorithm would be really appreciated!

I would note that there is neither a Convertible nor a a Transmutable trait, only the Coerce/CoerceRef/CoerceRefMut family.

I am not too satisfied with the move away from associated types either, as it means that the author of the struct loses control. With associated types only the crate containing the struct could implement the various Coerce, which makes it more resilient to changes (even though it’s labelled unsafe).

On the other hand, an associated type in Coerce and co restricts to single “inheritance” line. Today, as defined here, you can only safely coerce into:

  • an empty type
  • the first non-empty type

Maybe the lack of multiple targets would not be too much of an issue if we instead let the user pick to which type he wishes for automatic coercion; however it would only be safe if the compiler could check that the coercion is valid, so the trait would need some compiler “hook”, and I am wary of making the use of unsafe common-place.

That being said, the problem here might be that I use Coerce for two roles:

  • the ability to auto Deref
  • as a constraint on functions

I have to see what I could do if Coerce was used simply as constraint, and the user had to implement Deref for smooth conversions. It would remove the dual mechanism and the various ambiguities or surprising behaviours that could arise from it.

Note: this seems like an instance where associated constants (and the ability to check them in where clauses) would really help; with a trait Derived<T> where Self: T { isize const offset; }, we would not need Coerce for constraints check.

One potiential reason to perhaps not use () is that it creates two ways of extending every single type:

impl TraitName for T {...}

and

impl TraitName for () {...}

Interesting, however the two are not actually equivalent.

While a Child can easily “re-export” its Parent traits, it has to do so explicitly impl TraitName for Child {} in the current proposal. Therefore:

  • traits implemented for any T (no bound) are implicitly implemented for any type
  • traits implemented for () have to be implicitly re-implemented for any type, though that type might delegate the implementation by not overriding any method

It does produce a conflict though: now you can provide default method implementations either by doing so in the trait or by doing so for ().

It is unclear whether this is an issue.

I would need to spend some more time and effort to really understand most of this. (If you feel like helping me (there’s no obligation!), one thing that I suspect might be very helpful is if the RFC were organized more along the lines of “1. this is what we want to be able to do (e.g. such-and-such downcasting) 2. this is why we want to be able to do it (e.g. something something DOM) 3. this is how we propose to make it possible (e.g. rtti stuff)” (or maybe with 1 and 2 swapped) - for each feature. Right now it feels like a quick recap of requirements at the beginning and end, and a big grab-bag of features in the middle, with the connections and motivations not being immediately obvious. I suppose all these might be more apparent to someone who’s been following the various “inheritance-ish” RFCs in depth, but I regrettably haven’t.)

A couple of quick comments I can make though -

It’s my fault for introducing this confusion originally (in the proposal I made in haste) and the damage is probably irreparable, but I really think Coerce/“Coercible” is the wrong terminology here in the Rustic context. It’s an unfortunate case that the Haskell and Rust communities use the same words to mean different things, and I naively imported not just the concept but also its name from how GHC has it. But to recap, for the concept of reinterpreting the raw bytes of one type as another type, Haskell uses the word “coerce” (Coercible, Coercion, coerce, and unsafeCoerce) - and for the same thing, Rust uses the word “transmute” (our mem::transmute is the same thing as their unsafeCoerce). Meanwhile, Rust already uses the word “coerce”/“coercion” to mean a different thing: automatic compiler-inserted conversions between types (which are not in general reinterpretations of raw bytes - e.g. what we call “deref coercions”, which invoke Deref::deref rather than mem::transmute).

Basically, I would prefer to use something like “safely transmutable” where you currently have “coercible”.

And with respect to the formulation of the traits, I believe it is your intent that the implementation (as currently named) of coerce{,_ref{,_mut}} is always the same - it’s always mem::transmute. In other words, implementors of these traits should not be able to override these methods with different implementations. If that’s the case, the right thing to do would be to take the method out of the trait (the trait is now method-less) and provide it as a free-standing function instead (which requires the given trait as a bound).

Note: I'm the author of Trait Based Inheritance.

I think that it isn't sufficient to leave this vague in a final, on-the-repo RFC, but it sounds like this is exactly the same relation that @glaebhoerl and I put forward as in RFC 91, which does give a more formal description. Note that RFC 91 uses the term Coercible for your Coerce, both of which, as @glaebhoerl have pointed out, are probably suboptimal.

I have a couple questions here.

  1. What is the motivation for having separate CoerceRef and CoerceRefMut traits instead of just Coerce impls for references (e.g. impl<'a> Coerce<&'a Target> for &'a Source)?

  2. As @glaebhoerl pointed out, I'm not sure that these traits should actually have methods. Since every implementation should be mem::transmute, Coerce and friends can just be marker traits, and there can be a free-standing function

    fn coerce<T, S: Coerce<T>>(val: S) {
        unsafe { mem::transmute(val) }
    }
    
  3. Unless the impls of these traits are very carefully generated by the compiler, these traits will result in wildly incoherent implementations. This actually isn't a problem for marker traits, and I've been thinking of writing up an RFC to relax the coherence requirements for empty traits, but it would result in ambiguities in generated code if the traits had methods. Whatever strategy you choose to take for this, I think the coherence issue should be mentioned.

  4. I'm not sure that it is safe for CoerceRefMut to be a safe trait. Taking the &mut str to &mut [u8] issue, leaving CoerceRefMut safe would allow an incorrect implementation. Coherence checks might prevent this at the moment, but it isn't totally clear and, as mentioned in the previous point, the coherence rules might have to be weakened for these traits.

This looks nice, but one lighterweight alternative would be a #[parent] attribute that you could apply to the fields of a struct. This would make access of parents more explicit and would allow automatic conversions to be easily chosen or rejected through a choice of a Deref implementation.

Continuing from the previous #[parent] suggestion, this would fall immediately out of using fields for data inheritance.

This feels very nice, though I'm not completely clear on what would happen if multiple parents implemented the trait.

I'd rather have DynClass actually implement Deref<Target=S> and have the structs Deref to their parents, keeping only one system of implicit conversions.

Why not just have

unsafe impl<T, S> Coerce<Box<DynClass<T, S>>> for Box<Class<T, S>> { }

?

Similarly, this could be done via Coerce.

I know this is just my strong opinion aligning with yours, but I view this as an advantage.

Extend explicity did not injecct data into traits. Rather, Extend corresponds almost exactly with your CoerceRef trait - it simply stated a claim that the data was there, allowing you to safely coerce references, doing an upcast. In fact, I find our proposals largely aligned with each other, though I think you definitely have a better, cleaner downcasting story than I did.

My general feeling is that I definitely like this proposal, probably in large part because of its similarity to the proposal I made. I think that focusing on having a set of features that do not step on each others' toes and work very well together is incredibly important, and I think you have done a good job at doing that.

@matthieum why do need to have both Class and DynClass and in addition to that TypeInfo, VTable and VRef? Could you not just have:

struct DynClass {
    pub data: *mut (),
    pub vtable: *mut (),
}

Due to differences between Rust and a lot of other OO languages maybe it would be better if something like DynStruct or StructObject (like TraitObject) was used instead of DynClass / Class.

Continuing on from the discussion on (), if T safely transmutes to T then it would not inherit from ().

Why are thin pointers so important? It seems to be a lot of work to get them.

Also the other features don't seem to depend on that (and they shouldn't; thin pointers may be useful somewhere, but I still think most code will prefer to stick with trait references), so perhaps the Class/DynClass stuff could be split off to separate RFC to further simplify the discussion.

I did not make the list of requirements, they came from Servo.

If I remember correctly the problem in Servo is that there is a lot of pointers and therefore going down from 16 bytes to 8 bytes provides a substantial memory gain.

As for splitting Class/DynClass into another RFC; well, those two classes are the RFC, everything before is just erecting the necessary scaffolding to get there, so I would be quite reluctant at the early stage. I believe it’s important, in this discussion, to keep the whole picture in mind; afterward, when the time comes to implement stuff and the roadmap is clear then working in smaller increments (or switching out some of the scaffolding for other bits) will be much easier.

And I suspect there will be many projects that will want inheritance. but will not get big gain from this. The 8 bytes go to the Class/DynClass, so if there are many objects, but few pointers to each, the gain won't be that big.

On the other hand, servo would surely get this benefit already without any of the inheritance stuff. Trait objects are there and they use them already and this is simply optimization for trait objects.

That's why I think this is two mostly independent things that should be proposed separately. One is inheritance, the other is optimizing trait references.

@matthieum

Just wanted to drop a quick note, first of all to thank you for your effort here, and also to let you know that @nikomatsakis and I are both slowly making our way through. It’s been a crazy week post-1.0, but I hope to leave some thoughts next week.

1 Like