What is a Trait - not just "like an interface", but truly at the machine code level?


#1

In the 2 weeks I’ve been learning about Rust, Traits started off to me as being “interfaces” that certain structs implement.

But now being exposed to some type signatures that use Traits as a way to specify “all types that satisfy this Trait”, my understanding has become less (or more?) clear.

I think there are a few people thinking the same. Traits seem to be multipurpose.

And then it comes to compilation. When I think about it…Traits seem to only exist at the type/compiler level, and are not actually a thing when compiled. But this is just a thought, nothing concrete.

Am I on the right path here with my line of thinking?

Or is there more to it? Are Traits tangible code in assembly?

Usually I would ask this type of question in ##rust-beginners but I think this is something to be addressed on a more public and persistent medium.


#2

Traits are a type system concept. In terms of machine code footprint, trait objects have a vtbl pointer in them (and there’s a vtbl itself) which means dynamic dispatch will be done via that indirection - that indirection and runtime layout (i.e. fat pointer) will be seen at the assembly level.


#3

OK so we have the type-level “Trait” and a real thing “Trait object”.

How do I know when I’m using one or the other? How do I create a Trait object normally? (At this point I more or less know the answers to these, but would like something concise for myself and others finding this thread)


#4

You have a trait object whenever you use a trait not as a part of a generic type, for example &Trait or Box<Trait>.


#5

Where you use trait name directly, it’s dynamic Box<Trait>. Where you use trait name via generic name, it’s static (Box<T> where T: Trait).


#6

It is possible that some day, in the far off future, there will be no question about when you’re using a trait object. Just this funny new keyword that Google will be glad to tell you all about


#7

Statically dispatched calls to trait methods, such as in this example:

trait Foo {
    fn foo(&self);
}

fn staticDispatch<F: Foo>(f: F) {
    f.foo() // here
}

are compiled down to a copy of staticDispatch for each specialization of F found in the program. Each copy of staticDispatch directly calls the correct implementation of foo based on the actual type bound to F for that specialization.

Dynamically dispatched calls - those through pointers to trait objects of various forms, such as this:

trait Foo {
    fn foo(&self);
}

fn viaBorrow(f: &Foo) {
    f.foo() // here
}

fn viaBox(f: Box<Foo>) {
    f.foo() // here
}

use a vtable-based dispatch system, much like virtual functions in most object-oriented languages. There’s a section in the Rust book describing how these vtables work, but they’re much like any other indirected function call.