🐰 MoonBit 字符串的 山 WebAssembly 漂流 › 我们的 WAT [#255]

山 WebAssembly 被设计为两种不同的表示,一种是利于压缩和网络传输的 Webassembly Binary format,一种是便于人类阅读的 WAT (Webassembly Textual format);这两种格式是等价的。我们关心 山 WebAssembly 后端编译结果的细节,因此 WAT 是更合适的选择。

通过构建工具 moon 编译 🐰 MoonBit 到 WAT:

> moon build --target wasm --output-wat
target/wasm/release/build/lib/lib.wat 可以找到 WAT 文件。

现在我们得到了编译产物,在查看之前,先用 wasmtime 跑跑看。传递参数 --invoke foo 表示调用 lib.wat 中导出的 foo 函数:

> wasmtime --invoke foo target/wasm/release/build/lib/lib.wat
10000
看起来 foo 返回了一个数字,但我们想要的是一个字符串,这是什么?

让我们来看看 WAT 的内容,好在 WAT 相当短(鉴于源代码也相当短且简单,这是预期中的),只需要扫一眼,马上就能明白发生了什么。

target/wasm/release/build/lib/lib.wat

(data (memory $moonbit.memory) (offset (i32.const 10000))
      "\FF\FF\FF\FF\F3\02\00\00A\00B\00\00\00\00\03\00\00\00\00\00\00\00\00")
(memory $moonbit.memory 1)
(table $moonbit.global 0 0 funcref )
(elem (table $moonbit.global) (offset (i32.const 0))
      funcref)
(func $jinser/tour-of-moonbit-string/lib.foo (result i32) (; #2 ;)
  (i32.const 10000)) (; #3 ;)
(export "foo" (func $jinser/tour-of-moonbit-string/lib.foo)) (; #1 ;)
(start $*init*/1)
(func $*init*/1)
这里发生的事情是,函数 $jinser/tour-of-moonbit-string/lib.foo 被导出为 "foo"(#1),该函数的返回值类型为 i32(#2),是一个常数值 10000(#3)。

这虽然很好地解释了 wasmtime 为何返回这个数字,但还没有解释这个数字是什么。不用多说,我想你已经注意到了 WAT 第一行的可疑指令。看来我们又找到线索了!要使猜测更有说服力,让我们来试着解释一下这句指令做了什么。

根据 山 WebAssembly spec 中对 data segment 的描述,参考其形式化定义

\[\begin {array}{llll} \phantom {data segment} & data &::=& \{ \text {init}~vec(byte), \text {mode}~datamode \} \\ \phantom {data segment mode} & \text {datamode} &::=& passive \\&&|& active~\{ \text {mem}~memidx, \text {offset}~expr \} \\ \end {array} \]
得到以下信息:
  • \(data\) (data mode init) 此指令用于声明一段数据。这里是 active data segment ,意味着复制 init 到 WebAssembly 模块的线性内存中。
  • \(datamode (active~\{\text {mem}\})\) (memory $moonbit.memory) 指定数据要放置的内存段,这里引用了名为 $moonbit.memory 的内存。
  • \(datamode (active~\{\text {offset}\})\) (offset (i32.const 10000)) 指定了数据在内存中应该存放位置的偏移量,这里使用的是 (i32.const 10000),表示将数据放置在内存地址 10000。
  • \(\text {init}\) "\FF\FF\FF\FF\F3\02\00\00A\00B\00\00\00\00\03\00\00\00\00\00\00\00\00" 这是要写入到内存中的字节序列。每个 \XX 表示一个字节,使用十六进制表示法。
看起来,foo 返回的值 10000,就是这段数据的 offset 了。