2013-12-19 5 views
4

Примечание. Этот вопрос касается только asm.js не о C++ и других языках программирования.asm.js - Как должны быть реализованы указатели функций?

Как видно из названия уже говорит:

Как следует указатель на функцию будет реализован в эффективном способе?

Я не мог найти что-либо в Интернете, поэтому я решил расспросить его здесь.

Редактировать: Я хотел бы реализовать виртуальные функции в компиляторе, над которым я работаю.

В C++ я хотел бы сделать что-то вроде этого, чтобы сгенерировать vtable:

#include <iostream> 

class Base { 
    public: 
    virtual void doSomething() = 0; 
}; 

class Derived : public Base { 
    public: 
    void doSomething() { 
     std::cout << "I'm doing something..." << std::endl; 
    } 
}; 

int main() 
{ 
    Base* instance = new Derived(); 
    instance->doSomething(); 
    return 0; 
} 

Чтобы быть более точным; как я могу создать vtable в asm.js без необходимости простого JavaScript? В любом случае, я хотел бы использовать «близкие родные» возможности asm.js при использовании указателей функций.

Решение может быть использовано для генерируемого компьютером кода только.

+0

Вы имеете в виду, как это реализует asmjs? –

+0

Нет, я на самом деле имел в виду, как это должно быть реализовано ** в ** asmjs. – Tim

+0

Вы должны написать свою программу на C/C++, чтобы получить asm.js. –

ответ

2

Рассматривая, как работает asm.js, я считаю, что лучше всего использовать метод, используемый исходным компилятором CFront: скомпилируйте виртуальные методы до функций, которые берут этот указатель, и используйте thunks для исправления этого указателя перед тем как передать его. Я буду пройти через это шаг за шагом:

Нет Наследование

Сокращение методов к специальным функциям:

void ExampleObject::foo(void); 

не будет преобразован в

void exampleobject_foo(ExampleObject* this); 

Это прекрасно работает для объекты, не основанные на наследовании.

Single Inheritance

Мы можем легко добавлять поддержку для сколь угодно большого количества одиночного наследования через простой трюк: всегда хранить объект в базе памяти первого:

class A : public B 

стало бы, в памяти :

[[ B ] A ] 

Подходите ближе!

Множественное наследование Теперь, множественное наследование делает это гораздо труднее работать с

class A : public B, public C 

Это невозможно для обоих B и C, чтобы быть в начале A; они просто не могут сосуществовать. Существует два варианта:

  1. Храните явное смещение (известное как delta) для каждого вызова на базу.
  2. Не разрешать звонки через к B или C

Второй вариант намного предпочтительнее по ряду причин; если вы вызываете функцию члена базового класса, редко вы хотите сделать это через производный класс. Вместо этого вы можете просто перейти в C :: conlyfunc, который затем может выполнить настройку на ваш указатель для вас бесплатно. Разрешение A :: conlyfunc удаляет важную информацию, которую мог бы использовать компилятор, при очень небольшой пользе.

Первый выбор используется в C++; все объекты множественного наследования вызовут thunk перед каждым вызовом базовому классу, который настраивает этот указатель так, чтобы он указывал на подобъект внутри него. В простом примере:

class ExampleBaseClass 
{ 
    void foo(void); 
} 

class ExampleDerivedClass : public ExampleBaseClass, private IrrelevantBaseClass 
{ 
    void bar(void); 
} 

тогда станет

void examplebaseclass_foo(ExampleBaseClass* this); 
void examplederivedclass_bar(ExampleDerivedClass* this); 

void examplederivedclass_thunk_foo(ExampleDerivedClass* this) 
{ 
    examplebaseclass_foo(this + delta); 
} 

Это может быть встраиваемыми во многих ситуациях, так что это не слишком большая накладных расходов. Однако, если вы никогда не сможете ссылаться на ExampleBaseClass :: foo как ExampleDerivedClass :: foo, эти thunks не нужны, так как дельта будет легко распознаваться из самого вызова.

Виртуальные функции

Виртуальные функции добавляет целый новый уровень сложности. В примере с множественным наследованием у thunks были фиксированные адреса для вызова; мы просто корректировали это, прежде чем передавать его в уже известную функцию. С виртуальными функциями функция, которую мы вызываем, неизвестна; мы могли бы быть переопределены производным объектом, о котором у нас нет возможности знать во время компиляции, из-за того, что он находится в другой единицы перевода или в библиотеке и т. д.

Это означает, что нам нужна какая-то форма динамической отправки для каждого объекта, имеет практически передовую функцию; многие методы возможны, но реализации C++ имеют тенденцию использовать простой массив указателей на функции или таблицу vtable.Для каждого объекта, который имеет виртуальные функции, мы добавим точку в массив как скрытый член, как правило, на фронте:

class A 
{ 
hidden: 
    void* vtable; 
public: 
    virtual void foo(void); 
} 

Добавит стук функцию, которые перенаправляют на виртуальные таблицы

void a_foo(A* this) 
{ 
    int vindex = 0; 
    this->vtable[vindex](this); 
} 

виртуальных таблицы затем заполняется указателями на функции, которые мы на самом деле хотим назвать:

vtable [0] = & A :: foo_default; // наша базовая эксплоатация foo

В производном классе, если мы хотим переопределить эту виртуальную функцию, все, что нам нужно сделать, это изменить vtable в нашем собственном объекте, указать на новую функцию и переопределить ее в базовом классе, а также:

class B: public A 
{ 
    virtual void foo(void); 
} 

будет делать это в конструкторе:

((A*)this)->vtable[0] = &B::foo; 

Наконец, у нас есть поддержка всех форм наследования! Почти.

Virtual Наследования Существует один окончательный нюанс с этой реализацией: если вы по-прежнему позволяют Derived :: Foo будет использоваться, когда то, что вы на самом деле имеете в виду, Base :: Foo, вы получите проблемы с бриллиантом:

class A : public B, public C; 
class B : public D; 
class C : public D; 

A::DFunc(); // Which D? 

Эта проблема также может возникать, когда вы используете базовые классы как классы с сохранением состояния или когда вы помещаете функцию, которая должна быть has-a, а не is-a; как правило, это признак необходимости в реструктуризации. Но не всегда.

В C++ это имеет решение, которое не очень элегантно, но работает:

class A : public B, public C; 
class B : virtual D; 
class C : virtual D; 

Это требует тех, кто реализует такие классы и иерархий думать заранее и преднамеренно делают их классы немного медленнее, чтобы поддержать возможное будущее использование. Но это решает проблему.

Как мы можем реализовать это решение?

[ [ D ] [ B ] [ Dptr ] [ C ] [ Dptr ] A ] 

Вместо того, чтобы использовать базовый класс непосредственно, как и в обычном наследовании, с виртуальным наследованием мы помещаем все вхождения D через указатель, добавление окольного, в то время как топают многочисленные экземпляры в один. Обратите внимание, что оба B и C имеют свой собственный указатель, что оба указывают на тот же D; это потому, что B и C не знают, являются ли они свободными плавающими копиями или связаны в производных объектах. Те же вызовы должны использоваться для обоих, или виртуальные функции не будут работать должным образом.

Резюме

  1. Transform метод вызывает в вызовах функций с помощью специального этого параметра в базовых классах
  2. объекты структуры в памяти, так одиночном наследовании ничем не отличаются ни от наследования
  3. Добавить санки отрегулировать эти указатели затем вызывают базовые классы для множественного наследования
  4. Добавьте vtables в классы с виртуальными методами и сделайте все вызовы методов пройденными через vtable в метод (thunk -> vt состоянии -> метод)
  5. Сделка с виртуальным наследованием через BaseObject указатель на, а не выведем объект вызывает

Все это просто в js.asm.

+0

Спасибо за ваш ответ. Хотя это было не совсем то, что я искал, это будет большой помощью для меня. В сочетании с комментарием Joe Z это решение, в котором я нуждался! – Tim

1

Тим,

Я не буду ни в коем случае эксперт asm.js но ваш вопрос меня интригует. Это относится к hart объектно-ориентированного дизайна языка. Также кажется ироничным, что вы воссоздаете проблемы на уровне машины в домене javascript.

Решение вопроса, которое мне кажется, заключается в том, что вам нужно будет настроить учет определенных типов и функций. В Java это обычно делается путем декорирования байт-кода с идентификаторами, которые представляют правильное сопоставление функций класса и функции для любого заданного объекта. Если вы используете идентификатор Int32 для каждого определенного класса и дополнительный идентификатор Int32 для каждой определенной функции, вы можете сохранить их в представлениях объектов в куче. Ваша vtable - это не более, чем сопоставление этих комбинаций с конкретными функциями.

Надеюсь, это вам поможет.

1

Я не очень хорошо знаком с точным синтаксисом asm.js, но вот как я реализовал в виртуальные таблицы транслятора шахты (для x86):

Каждый объект получены из структуры, как это:

struct Object { 
    VTable *vtable; 
}; 

Тогда другие типы, которые я использую будет выглядеть примерно так в C++ - синтаксис:

struct MyInt : Vtable { 
    int value; 
}; 

который (в данном случае), что эквивалентно:

struct MyInt { 
    VTable *vtable; 
    int value; 
}; 

Таким образом, окончательная компоновка объектов заключается в том, что при смещении 0 я знаю, что у меня есть указатель на таблицу vtable.Виртуальные таблицы я использую это просто массив указателей на функции, снова в C-синтаксис типа виртуальные таблицы могут быть определены следующим образом:

typedef Function *VTable; 

Где в C я хотел бы использовать недействительным * вместо функции *, так как фактические типы функции будет отличаться. Осталось только компилятору:

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

2: Когда объект создан, установите элемент vtable объекта (со смещением 0), чтобы указать на глобальную таблицу vtable.

Затем, когда вы хотите вызвать виртуальные функции, которые вы можете сделать что-то вроде этого:

(*myObject->vtable[1])(1); 

для вызова функции компилятор назначил идентификатор 1 в таблицу виртуальных (methodB в примере ниже).

Последний пример: Допустим, мы имеем следующие два класса:

class A { 
public: 
    virtual int methodA(int) { ... } 
    virtual int methodB(int) { ... } 
    virtual int methodC(int) { ... } 
}; 

class B : public A { 
public: 
    virtual int methodA(int) { ... } 
    virtual int methodB(int) { ... } 
}; 

виртуальные таблицы для класса А и В может выглядеть следующим образом:

A:     B: 
0: &A::methodA  0: &B::methodA 
1: &A::methodB  1: &B::methodB 
2: &A::methodC  2: &A::methodC 

Используя эту логику, мы знаем, что, когда мы вызываем метод B на любом типе, производном от A, мы будем называть любую функцию, расположенную в индексе 1 в таблице vtable этого объекта.

Конечно, это решение не работает сразу, если вы хотите разрешить множественное наследование, но я уверен, что он может быть расширен для этого. После некоторой отладки с Visual Studio 2008 кажется, что это более или менее то, как там реализуются vtables (конечно, там он расширен для обработки множественного наследования, я еще не пытался это понять).

Надеюсь, вы получите идеи, которые могут быть применены как минимум в asm.js. Как я уже сказал, я точно не знаю, как работает asm.js, но мне удалось реализовать эту систему в сборке x86, и я не вижу никаких проблем с ее внедрением в JavaScript, поэтому надеюсь, что она будет использоваться в asm. js.