了解 class 结构和构造函数调用

Understanding class structures and constructor calls

玩过循环、分支、表和所有这些不错的运算符后,我几乎开始对这种语言感到满意,足以创建一些有用的东西,但有些逻辑我仍然不明白。请耐心等待,因为它会有点长。

问题:谁能解释一下翻译后的代码是如何工作的?我在下面进一步添加了具体问题。

首先是我一直在转换的一些简单的 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 [=12=] 1)
  (export "memory" (memory [=12=]))
  (export "main" (func $main))
  (func $main (result i32)
    (local [=12=] i32)
    (i32.store offset=4
      (i32.const 0)
      (tee_local [=12=]
        (i32.sub
          (i32.load offset=4
            (i32.const 0)
          )
          (i32.const 64)
        )
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local [=12=])
          (i32.const 48)
        )
        (i32.const 10)
        (i32.const 5)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local [=12=])
          (i32.const 32)
        )
        (i32.const 30)
        (i32.const 15)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (i32.add
          (get_local [=12=])
          (i32.const 16)
        )
        (i32.const 2)
        (i32.const 4)
      )
    )
    (drop
      (call $_ZN10FirstClassC2Eii
        (get_local [=12=])
        (i32.const 2)
        (i32.const 4)
      )
    )
    (i32.store offset=4
      (i32.const 0)
      (i32.add
        (get_local [=12=])
        (i32.const 64)
      )
    )
    (i32.const 0)
  )
  (func $_ZN10FirstClassC2Eii (param [=12=] i32) (param  i32) (param  i32) (result i32)
    (i32.store offset=8
      (get_local [=12=])
      (i32.const 222)
    )
    (i32.store offset=4
      (get_local [=12=])
      (i32.const 222)
    )
    (i32.store
      (get_local [=12=])
      (i32.add
        (i32.add
          (get_local )
          (get_local )
        )
        (i32.const 111)
      )
    )
    (get_local [=12=])
  )
)

所以现在我对这里实际发生的事情有一些疑问。虽然我想我明白了大部分内容,但仍有一些事情我不确定:

例如,查看构造函数及其签名:

(func $_ZN10FirstClassC2Eii (param [=13=] i32) (param  i32) (param  i32) (result i32)

它有以下参数:(param [=16=] i32) 我假设它是在 main 函数中定义的一些局部参数。让我们说一些记忆。然而,我们知道我们在 main 函数中有 4 个实例,这意味着所有这些实例都保存在同一个 (local [=17=] i32) 中但偏移量不同,我是对还是错?

接下来让我们看一下对构造函数的调用:

(drop
  (call $_ZN10FirstClassC2Eii
    (i32.add
      (get_local [=14=])
      (i32.const 32)
    )
    (i32.const 30)
    (i32.const 15)
  )
)

我们调用构造函数并传入3个参数。但是,添加到底是为了什么?我们要在本地添加 space 吗?仔细观察,对于每个构造函数调用,这个数字都减少了 16(我从上到下阅读代码),这大约是一个单词的大小。不知道什么意思

最后我们有:

(i32.store offset=4
  (i32.const 0)
  (tee_local [=15=]
    (i32.sub
      (i32.load offset=4
        (i32.const 0)
      )
      (i32.const 64)
    )
  )
)

它甚至加载了什么,为什么减法?我的意思是它设置一个本地并返回它以便我们可以将它存储在偏移量为 4 的线性内存中?偏移量 4 与什么有关?

您注意到的许多内容是在 C++ 中到 某些编译器 IR 的翻译。由于您使用的工具是基于 LLVM 的,如果您想进行洞穴探险,我建议您查看 LLVM 的 IR。 Here's your example, also unoptimized, in LLVM IR。这很有趣,因为 WebAssembly 发生在这个 LLVM IR 之后,所以你可以看到 C++ 的部分翻译。也许我们可以理解它!


与 C++ 中的所有非静态函数 class 成员一样,构造函数具有隐式 *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 时,堆栈必须去某个地方。 WebAssembly 中有 3 个 C++ 堆栈可以进入:

  1. 在执行堆栈上(每个操作码压入和弹出值,所以 add 弹出 2,然后压入 1)。
  2. 作为本地人。
  3. Memory.

请注意,您不能获取 1. 和 2 的地址。构造函数将 *this 传递给函数,因此编译器 必须 将该值放在MemoryMemory 中的堆栈在哪里? Emscripten 会为您处理!它决定将内存中的堆栈指针存储在地址 4,因此 (i32.load offset=4 (i32.const 0))。来自 LLVM 的四个 alloca 然后位于该地址的偏移量处,因此 (i32.add (get_local [=27=]) (i32.const 48)) 正在获取堆栈位置(我们在本地 [=28=] 中加载)并获取其偏移量。那就是*this.

的价值

请注意,经过优化后,绝大多数 C++ 堆栈变量不会最终进入内存!大多数将被推送/弹出,或存储在 WebAssembly locals(其中有一个无穷大)。这与 x86 或 ARM 等其他 ISA 类似:将局部变量放在寄存器中会更好,但这些 ISA 只有一小部分。因为 WebAssembly 是一个 虚拟 ISA 我们可以负担得起无限的局部变量,所以 LLVM / Emscripten 必须具体化到内存中的堆栈要小得多。它们必须具体化的唯一时间是它们的地址被获取,或者它们通过引用传递(实际上是一个指针),或者一个函数有多个 return 值(WebAssembly 将来可能支持)。


您拥有的最后一段代码:

  1. 加载内存堆栈指针。
  2. 从中减去 64。
  3. 存储堆栈指针。

这就是你的功能序言。如果您查看函数的最后,您会发现匹配的结语,它将 64 添加回指针。这使得四个 alloca 成为 space。它是(非官方的)WebAssembly ABI 的一部分,每个函数负责为其变量增加和缩小内存中的堆栈。

为什么是 64?那是 4 x 16,对于这四个 FirstClass 实例来说刚好 space:它们每个都有 3 个 i32,存储时每个实例都四舍五入到 16 个字节,以便对齐。在 C++ 中尝试 sizeof(FirstClass)(它是 12),然后尝试分配它们的数组(它们将各自填充 4 个字节,以便每个条目对齐)。这只是C++通常实现的一部分,与LLVM或WebAssembly无关。