如何在 LLVM 中实现面向对象的动态调度?
How to implement Object Oriented Dynamic Dispatch in LLVM?
我正在尝试为支持动态调度和简单继承的面向对象语言制作玩具编译器。这意味着任何 class 扩展父级的子级都可以在声明父级 class 时使用。它继承了它的所有字段和方法,并且可以覆盖方法。
到目前为止,我一直在考虑实现虚函数表,并确保 Parent 和 Child classes 的内存布局尽可能相似。这里有一个 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 方法中看到的那样,子 class 可以用作父 class 前提是它是 cast 作为父.使用 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 中完成的转换是 implicit。 我没有办法请参阅,在编译时,需要进行强制转换。因此,我的问题是,我如何知道 何时必须对 llvm 变量进行位转换 例如,在分配它之前或将其作为参数传递给 call 等之前。假设这仅在运行。我是否遗漏了什么,我是否应该每次都简单地进行位转换以确保在代码中传递正确的 'objects' ?如有任何帮助,我们将不胜感激!
您看到的是 Liskov substutution principle 的副作用。根据 Liskov 的原则,面向对象的语言具有那些隐式转换,LLVM IR 是一种汇编语言但不是,并且您正在编写一种将代码从一种语言转换为另一种语言的工具,因此您必须添加显式转换。
像isAssignableFrom() and cast()那样编写函数,在需要的时候调用它们,你会发现它运行起来非常自然。
基本类型也会发生同样的事情,顺便说一句。 a=b
要求您检查 a 的类型是否可从 b 的类型分配并添加强制转换,而强制转换的类型取决于您的语言的类型系统。 (在某些语言中,将 42 转换为布尔值会产生 true,在其他语言中会产生 false,并且在某些语言中会导致错误。)当您实现所有类型规则时,您对 cast() 的实现可能会相当复杂。
我正在尝试为支持动态调度和简单继承的面向对象语言制作玩具编译器。这意味着任何 class 扩展父级的子级都可以在声明父级 class 时使用。它继承了它的所有字段和方法,并且可以覆盖方法。
到目前为止,我一直在考虑实现虚函数表,并确保 Parent 和 Child classes 的内存布局尽可能相似。这里有一个 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 方法中看到的那样,子 class 可以用作父 class 前提是它是 cast 作为父.使用 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 中完成的转换是 implicit。 我没有办法请参阅,在编译时,需要进行强制转换。因此,我的问题是,我如何知道 何时必须对 llvm 变量进行位转换 例如,在分配它之前或将其作为参数传递给 call 等之前。假设这仅在运行。我是否遗漏了什么,我是否应该每次都简单地进行位转换以确保在代码中传递正确的 'objects' ?如有任何帮助,我们将不胜感激!
您看到的是 Liskov substutution principle 的副作用。根据 Liskov 的原则,面向对象的语言具有那些隐式转换,LLVM IR 是一种汇编语言但不是,并且您正在编写一种将代码从一种语言转换为另一种语言的工具,因此您必须添加显式转换。
像isAssignableFrom() and cast()那样编写函数,在需要的时候调用它们,你会发现它运行起来非常自然。
基本类型也会发生同样的事情,顺便说一句。 a=b
要求您检查 a 的类型是否可从 b 的类型分配并添加强制转换,而强制转换的类型取决于您的语言的类型系统。 (在某些语言中,将 42 转换为布尔值会产生 true,在其他语言中会产生 false,并且在某些语言中会导致错误。)当您实现所有类型规则时,您对 cast() 的实现可能会相当复杂。