Virtual Methods

A virtual method is no more than a compiler-controlled function pointer. Each virtual method is recorded in the vtable, which is a structure of all the function pointers needed by a given class:

class Foo
{
public:
    virtual int GetLengthTimesTwo() const
    {
        return _length * 2;
    }

    void SetLength(size_t value)
    {
        _length = value;
    }

private:
    int _length;
};

int main()
{
    Foo foo;
    foo.SetLength(4);
    return foo.GetLengthTimesTwo();
}

This becomes:

%Foo_vtable_type = type { i32(%Foo*)* }

%Foo = type { %Foo_vtable_type*, i32 }

define i32 @Foo_GetLengthTimesTwo(%Foo* %this) nounwind {
    %1 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
    %2 = load i32, i32* %1
    %3 = mul i32 %2, 2
    ret i32 %3
}

@Foo_vtable_data = global %Foo_vtable_type {
    i32(%Foo*)* @Foo_GetLengthTimesTwo
}

define void @Foo_Create_Default(%Foo* %this) nounwind {
    %1 = getelementptr %Foo, %Foo* %this, i32 0, i32 0
    store %Foo_vtable_type* @Foo_vtable_data, %Foo_vtable_type** %1
    %2 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
    store i32 0, i32* %2
    ret void
}

define void @Foo_SetLength(%Foo* %this, i32 %value) nounwind {
    %1 = getelementptr %Foo, %Foo* %this, i32 0, i32 1
    store i32 %value, i32* %1
    ret void
}

define i32 @main(i32 %argc, i8** %argv) nounwind {
    %foo = alloca %Foo
    call void @Foo_Create_Default(%Foo* %foo)
    call void @Foo_SetLength(%Foo* %foo, i32 4)
    %1 = getelementptr %Foo, %Foo* %foo, i32 0, i32 0
    %2 = load %Foo_vtable_type*, %Foo_vtable_type** %1
    %3 = getelementptr %Foo_vtable_type, %Foo_vtable_type* %2, i32 0, i32 0
    %4 = load i32(%Foo*)*, i32(%Foo*)** %3
    %5 = call i32 %4(%Foo* %foo)
    ret i32 %5
}

Please notice that some C++ compilers store _vtable at a negative offset into the structure so that things like memset(this, 0, sizeof(*this)) work, even though such commands should always be avoided in an OOP context.

Rust Traits and VTables

Rust does have quite a different object model when compared to C++. However, when it comes to the low-level details of dynamic dispatch, they are remarkably similar. We’ll explore an example from the rust documentation and what kind of llvm IR is emitted by the rustc compiler. Both rust and C++ utilizes virtual method tables for dynamic dispatch. However, in rust there is no such thing as virtual methods in the high-level language. Instead we can implement traits for our data types and then implement an interface that accepts all data types that implement this trait and dynamically dispatch to the right trait implementation (i.e., this is the dyn Trait syntax in the example below). The full example is given here for easy reference:

Here the compiler must dynamically decide at runtime, which function to execute. The compiler only knows that the object stored in the components vector does satisfy the trait Draw. As a side note for those not so familiar with rust: wrapping the object within a Box essentially puts the object on the heap (somewhat akin to a unique_ptr in C++) and effectively allows us to put trait objects (i.e., dyn Drawable in this example) in a vector.

; test::Screen::run
; Function Attrs: nonlazybind uwtable
define void @"Screen::run"(%Screen* %self) {
start:

;; (omitting the initial prologue and setup code)
;; this is the start of the for loop in Screen::run calling the next method
;; on the iterator for the first time and checking whether it is None (or
;; null in llvm here)
;; %5 contains the pointer to the first component in the vector here
  %6 = icmp eq i64* %5, null
  br i1 %6, label %end, label %forloop

end:                                              ; preds = %forloop, %start
  ret void

forloop:                                          ; preds = %start, %forloop
  %7 = phi i64* [ %next_component, %forloop ], [ %5, %start ]
;; here the boxed pointer is retrieved and dereferenced to retrieve the
;; vtable pointer
  %8 = bitcast i64* %7 to {}**
  %self_ptr = load {}*, {}** %8
  %9 = getelementptr inbounds i64, i64* %7, i64 1
  %vtable_ptr = bitcast i64* %9 to void ({}*)***
  %vtable = load void ({}*)**, void ({}*)*** %vtable_ptr
;; 3 is the index into the vtable struct, which refers to the draw implementation for this particular struct
  %trait_method_ptr = getelementptr inbounds void ({}*)*, void ({}*)** %vtable, i64 3
  %trait_method = load void ({}*)*, void ({}*)** %vmethod
;; indirect call to trait method
  call void %trait_method({}* %self_ptr)

;; retrieve the next object
  %next_component = call i64* @"<core::slice::iter::Iter<T> as core::iter::traits::iterator::Iterator>::next"({ i64*, i64* }* %iter)
  %14 = icmp eq i64* %next_component, null
  br i1 %14, label %end, label %forloop
}

Within the global variables in the llvm module we can see the vtable as shown here. Both the Button and the SelectBox have associated vtables.

@vtable.screen = private unnamed_addr constant
  ;; the Type of the constant vtable structure
  { void (%SelectBox*)*, i64, i64, void (%SelectBox*)* }
  {
    ;; first entry is the function to drop the object
    void (%SelectBox*)* @"core::ptr::drop_in_place<test::SelectBox>",  ;; destructor
    i64 32, ;; size
    i64 8,  ;; alignment
    ;; last in the vtable is the pointer to the SelectBox::draw implementation
    void (%SelectBox*)* @"<test::SelectBox as test::Draw>::draw"
  }

;; the vtable for Button is structured basically the same
@vtable.button = private unnamed_addr constant
    { void (%Button*)*, i64, i64, void (%Button*)* }
    {
        void (%Button*)* @"core::ptr::drop_in_place<test::Button>",
        i64 32, i64 8,
        void (%Button*)* @"<test::Button as test::Draw>::draw"
    }

The older version of the rust book also features an excellent an concise description of how vtables in rust work. It seems that newer version follow the same pattern internally, although this has been removed from the official rust book.

Finally, here is a blogpost that explains vtables and dynamic dispatch and their differences in rust vs C++ in some more detail.