asm.js - 应该如何实现函数指针

时间:2013-12-19 12:25:13

标签: c++ vtable 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;
}

更准确;如何在不需要纯JavaScript的情况下在asm.js中生成vtable? 无论如何,在使用函数指针时,我想要asm.js的“近原生”功能。

该解决方案可能适用于计算机生成的代码

3 个答案:

答案 0 :(得分:2)

了解asm.js是如何工作的,我相信你最好的选择是使用原始CFront编译器使用的方法:将虚拟方法编译为带有this指针的函数,然后使用thunks更正此指针通过它。我会一步一步地完成它:

无遗传

将方法简化为特殊功能:

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. 为每次对base的调用存储显式偏移量(称为delta)。
  2. 不允许通过A到B或C
  3. 进行通话

    出于各种原因,第二种选择更为可取;如果您正在调用基类成员函数,那么您很少想通过派生类来完成它。相反,您可以简单地使用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,因为delta很容易从调用本身中识别出来。

    虚拟功能

    虚拟功能增加了一层全新的复杂性。在多继承示例中,thunk具有要调用的固定地址;我们只是在将它传递给已知函数之前调整它。使用虚拟功能,我们调用的功能是未知的;我们可以被派生对象覆盖,我们在编译时无法知道它,因为它在另一个翻译单元或库等中。

    这意味着我们需要某种形式的动态调度,每个对象具有几乎可覆盖的功能;许多方法都是可能的,但C ++实现倾向于使用简单的函数指针数组或vtable。对于具有虚函数的每个对象,我们将一个点作为隐藏成员添加到数组中,通常位于前面:

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

    我们添加重定向到vtable的thunk函数

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

    然后使用指向我们实际想要调用的函数的指针填充vtable:

    vtable [0] =&amp; A :: foo_default; //我们的基类实现foo

    在派生类中,如果我们希望覆盖此虚函数,我们需要做的就是更改自己对象中的vtable,指向新函数,它也将在基类中重写:< / p>

    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?
    

    当您将基类用作有状态类时,或者当您放置应该是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. 转换方法调用函数调用,在基类中使用特殊的this参数
    2. 在内存中构造对象,因此单继承与无继承没有区别
    3. 添加thunk以调整此指针,然后调用基类以进行多重继承
    4. 使用虚方法将vtable添加到类中,并且对方法的所有调用都通过vtable到方法(thunk - &gt; vtable - &gt;方法)
    5. 通过指向基础对象的指针处理虚拟继承,而不是派生对象调用
    6. 所有这些在js.asm中都很简单。

答案 1 :(得分:1)

我绝不是asm.js专家,但你的问题引起了我的兴趣。它涉及面向对象语言设计的仇恨。你在javascript域中重新创建机器级问题似乎很讽刺。

在我看来,您的问题的解决方案是您需要设置已定义类型和功能的会计。在Java中,这通常通过使用表示任何给定对象的正确类 - &gt;函数映射的标识符来装饰字节码来完成。如果为每个定义的类使用Int32标识符,并为每个定义的函数使用附加的Int32标识符,则可以将这些标识符存储在堆上的对象表示中。您的vtable只不过是将这些组合映射到特定功能。

我希望这会对你有所帮助。

答案 2 :(得分: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 i中,我将使用void *而不是Function *,因为函数的实际类型会有所不同。编译器要做的是:

1:对于包含虚函数的每个类型,创建一个全局vtable并使用指向被覆盖函数的函数指针填充它。

2:创建对象时,设置对象的vtable成员(偏移0处)以指向全局vtable。

然后,当您想要调用虚函数时,您可以执行以下操作:

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

调用编译器在vtable中分配ID 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类的VTable可能如下所示:

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

通过使用这个逻辑,我们知道当我们在从A派生的任何类型上调用methodB时,我们将调用该对象的vtable中位于索引1的任何函数。

当然,如果你想允许多重继承,这个解决方案不会马上工作,但我相信它可以扩展到这样做。在使用Visual Studio 2008进行一些调试之后,似乎这或多或少是如何在那里实现vtable的(当然它已经扩展到处理多重继承,我还没有想过要解决这个问题)。

我希望你能得到一些可以在asm.js中应用的想法。就像我说的,我不知道asm.js究竟是如何工作的,但我已经设法在x86程序集中实现了这个系统,我也没有看到在JavaScript中实现它的任何问题,所以我希望它可以在asm中使用。也是。