理解类结构和构造函数调用

Asp*_*ger 6 webassembly

玩过循环,分支,表格和所有那些不错的操作符后,我几乎开始对语言感到满意,足以创建有用的东西,但有一些我仍然不理解的逻辑.请耐心等待,因为它会有点长.

问题:有人可以解释翻译代码的工作原理吗?我在下面进一步提出具体问题.

首先是我一直在转换的一些简单的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);
}
Run Code Online (Sandbox Code Playgroud)

这转化为:

(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)
  )
)
Run Code Online (Sandbox Code Playgroud)

所以现在我对这里的实际情况有一些疑问.虽然我认为我理解其中的大部分内容,但仍有一些事情我不确定:

例如,请参阅构造函数及其签名:

(func $_ZN10FirstClassC2Eii (param $0 i32) (param $1 i32) (param $2 i32) (result i32)
Run Code Online (Sandbox Code Playgroud)

它有以下参数:(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)
  )
)
Run Code Online (Sandbox Code Playgroud)

我们调用构造函数并传入3个参数.虽然增加了什么呢?我们在当地增加空间吗?仔细观察它,对于每个构造函数调用,这个数字减少16(我从上到下读取代码),这大约是一个单词的大小.我不知道这意味着什么.

最后我们有:

(i32.store offset=4
  (i32.const 0)
  (tee_local $0
    (i32.sub
      (i32.load offset=4
        (i32.const 0)
      )
      (i32.const 64)
    )
  )
)
Run Code Online (Sandbox Code Playgroud)

什么是均匀加载和为什么减法?我的意思是它设置一个本地并返回它,以便我们可以将它存储在偏移4的线性内存中?相对于什么偏移4?

JF *_*ien 2

您注意到的很多内容都是 C++ 到某些编译器 IR的翻译。由于您使用的工具是基于 LLVM 的,因此如果您想深入了解,我建议您查看 LLVM 的 IR。这是您在 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 {
Run Code Online (Sandbox Code Playgroud)

%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
Run Code Online (Sandbox Code Playgroud)

所以它的堆栈思想保存了四个类型的变量FirstClass。当我们降低到 WebAssembly 时,堆栈必须转移到某个地方。WebAssembly 中有 3 个地方可以使用 C++ 堆栈:

  1. 在执行堆栈上(每个操作码都会压入和弹出值,因此add先弹出 2,然后压入 1)。
  2. 作为当地人。
  3. 在里面Memory

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

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


您拥有的最后一段代码:

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

这就是你的函数序言。如果您查看函数的最后部分,您会发现匹配的尾声,它将 64 添加回指针。这是为四个人腾出空间alloca。它是(非官方)WebAssembly ABI 的一部分,每个函数负责增长和收缩其变量的内存中的堆栈。

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