In the MSVC ABI, how do I reliably find the vtable given only a (void*)?
Asked Answered
H

1

6

This question is specifically about non-portable MSVC ABI stuff.

I'm trying to write the equivalent of C++'s typeid in obviously-nonportable-yet-not-magic C++. For the Itanium ABI (as used on Linux/Mac), it's super easy:

const std::type_info& dynamicast_typeid(void *mdo)
{
    std::type_info **vptr = *reinterpret_cast<std::type_info ***>(mdo);
    std::type_info *typeinfo_ptr = vptr[-1];
    return *typeinfo_ptr;
}

So now I'm looking at the 64-bit MSVC ABI, and dang it, I'm stumped. For very simple classes, the ones that start with a vfptr at offset 0, it's almost as easy as Itanium:

const std::type_info& dynamicast_typeid(void *mdo)
{
    int *rtti_complete_object_locator = ((int ***)mdo)[0][-1];
    char *result = (char *) rtti_complete_object_locator;
    result -= rtti_complete_object_locator[5];
    result += rtti_complete_object_locator[3];
    return *(std::type_info*)result;
}

(This code is based on the Wine project's __RTtypeid.)

The problem is that some C++ classes don't start with a vfptr at offset 0! Sometimes they start with a vbptr.

struct Class1 { virtual ~Class1() {} };
struct Class2 : virtual Class1 {};

Class1 starts with a vfptr; Class2 starts with a vbptr.

When a class starts with a vbptr, I believe it's always the case that its first virtual base subobject (its first in layout order, and thus its "leafiest") will always have a vfptr at its offset 0. So if I know I'm dealing with a class that starts with a vbptr, I want to do this instead:

const std::type_info& dynamicast_typeid_for_vbptr_class(void *mdo)
{
    int first_vbase_offset = ((int**)mdo)[0][1];
    mdo = (char*)mdo + first_vbase_offset;
    int *rtti_complete_object_locator = ((int ***)mdo)[0][-1];
    char *result = (char *) rtti_complete_object_locator;
    result -= rtti_complete_object_locator[5];
    result += rtti_complete_object_locator[3];
    return *(std::type_info*)result;
}

I've determined that MSVC does actually generate the equivalent of

    if constexpr(IS_VBPTR_CLASS) {
        int first_vbase_offset = ((int**)mdo)[0][1];
        mdo = (char*)mdo + first_vbase_offset;
    }
    return __RTtypeid(mdo);

when compiling the C++ expression typeid(x) — where that IS_VBPTR_CLASS is pseudocode for "the compiler magically knows whether x has a vbptr, based on the static type of x and the fact that the compiler knows the layout of every type."

However, in my case I don't know the static type of x, and even if I did, I don't know how to find out (from within C++, with template metaprogramming) whether x starts with a vbptr or not.

Finally, I went ahead and fudged it a little bit with

const std::type_info& dynamicast_typeid(void *mdo)
{
    while (((int**)mdo)[0][0] == 0) {
        mdo = (char *)mdo + ((int**)mdo)[0][1];
    }
    int *rtti_complete_object_locator = ((int ***)mdo)[0][-1];
    char *result = (char *)complete_object_locator;
    result -= rtti_complete_object_locator[5];
    result += rtti_complete_object_locator[3];
    return *(std::type_info*)result;
}

only to discover that the typeinfo stored in the vftable for "Class1 in Class2" holds the typeinfo for the subobject type Class1, not for the most-derived-type Class2! So there's another piece of the puzzle missing.

So my question in a nutshell: Given (void*)&object_of_type_class2, how do I retrieve typeid(Class2)?

Helsell answered 18/6, 2017 at 18:34 Comment(4)
A void* doesn't carry any type information. You need at least a reinterpret_cast.Barnette
@πάνταῥεῖ: Notice that my code already contains lots of reinterpret_casts. I'm way ahead of you on that one. :) Also notice that the language-lawyer/portability version of this question is over here: #6572691 — but that's specifically NOT what I'm asking. I'm asking about details of the MSVC ABI.Helsell
The code would be easier to understand if you used C++-style casts.Tunny
There isn't a Visual C++ ABI. There are many of them, just like most C++ ABIs. The ABI can change from major version to major version, which means details of class layout can also change. The only thing you can depend on here is that the vtable pointer for classes that follow the same rules as COM interfaces will always be in the same COM compatible place. That applies to the layout of the vtable itself, you can't depend on type id information being located where you expect it, since COM doesn't use it.Garret
H
1

I've got something that works now!

The function dynamicast_to_mdo does the equivalent of dynamic_cast<void*>(p) — it adjusts p to point to its most derived object.

The function dynamicast_typeid does the equivalent of typeid(p) — it fetches the typeinfo from p's vtable. I'm just using the fudge/hack that I gave in my question, and I'm actually not sure why I was getting wrong answers a few hours ago; I think when I was seeing the wrong typeinfo, it may have been because I was accidentally trying to take the typeid of a dangling reference to a destroyed stack variable.

// 64-bit MSVC ABI
void *dynamicast_to_mdo(void *p)
{
    if (((int**)p)[0][0] == 0) {
        p = (char *)p + ((int**)p)[0][1];
    }
    int *complete_object_locator = ((int ***)p)[0][-1];
    int mdoffset = complete_object_locator[1];
    void *adjusted_this = static_cast<char *>(p) - mdoffset;
    return adjusted_this;
}

// 64-bit MSVC ABI
const std::type_info& dynamicast_typeid(void *p)
{
    if (((int**)p)[0][0] == 0) {
        p = (char *)p + ((int**)p)[0][1];
    }
    int *complete_object_locator = ((int ***)p)[0][-1];

    char *result = (char *)complete_object_locator;
    result -= complete_object_locator[5];
    result += complete_object_locator[3];
    return *(const std::type_info*)result;
}
Helsell answered 18/6, 2017 at 22:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.