首页 > 解决方案 > 如何在 LLVM 中实现面向对象的动态调度?

问题描述

我正在尝试为支持动态调度和简单继承的面向对象语言制作玩具编译器。这意味着只要声明了 Parent 类,就可以使用任何扩展父类的 Child 类。它继承了它的所有字段和它的方法,并且它可以覆盖方法。

到目前为止,我一直在考虑实现虚函数表,并确保父类和子类的内存布局尽可能相似。这是 C 中的一个小例子来说明我的意思:

#include <stdlib.h>

typedef struct {
    struct ParentVTable *_vtable;
    int inheritedField;
} Parent;

struct ParentVTable {
    void (*inheritedMethod)(Parent *, int);
};

void Parent_inheritedMethod(Parent *self, int b) {
    b = 0;
}

struct ParentVTable ParentVTable_inst = {
    .inheritedMethod = &Parent_inheritedMethod
};

void Parent_init(Parent *self) {
    self->_vtable = &ParentVTable_inst;
    self->inheritedField = 42;
}

Parent *Parent_new(void) {
    Parent *self = (Parent*)malloc(sizeof(Parent));
    Parent_init(self);
    return self;
}

typedef struct {
    struct ChildVTable *_vtable;
    int inheritedField;
    int newField;
} Child;

struct ChildVTable {
    void (*inheritedMethod)(Child *, int);
    int (*newMethod)(Child *, int);
};

int Child_newMethod(Child *self, int i) {
    return i + self->inheritedField;
}

struct ChildVTable ChildVTable_inst = {
    .inheritedMethod = (void (*)(Child *, int)) Parent_inheritedMethod,
    .newMethod = &Child_newMethod
};

void Child_init(Child *self) {
    Parent_init((Parent *) self);
    self->_vtable = &ChildVTable_inst;
    self->newField = 0;
}

Child *Child_new(void) {
    Child *self = (Child*)malloc(sizeof(Child));
    Child_init(self);
    return self;
}

int main() {
    Parent *p = (Parent*) Child_new();
    return 0;
}

正如您在 main 方法中看到的那样,只要将 Child 类转换为 Parent 类,它就可以用作 Parent 类。使用 clang 此转换在 LLVM 中转换为bitcast操作,否则 llvm 无效(llvm 代码无法编译)。下面是对应的一段代码:

define i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca %struct.Parent*, align 8
  store i32 0, i32* %1, align 4
  %3 = call %struct.Child* @Child_new()
  %4 = bitcast %struct.Child* %3 to %struct.Parent*
  store %struct.Parent* %4, %struct.Parent** %2, align 8
  ret i32 0
}

到那里一切都很好。问题是,在我尝试制作的语言中,以及在大多数实现继承的面向对象语言中,在 C 中完成的这种转换是隐式的。在编译时,我无法看到需要强制转换。因此,我的问题是,我如何知道何时必须对 llvm 变量进行比特转换,例如在分配它之前或在将其作为参数传递给调用等之前。假设这仅在运行时知道。我是否遗漏了什么,我是否应该每次都简单地进行 bitcast 以确保在代码中传递正确的“对象”?任何帮助,将不胜感激!

标签: compiler-constructionllvmllvm-irdynamic-dispatch

解决方案


您所看到的是Liskov 替代原则的副作用。根据 Liskov 的原则,面向对象的语言具有那些隐式转换,LLVM IR 是一种汇编语言并且没有,并且您正在编写一个将代码从一种语言转换为另一种语言的工具,因此您必须添加显式转换。

编写诸如isAssignableFrom()cast()之类的函数,并在必要时调用它们,您会发现它工作得非常自然。

顺便说一句,原始类型也会发生同样的事情。a=b要求您检查 a 的类型是否可以从 b 分配并添加强制转换,并且强制转换的类型取决于您的语言的类型系统。(在某些语言中,将 42 转换为布尔值会产生 true,在其他语言中会产生 false,而在某些语言中则会导致错误。)当您实现所有类型规则时,您的 cast() 实现可能已经相当复杂了。


推荐阅读