玩过循环,分支,表格以及所有那些不错的操作符后,我几乎开始觉得语言足以创造有用的东西,但有一些我仍然不理解的逻辑。请耐心等待,因为它会有点长。
问题:有人可以解释翻译代码的工作原理吗?我在下面进一步提出具体问题。
首先,这里是我一直在转换的一些简单的c ++代码:
class FirstClass {
int prop1 = 111;
int prop2 = 222;
int prop3 = 333;
public:
FirstClass(int param1, int param2) {
prop1 += param1 + param2;
}
};
class SecondClass {
public:
SecondClass() {
}
};
int main() {
FirstClass firstClass1(10, 5);
FirstClass firstClass2(30, 15);
FirstClass firstClass3(2, 4);
FirstClass firstClass4(2, 4);
}
转化为:
(module
(table 0 anyfunc)
(memory $0 1)
(export "memory" (memory $0))
(export "main" (func $main))
(func $main (result i32)
(local $0 i32)
(i32.store offset=4
(i32.const 0)
(tee_local $0
(i32.sub
(i32.load offset=4
(i32.const 0)
)
(i32.const 64)
)
)
)
(drop
(call $_ZN10FirstClassC2Eii
(i32.add
(get_local $0)
(i32.const 48)
)
(i32.const 10)
(i32.const 5)
)
)
(drop
(call $_ZN10FirstClassC2Eii
(i32.add
(get_local $0)
(i32.const 32)
)
(i32.const 30)
(i32.const 15)
)
)
(drop
(call $_ZN10FirstClassC2Eii
(i32.add
(get_local $0)
(i32.const 16)
)
(i32.const 2)
(i32.const 4)
)
)
(drop
(call $_ZN10FirstClassC2Eii
(get_local $0)
(i32.const 2)
(i32.const 4)
)
)
(i32.store offset=4
(i32.const 0)
(i32.add
(get_local $0)
(i32.const 64)
)
)
(i32.const 0)
)
(func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
(i32.store offset=8
(get_local $0)
(i32.const 222)
)
(i32.store offset=4
(get_local $0)
(i32.const 222)
)
(i32.store
(get_local $0)
(i32.add
(i32.add
(get_local $1)
(get_local $2)
)
(i32.const 111)
)
)
(get_local $0)
)
)
所以现在我对这里的实际情况有一些疑问。虽然我认为我理解其中的大部分内容,但仍然存在一些我不确定的事情:
例如,请参阅构造函数及其签名:
(func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
它有以下参数:(param $0 i32)
我假设它是在main函数中定义的一些局部。让我们说一些记忆。但是,我们知道main函数中有4个实例,这意味着所有这些实例都保存在同一(local $0 i32)
内,但偏移量不同,我是对还是错了?
接下来让我们看看对构造函数的调用:
(drop
(call $_ZN10FirstClassC2Eii
(i32.add
(get_local $0)
(i32.const 32)
)
(i32.const 30)
(i32.const 15)
)
)
我们调用构造函数并传入3个参数。虽然增加了什么呢?我们在当地增加空间吗?仔细观察它,对于每个构造函数调用,这个数字减少16(我从上到下读取代码),这大约是一个单词的大小。我不知道这意味着什么。
最后我们有:
(i32.store offset=4
(i32.const 0)
(tee_local $0
(i32.sub
(i32.load offset=4
(i32.const 0)
)
(i32.const 64)
)
)
)
什么是均匀加载以及为什么减法?我的意思是它设置一个本地并返回它,以便我们可以将它存储在偏移4的线性内存中?相对于什么偏移4?
答案 0 :(得分:2)
您注意到很多内容是在C ++中某些编译器IR 转换。由于您使用的工具基于LLVM,我建议您查看LLVM的IR,如果您想要进行探索。 Here's your example, also unoptimized, in LLVM IR。这很有趣,因为WebAssembly发生在此LLVM IR之后,因此您可以从C ++中看到部分转换。也许我们可以理解它!
与C ++中的所有非静态函数类成员一样,构造函数具有隐式*this
参数。这就是第零个参数。为什么是i32
?因为WebAssembly中的所有指针都是i32
。
在LLVM IR中,这是:
define linkonce_odr void @FirstClass::FirstClass(int, int)(%class.FirstClass*, i32, i32) unnamed_addr #2 comdat align 2 !dbg !29 {
其中%class.FirstClass*
是*this
指针。稍后,当降级到WebAssembly时,它将变为i32
。
对于您的以下问题...调用构造函数时添加了什么?我们必须创建*this
,然后在堆栈上分配它们。 LLVM因此执行这些分配:
%1 = alloca %class.FirstClass, align 4
%2 = alloca %class.FirstClass, align 4
%3 = alloca %class.FirstClass, align 4
%4 = alloca %class.FirstClass, align 4
因此,它的堆栈概念包含四个FirstClass
类型的变量。当我们降低到WebAssembly时,堆栈必须到达某个地方。 C ++堆栈可以在WebAssembly中有3个位置:
add
弹出2然后按1)。Memory
。请注意,您不能取地址1.和2.构造函数将*this
传递给函数,因此编译器必须将该值放在{{1}上}。 Memory
中的堆栈在哪里? Emscripten为您照顾它!它决定将内存堆栈指针存储在地址4,因此Memory
。然后来自LLVM的四个(i32.load offset=4 (i32.const 0))
位于该地址的偏移处,因此alloca
将获取堆栈位置(我们在本地(i32.add (get_local $0) (i32.const 48))
中加载)并获得其偏移量。这是$0
的价值。
请注意,优化后,绝大多数C ++ on-stack变量都不会在内存中结束!大多数将被推送/弹出,或存储在WebAssembly本地(其中有无穷大)。这类似于其他ISA,如x86或ARM:将locals放入寄存器更好,但这些ISA只有少数几种。因为WebAssembly是虚拟ISA ,所以我们可以提供无限的本地化,因此LLVM / Emscripten必须实现到内存中的堆栈要小得多。它们必须实现的唯一时间是它们的地址,或者它们是通过引用传递(实际上是一个指针),或者一个函数有多个返回值(WebAssembly将来可能会支持)。
您拥有的最后一段代码:
这是你的功能序幕。如果你查看函数的最后一部分,你会找到匹配的结尾,它会向指针添加64。这为四个*this
腾出了空间。它是(非官方)WebAssembly ABI的一部分,每个函数负责在内存中为其变量增加和缩小堆栈。
为什么是64?那是4 x 16,这对于那四个alloca
个实例来说就足够了:它们每个都有3个FirstClass
,在存储时每个都被四舍五入到16个字节,用于对齐。在C ++中尝试i32
(它是12),然后尝试分配它们的数组(它们每个都填充4个字节,因此每个条目都是对齐的)。这只是C ++常用实现的一部分,与LLVM或WebAssembly无关。