asm.js — Как должны быть реализованы функциональные указатели

Примечание: этот вопрос чисто о 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 при использовании указателей на функции.

Решение может быть подходящим для компьютерного кода только.

4

Решение

Рассматривая, как работает 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; они просто не могут сосуществовать. Есть два варианта:

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

Второй выбор гораздо предпочтительнее по ряду причин; если вы вызываете функцию-член базового класса, редко вы захотите сделать это через производный класс. Вместо этого вы можете просто перейти на 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 не знают, являются ли они свободно плавающими копиями или связаны с производными объектами. Одни и те же вызовы должны использоваться для обоих, иначе виртуальные функции не будут работать должным образом.

Резюме

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

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

2

Другие решения

Тим,

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

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

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

1

Я не очень знаком с точным синтаксисом 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 также.

1
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector