как реализовать переход вызова к соответствующей записи Vtable - PullRequest
0 голосов
/ 07 ноября 2011

Я работаю над заданием по разработке компилятора. В части генерации кода я застрял с тем, как создавать инструкции, которые будут гарантировать, что соответствующий метод вызывается во время выполнения. Язык - это очень небольшое подмножество C ++.

скажем:

void main()
{
  Animal* a;
  a = new Cow;
  //what code should be generated to ensure that object 'a' calls Cow::Init here
  a->Init(5);
}

class Cow : public Animal{
 void Init(int h)
 {
   height = h;
 }
}

class Animal {
 int height; 
 virtual void Init(int h){
   height = h;
  }
}

Ответы [ 3 ]

2 голосов
/ 07 ноября 2011

Это может быть выражено в облегченном C ++, если вы находите его более читабельным, чем ассемблер (я так понимаю). Я ограничусь C (в основном) и просто добавлю наследование, чтобы избежать большого количества приведения.

Для ясности детали реализации будут иметь префикс __. Обратите внимание, что в целом эти идентификаторы зарезервированы для реализации, поэтому обычно вы не должны использовать их в своих программах.


Тип безопасной виртуальной диспетчеризации .

Примечание: ограничено простым наследованием (одно базовое, виртуальное наследование отсутствует)

Давайте создадим класс Animal.

struct __AnimalTableT;

struct Animal { __AnimalTableT const * const __vptr; int height; }

void AnimalInit(Animal* a, int height) {
  a->height = height;
}

Мы резервируем место для указателя на виртуальную таблицу в Animal и выражаем метод как внешнюю функцию, чтобы сделать this явным.

Далее мы «создаем» виртуальную таблицу. Обратите внимание, что массив в C должен состоять из аналогичных элементов, поэтому здесь мы будем использовать подход более высокого уровня.

struct __AnimalTableT {
  typedef void (*InitFunction)(int);

  InitFunction Init;
};
static __AnimalTableT const __AnimalTable = { &AnimalInit };

Теперь давайте создадим корову:

struct Cow: Animal {};

void CowInit(Animal* a, int height) {
  Cow* c = static_cast<Cow*>(a);
  c->height = height;
}

И связанная таблица:

// Note: we could have new functions here (that only Cow has)
// they would be appended after the "Animal" part
struct __CowTableT: __AnimalTableT {};

static __CowTableT const __CowTable = { &CowInit };

И использование:

typedef void (*__AnimalInitT)(Animal*,int);

int main() {
  Cow cow = { &__CowTable, 0 };

  __AnimalInitT const __ai = cow.__vptr->Init;
  (*__ai)(&cow, 5);
}

А настоящий?

Реальное использование немного сложнее, но основывается на той же идее.

Как вы можете заметить, странно, что CowInit принимает указатель Animal* в качестве первого аргумента. Проблема в том, что вам нужен совместимый тип указателя на функцию с изначально перегруженным методом. В случае линейного наследования это не имело бы большого значения, но в случае множественного наследования или виртуального наследования все становится довольно беспокойным, и подраздел Animal в Cow может не планироваться в самом начале, что приводит к настройка указателя.

В реальной жизни у нас есть множество:

Ну, мы можем изменить подпись CowInit на более естественную:

void CowInit(Cow* cow, int height);

И затем, мы "ликвидируем" разрыв, создавая "толчок", чтобы сделать адаптацию:

void __CowInit(Animal* a, int height) {
  CowInit(static_cast<Cow*>(a), height);
}

static __CowTableT const __CowTable = { &__CowInit };

В реальной жизни у нас есть таблицы:

Еще одно замечание: использование структуры очень хорошо, но мы говорим здесь о деталях реализации, поэтому излишняя тщательность не требуется. В общем, поэтому компиляторы используют простой массив, а не структуру:

typedef (void)(*__GenericFunction)();

static __GenericFunction const __AnimalTable[] = {
  __GenericFunction(&AnimalInit)
};

static __GenericFunction const __CowTable[] = {
  __GenericFunction(&__CowInit)
};

Это немного меняет вызов: вы используете индекс вместо имени атрибута, и вам нужно привести обратно к соответствующему типу функции.

typedef void (*__AnimalInitT)(Animal*,int);

int main() {
  Cow cow = { &__CowTable, 0 };

  // old line: __AnimalInitT const __ai = cow.__vptr->Init;
  __AnimalInit const __ai = __AnimalInit(cow.__vptr[0]);
  (*__ai)(&cow, 5);
}

Как видите, использование таблиц - это действительно детали реализации.

Действительно важным моментом здесь является введение thunk для адаптации сигнатуры функции. Обратите внимание, что thunk введен при создании таблицы класса Derived (Cow здесь). В нашем случае это не нужно, поскольку на низком уровне оба объекта имеют один и тот же адрес, поэтому мы могли бы обойтись без него, и умный компилятор не сгенерирует его и непосредственно примет &CowInit.

2 голосов
/ 07 ноября 2011

очень простой способ сделать это (примечание: это исключает оптимизацию для известных вызовов во время компиляции): если в вашем классе есть какие-либо виртуальные члены (включая унаследованные), то его самый первый член становится указателем на vftable. vftable является константой для определения класса , поэтому вам нужен только указатель.

оттуда каждой уникальной функции присваивается индекс в этой vftable, поэтому каждому уникальному имени (примечание: под именем я подразумеваю имя символа, включая типы, но без квалификации пространства имен класса) есть уникальный индекс, тогда таблица заполнено от класса в самом верху дерева наследования до текущего определения рабочего класса.

При этом более новые переопределения виртуальной функции будут перезаписывать более старые записи, которые совместно используют их индекс. вызов функций становится тривиальным, так как вы просто генерируете вызов индекса для индекса имени этой функции.

Итак, в вашем примере Animal имеет vftable с 1 записью, Init(int), которой присваивается уникальный индекс 0. Таким образом, у вас есть vftable, который выглядит так:

;Animal - vftable
&Animal::Init //note: this isn't a class member pointer in the C++ sense, its a namespaced function pointer if you will

затем, когда вы создаете vftable для Cow, вы используете Animals в качестве базы и добавляете виртуальные функции, в данном случае Init(int), но он уже имеет уникальный индекс 0, поэтому мы перезаписываем функция с индексом 0:

;Cow - vftable
&Cow::Init

тогда, если у нас есть звонок:

a->Init(5);

мы просто преобразуем это в:

a->vftable[0](5);

, где 0 - уникальный индекс, присвоенный Init(int).

пример сборки на случай, если это поможет:

;ecx contains our class pointer
mov eax,[ecx] ;get the vftable ptr
mov eax,[eax] ; get the ptr at (vftable + (unique_index * sizeof(func_ptr)))
push 5 ;push our arg 5, ecx is already setup for __thiscall
call eax ; let it rip!

примечание: все это предполагает, что ваша таблица символов настроена так, чтобы иметь возможность обнаруживать виртуальные функции, передаваемые через наследование, или те, которые становятся виртуальными из наследования.


Если это место для оптимизации, вы можете проанализировать a и обнаружить, что ему присваивается значение только один раз, поэтому вы можете преобразовать его класс в класс значения, которому оно было присвоено, Cow. затем, увидев, что у вас есть класс на конце цепочки деривации, вы можете свернуть вызов vftable и использовать вызов непосредственно к Cow::Init, как это намного сложнее, и есть много способы оптимизации вызовов vftable, для проекта это не должно иметь значения.

0 голосов
/ 07 ноября 2011

Концепция часто реализуется с помощью thunk , который является сгенерированной компилятором функцией оболочки.

...