wasm以小巧安全的特性受到广泛的关注,但是他也有很多的局限性。
这些局限性可以通过Host Function进行扩展,使得wasmhost能够进行交互,完成更多的功能。

Host Function需要import到wasm中,同样wasm中的function需要export到host中才能调用。

本篇主要记录下在Golang下,wasm function如何与host function进行交互。

环境依赖

  1. 使用wazero或者wasmtime-go作为wasm的runtime
  2. Tinygo编译Golang为wasm文件

为什么需要Host Function

Host Function指在Host程序里定义的方法。某些方面wasm还不太完善,比如默认情况下不能在终端上打印信息,比如fmt.Println("Hello"),此时就可以在host程序中创建一个Print(string)方法,让wasm调用这个Print(string)方法。

在Golang中需要wasm runtime将Host Function(比如Print(string))加载到wasm中即可运行,这个操作从wasm角度看,为import

show me code

wasmhost进行交互涉及到importexport操作,通过代码具体展示何时使用importexport,以及在tinygo中又该如何使用。

1. Golang创建wasm模块

1
2
3
4
5
6
7
8
9
10
11
package main

// 1
func main() {}

// 2

//export add
func add(x uint32, y uint32) uint32 {
return x + y
}
  • 1标记的空main函数是tinygo编译wasm时需要的
  • 2标记的是将add方法导出到host,这样就可以在host程序中调用wasm模块

使用tinygo将golang编译为wasm模块
tinygo build -o hello.wasm -scheduler=none --no-debug -target wasi ./hello.go

2. 编写host程序调用wasm模块

host模块是用Wazeroruntime编写,首先创建一个WebAssembly Runtime,然后实例化一个WebAssembly module,这样host程序就能调用wasm function了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/wasi_snapshot_preview1"
)

func main() {
ctx := context.Background()

// 1
r := wazero.NewRuntimeWithConfig(
ctx, wazero.NewRuntimeConfig().
WithWasmCore2())

defer r.Close(ctx)

_, err := wasi_snapshot_preview1.Instantiate(ctx, r)
if err != nil {
log.Panicln(err)
}

// 2
helloWasm, err := os.ReadFile("./hello.wasm")
if err != nil {
log.Panicln(err)
}

// 3
mod, err := r.InstantiateModuleFromBinary(ctx, helloWasm)
if err != nil {
log.Panicln(err)
}

// 4
addWasmModuleFunction := mod.ExportedFunction("add")

// 5
result, err := addWasmModuleFunction.Call(ctx, 20, 22)
if err != nil {
log.Panicln(err)
}

fmt.Println("result:", result[0])
}
  • 1标记的是创建一个WebAssembly Runtime
  • 2标记的是加载一个wasm模块
  • 3标记的是实例化一个WebAssembly module
  • 4标记的是获取一个wasm function add的引用
  • 5标记的是在host程序中调用wasm

然后可以像运行正常Go程序那样执行go run main.go,输出是result: 42

这个基础例子展示了wasm模块的常规用法,主要展示了host程序如何调用wasm模块。

  1. 但是wasm模块如何调用host function呢?
  2. 在Go编写的wasm模块中,使用export关键字标记host程序可调用的wasm function,那host function又是如何标记被wasm模块调用的呢?

带着这两个疑问看下面这一节内容。

export import 傻傻分不清楚

上面说wasm局限性时提到无法在终端打印信息,需要在host function中定义一个Print(string)方法,然后在wasm模块中调用,现在就在host程序中定义一个在终端打印数字的方法hostLogUint32

hostLogUint32

wasm模块调用host function需要在host程序中和wasm中都进行改动

  • host程序中的改动
  1. 定义一个logUint32方法
  2. export logUint32方法到Wasm runtime种,为了方便标记logUint32 export的名字为hostLogUint32
  • wasm程序中的改动
  1. import hostLogUint32方法
  2. 在wasm模块中调用hostLogUint32方法在终端打印一个数字

看下具体的代码改动,在main.go中添加logUint32方法

1
2
3
func logUint32(value uint32) {
fmt.Println("🤖:", value)
}

然后在runtime中export logUint32方法为hostLogUint32,代码如下:

1
2
3
4
5
6
7
_, errEnv := wasmRuntime.NewModuleBuilder("env").
ExportFunction("hostLogUint32", logUint32).
Instantiate(ctx, wasmRuntime)

if errEnv != nil {
log.Panicln("env/host function(s) error:", errEnv)
}

接下来在wasm模块中import hostLogUint32,代码如下:

1
2
//export hostLogUint32
func hostLogUint32(value uint32)

这里是不是有人开始疑惑了,不是说好import嘛,怎么还是export呀???我当时可是困扰了好久好久。。。
这里//export hostLogUint32其实是wasm模块import host function,可以注意到这里的hostLogUint32并没有方法体,这个可以用来区分是export还是import

最后就是在wasm模块中调用host function了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

func main() {}

//export hostLogUint32
func hostLogUint32(value uint32)

//export add
func add(x uint32, y uint32) uint32 {
res := x + y
hostLogUint32(res)
return res
}

程序准备好之后就可以运行,由于wasm模块也发生了变化,所以需要重新编译wasm文件,执行如下命令:

1
2
3
4
5
6
7
tinygo build -o hello.wasm -scheduler=none --no-debug -target wasi ./hello.go

go run main.go

# 输出如下
🤖: 42 # from the Wasm module
result: 42 # from the host program

结论

写这篇文章的主要目的其实想理解tinygo中export的用法,因为在参考wasm相关教程中host function需要import到wasm中才能使用,但是tinygo中并没有提供import关键字,这使我一度怀疑代码有问题,为了消除疑虑所以才查找了很多资料。

我的理解是tinygo中的export关键字有时间充当将wasm模块中的方法export到host的功能,有时也充当将host function中的方法import到wasm模块的功能,那这两种场景如何区分呢?是看带有export关键字的方法是否有方法体,如何有就是export,没有则是import。这个结果从hostLogUint32的解析图中可以猜想到,我们还可以将编译之后的wasm文件进行反编译进行验证。

wasm文件反编译使用的是wasm-decompile命令,具体命令为wasm-decompile log.wasm -o log.dcmp,查看log.dcmp文件可看到import和export标注的方法。

参考