Примечание: этот вопрос чисто о asm.js
ни о C ++, ни о каком-либо другом языке программирования.
Как уже сказано в заголовке:
Как эффективно реализовать указатель на функцию?
Я не мог найти что-либо в Интернете, поэтому я решил спросить здесь.
Редактировать:
Я хотел бы реализовать виртуальные функции в компиляторе, над которым я работаю.
В 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 при использовании указателей на функции.
Решение может быть подходящим для компьютерного кода только.
Рассматривая, как работает asm.js, я считаю, что лучше всего было бы использовать метод, использованный оригинальным компилятором CFront: компилировать виртуальные методы до функций, которые принимают указатель this, и использовать thunks для исправления указателя this перед его передачей. Я пойду через это шаг за шагом:
Нет наследования
Свести методы к специальным функциям:
void ExampleObject::foo( void );
будет преобразован в
void exampleobject_foo( ExampleObject* this );
Это отлично работает для объектов, не связанных с наследованием.
Одиночное наследование
Мы можем легко добавить поддержку произвольного большого количества одиночного наследования с помощью простого трюка: всегда сначала сохраняйте объект в базе памяти:
class A : public B
станет в памяти
[[ B ] A ]
Все ближе!
Множественное наследование
Теперь множественное наследование значительно усложняет работу
class A : public B, public C
Невозможно, чтобы и B, и C были в начале A; они просто не могут сосуществовать. Есть два варианта:
Второй выбор гораздо предпочтительнее по ряду причин; если вы вызываете функцию-член базового класса, редко вы захотите сделать это через производный класс. Вместо этого вы можете просто перейти на C :: conlyfunc, который затем может бесплатно выполнить настройку вашего указателя. Разрешение A :: conlyfunc удаляет важную информацию, которую мог бы использовать компилятор, с очень небольшим преимуществом.
Первый выбор используется в C ++; все множественные объекты наследования вызывают thunk перед каждым вызовом базового класса, который корректирует указатель this для указания на подобъект внутри него. В простом примере:
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 были фиксированные адреса для вызова; мы просто настраивали это перед передачей его уже известной функции. С виртуальными функциями функция, которую мы вызываем, неизвестна; мы можем быть переопределены производным объектом, о котором мы не можем знать во время компиляции, потому что он находится в другом модуле перевода или библиотеке и т. д.
Это означает, что нам нужна некоторая форма динамической диспетчеризации для каждого объекта, который имеет практически переопределенную функцию; возможно много методов, но реализации C ++ имеют тенденцию использовать простой массив указателей на функции или vtable. К каждому объекту, имеющему виртуальные функции, мы добавляем точку в массив как скрытый элемент, обычно спереди:
class A
{
hidden:
void* vtable;
public:
virtual void foo( void );
}
Добавляем thunk-функции, которые перенаправляют в vtable
void a_foo( A* this )
{
int vindex = 0;
this->vtable[vindex](this);
}
Затем vtable заполняется указателями на функции, которые мы на самом деле хотим вызвать:
vtable [0] = &A :: foo_default; // наш базовый класс реализации foo
В производном классе, если мы хотим переопределить эту виртуальную функцию, все, что нам нужно сделать, это изменить виртуальную таблицу в нашем собственном объекте, чтобы она указала на новую функцию, и она также будет переопределена в базовом классе:
class B: public A
{
virtual void foo( void );
}
Затем сделаем это в конструкторе:
((A*)this)->vtable[0] = &B::foo;
Наконец, у нас есть поддержка всех форм наследования!
Почти.
Виртуальное Наследование
В этой реализации есть одно заключительное предостережение: если вы продолжаете разрешать использование Derived :: foo, когда вы действительно имеете в виду Base :: foo, вы получаете проблему с алмазом:
class A : public B, public C;
class B : public D;
class C : public D;
A::DFunc(); // Which D?
Эта проблема также может возникать, когда вы используете базовые классы в качестве классов с сохранением состояния или когда вы помещаете функцию, которая должна иметь значение «a-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 не знают, являются ли они свободно плавающими копиями или связаны с производными объектами. Одни и те же вызовы должны использоваться для обоих, иначе виртуальные функции не будут работать должным образом.
Резюме
Все это просто в js.asm.
Тим,
Я ни в коем случае не эксперт asm.js, но ваш вопрос меня заинтриговал. Это идет к сердцу объектно-ориентированного языкового дизайна. Также кажется ироничным, что вы воссоздаете проблемы машинного уровня в домене javascript.
Мне кажется, что решение вашего вопроса заключается в том, что вам необходимо настроить учет определенных типов и функций. В Java это обычно делается путем украшения байт-кода идентификаторами, которые представляют правильное отображение класса-> функции любого данного объекта. Если вы используете идентификатор Int32 для каждого определенного класса и дополнительный идентификатор Int32 для каждой определенной функции, вы можете сохранить их в представлениях объектов в куче. В этом случае ваша таблица не более чем отображение этой комбинации на определенные функции.
Я надеюсь, это поможет вам.
Я не очень знаком с точным синтаксисом asm.js, но вот как я реализовал vtable в моем компиляторе (для x86):
Каждый объект получен из структуры, подобной этой:
struct Object {
VTable *vtable;
};
Тогда другие типы, которые я использую, будут выглядеть примерно так в c ++ — синтаксис:
struct MyInt : Vtable {
int value;
};
что (в данном случае) эквивалентно:
struct MyInt {
VTable *vtable;
int value;
};
Итак, окончательная компоновка объектов такова, что по смещению 0 я знаю, что у меня есть указатель на vtable. Используемая мной vtable — это просто массив указателей на функции, опять же в C-синтаксисе тип VTable может быть определен следующим образом:
typedef Function *VTable;
Где в C я бы использовал void * вместо Function *, поскольку фактические типы функций будут отличаться. Что остается сделать компилятору:
1: Для каждого типа, содержащего виртуальные функции, создайте глобальную виртуальную таблицу и заполните ее указателями на переопределенные функции.
2: Когда объект создан, установите элемент vtable объекта (со смещением 0), чтобы он указывал на глобальную vtable.
Затем, когда вы хотите вызвать виртуальные функции, вы можете сделать что-то вроде этого:
(*myObject->vtable[1])(1);
чтобы вызвать функцию, которую ваш компилятор назначил ID 1 в vtable (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) { ... }
};
VTable для классов A и B может выглядеть так:
A: B:
0: &A::methodA 0: &B::methodA
1: &A::methodB 1: &B::methodB
2: &A::methodC 2: &A::methodC
Используя эту логику, мы знаем, что когда мы вызываем methodB для любого типа, производного от A, мы будем вызывать любую функцию, расположенную в индексе 1 в vtable этого объекта.
Конечно, это решение не работает сразу, если вы хотите разрешить множественное наследование, но я уверен, что оно может быть расширено для этого. После некоторой отладки в Visual Studio 2008 кажется, что это более или менее то, как vtables реализованы там (конечно, там он расширен для обработки множественного наследования, я еще не пытался выяснить это).
Я надеюсь, что вы получите некоторые идеи, которые могут быть применены в asm.js по крайней мере. Как я уже сказал, я не знаю точно, как работает asm.js, но мне удалось внедрить эту систему в сборке x86, и я не вижу никаких проблем с ее реализацией в JavaScript, поэтому я надеюсь, что она может быть использована в asm. JS также.