compiler-construction - 如何在 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 以确保在代码中传递正确的“对象”?任何帮助,将不胜感激!
解决方案
您所看到的是Liskov 替代原则的副作用。根据 Liskov 的原则,面向对象的语言具有那些隐式转换,LLVM IR 是一种汇编语言并且没有,并且您正在编写一个将代码从一种语言转换为另一种语言的工具,因此您必须添加显式转换。
编写诸如isAssignableFrom()和cast()之类的函数,并在必要时调用它们,您会发现它工作得非常自然。
顺便说一句,原始类型也会发生同样的事情。a=b
要求您检查 a 的类型是否可以从 b 分配并添加强制转换,并且强制转换的类型取决于您的语言的类型系统。(在某些语言中,将 42 转换为布尔值会产生 true,在其他语言中会产生 false,而在某些语言中则会导致错误。)当您实现所有类型规则时,您的 cast() 实现可能已经相当复杂了。
推荐阅读
- vue.js - 在向我的 heroku 端点发出 POST 请求并将其保存到我的 mongoDB 后,数据仅在我刷新页面时显示
- azure - 在基于 Azure 的微服务架构中添加聚合的位置
- wso2 - WSO2 ESB.5.0.0 开放并行连接限制
- python - 当我想使用名称的一小部分时如何调用文件名?
- sql - 在一个结果行中选择具有给定时间间隔的 id 历史记录的行
- background-process - python 2.7.5:在后台运行整个函数
- eloquent - 我可以强制 Eloquent 模型 created_at 使用 unix_timestamp() 作为 DB 服务器时间来对抗时间漂移
- ruby-on-rails - Ruby on Rails - ActionCable - : undefined method `redis_connection_for_subscriptions' for #
我最近开始使用 ActionCable 并设法让多个频道正常工作,但是在测试频道时我遇到了问题。
示例频道 - 订阅方法:
- variable-assignment - 匈牙利方法 - 3 选择分配
- python - 使用 dask 监控 xarrays 拆分应用组合的进度