«

《WebAssembly标准入门》读书笔记之Go语言

image.png

—— WebAssembly的终极目标是为从底层的CPU到上层的动态库构建可移植的标准。

[toc]

《WebAssembly标准入门》读书笔记系列


本文是《WebAssembly标准入门》第七章节主要内容,重点介绍 WebAssembly 在 Go 语言中的支持情况。

Go语言是流行的新兴编程语言之一,目前已经开始称霸云计算等领域。从Go 1.11开始,WebAssembly开始作为一个标准平台被官方支持,这说明了Go语言官方团队也认可了WebAssembly平台的重要性和巨大潜力。目前Go语言社区已经有众多与WebAssembly相关的开源项目,例如,有很多开源的WebAssembly虚拟机就是采用Go语言实现的。本章将介绍Go语言和WebAssembly相关的技术。

你好,Go语言

Go语言是一种极度精简的编译型语言,诞生时间已经超过十年。本章假设读者已经有一定的Go语言使用经验。读者如果想深入了解Go语言,可以从Go语言官方团队成员写的《Go语言程序设计》(The Go Programming Language)和本书作者写的《Go语言高级编程》等教程开始学习。我们先看看Go语言如何输出“你好,WebAssembly”信息。

先安装Go1.11+版本的Go语言环境,然后创建hello.go文件:

package main

import (  
  "fmt"
)

func main() {  
  fmt.Println("你好,WebAssembly")
}

我们简单介绍一下这个Go程序。第一部分package语句表示当前的包名字为main。第二部分的import语言导入了名为fmt的包,这个包提供了诸多与格式化输出相关的函数。第三部分的func语句定义了一个名为main的函数,后面用花括号包含的部分是函数的主体代码。在main()函数的主体代码部分,通过导入的fmt包提供的Println()方法输出了字符串。根据Go语言的规范,main包内的main()函数是程序的入口。

对于macOS或Linux等类UNIX系统,可以通过以下命令直接运行Go语言程序:

$ export GOOS=js
$ export GOARCH=wasm
$ go run -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" hello.go
你好,Go语言

其中GOOS环境变量对应的操作系统名为jsGOARCH对应的CPU类型为wasmgo命令通过-exec参数指定$(GOROOT)/misc/wasm/go_js_wasm_exe脚本为真实的启动执行命令。gojswasm_exe脚本会自动处理Go语言运行时初始化等操作。go test单元测试命令通过类似的方式运行。

对于不支持gojswasm_exe脚本的操作系统,则可以先将Go程序编译为wasm模块。例如,Windows系统可以通过以下命令编译生成wasm文件:

C:\hello\> set GOARCH=wasm  
C:\hello\> set GOOS=js  
C:\hello\> go build -o a.out.wasm hello.go  

在生成的a.out.wasm文件中包含了完整的Go语言运行时环境,因此模块文件的体积可能超过2 MB大小。如果本地机器安装了wasm2wat反汇编工具(参见《WebAssembly标准入门》之快速入门),可以用以下命令将反汇编的结果输出到一个文件:

$ wasm2wat a.out.wasm -o a.wasm2wat.txt

反汇编之后的是文本格式的WebAssembly程序,体积可能是二进制格式的几十倍,需要采用专业的编辑工具才能打开查看(参见《WebAssembly标准入门》之快速入门)。如果反汇编结果正常就说明Go语言已经可以正常输出为WebAssembly模块了。

下面可以尝试用Node.js提供的node命令在命令行环境直接执行a.out.wasm文件:

$ node a.out.wasm
/path/to/hello/a.out.wasm:1
(function (exports, require, module, __filename, __dirname) {

SyntaxError: Invalid or unexpected token  
  at new Script (vm.js:74:7)
  at createScript (vm.js:246:10)
  at Object.runInThisContext (vm.js:298:10)
  at Module._compile (internal/modules/cjs/loader.js:657:28)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
  at Module.load (internal/modules/cjs/loader.js:599:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
  at Function.Module._load (internal/modules/cjs/loader.js:530:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
  at startup (internal/bootstrap/node.js:266:19)

用Node.js的node命令直接执行Go语言输出的WebAssembly模块时遇到了错误。分析错误时的函数调用栈信息,可以推测Go语言执行main.main()函数之前没有正确地初始化Go语言运行时环境导致错误。Go语言运行时的初始化是一个相对复杂的工作,因此Go语言提供了一个wasmexec.js文件用于初始化或运行的工作。同时提供了一个基于node命令包装的gojswasmexec脚本文件,用于执行Go语言生成的WebAssembly模块前调用wasm_exec.js文件进行运行时的初始化工作。

gojswasmexec脚本类似node wasm_exec.js命令的组合。我们可以通过以下命令来执行a.out.wasm模块(需要将wasmexec.js文件先复制到当前目录):

$ node wasm_exec.js a.out.wasm
你好,Go语言

浏览器中的Go语言

在上一节中,我们已经在命令行环境成功运行了Go语言生成的WebAssembly模块。本节,我们将学习如何在浏览器环境运行WebAssembly模块。在浏览器环境运行Go语言生成的WebAssembly模块同样需要通过node wasm_exec.js文件初始化Go语言运行时环境。

要在浏览器运行WebAssembly模块,首先序言准备一个index.html文件:

<!doctype html>

<html>  
<head>  
<title>Go wasm</title>  
</head>

<body>  
<script src="wasm_exec.js"></script>  
<script src="index.js"></script>

<button onClick="run();" id="runButton">Run</button>  
</body>  
</html>  

其中第一个script结点包含了wasm_exec.js文件,用于准备用于初始化Go语言运行时环境的Go类对象。真正的Go运行时的初始化工作在index.js文件中完成。最后在HTML页面放置一个按钮,当按钮被点击时通过在dinex.js提供的run()函数启动Go语言的main()函数。

初始化的代码在index.js文件提供:

const go = new Go();  
let mod, inst;

WebAssembly.instantiateStreaming(  
  fetch("a.out.wasm"), go.importObject
).then(
  (result) => {
    mod = result.module;
    inst = result.instance;
    console.log("init done");
  }
).catch((err) => {
  console.error(err);
});

async function run() {  
  await go.run(inst);

  // reset instance
  inst = await WebAssembly.instantiate(
    mod, go.importObject
  );
}

首先构造一个Go运行时对象,Go类是在wasm_exec.js文件中定义。然后通过JavaScript环境提供的WebAssembly对象接口加载a.out.wasm模块,在加载模块的同时注入了Go语言需要的go.importObject()辅助函数。当加载完成时将模块对象和模块的实例分别保持到modeinst变量中,同时打印模块初始化完成的信息。

为了便于在页面触发Go语言的main()函数,index.js文件还定义了一个run()异步函数。在run()函数内部,首先通过await go.run(inst)异步执行main()函数。当main()函数退出后,再重新构造运行时实例。

如果运行环境不支持WebAssembly.instantiateStreaming()函数,可以通过以下代码填充一个模拟实现:

if (!WebAssembly.instantiateStreaming) { // polyfill  
   WebAssembly.instantiateStreaming = async (resp, importObject) => {
   const source = await (await resp).arrayBuffer();
   return await WebAssembly.instantiate(source, importObject);
  };
}

然后在当前目录启动一个Web服务,就可以在浏览器查看页面了,如图所示。

图7-1

需要说明的是,必须在模块初始化工作完成后点击按钮运行main()函数。多次点击按钮和多次运行Go程序类似,之前运行时的内存状态不会影响当前的内存,每次运行的内存状态是独立的。

使用JavaScript函数

Go语言输出的WebAssembly模块已经可以在Node.js或浏览器环境运行,在这一节我们将进一步尝试在Go语言中使用宿主环境的JavaScript函数。在跨语言编程中,首先要解决的问题是两种编程语言如何交换数据。在JavaScript语言中,总共有空值、未定义、布尔值、数字、字符串、对象、符号和函数8种类型。JavaScript语言的8种类型都可以通过Go语言提供的syscall/js包中的Value类型表示。

在JavaScript语言中,可以通过self来获取全局的环境对象,它一般对应浏览器的window对象,或者是Node.js环境的global对象。我们可以通过syscall/js包提供的Global()函数获取宿主JavaScript环境的全局对象:

import (  
  "syscall/js"
)

func main() {  
  var g = js.Global()
}

js.Global()返回的全局对象是一个Value类型的对象。因此可以使用Value提供的Get方法获取全局对象提供的方法:

func main() {  
  var g = js.Global()
  var console = g.Get("console")
  var console_log = console.Get("log")
}

以上代码首先是通过全局对象获取内置的console对象,然后再从console对象获取log()方法。在JavaScript中方法也是一种对象,在获取到console.log()函数之后,就可以通过Value提供的Invoke()调用函数进行输出:

func main() {  
  var g = js.Global()
  var console = g.Get("console")
  var console_log = console.Get("log")

  console_log.Invoke("hello wasm!")
}

上述代码的功能和JavaScript中的console.log("hello wasm!")语句是等价的。如果是在浏览器环境,还可以调用内置的alert()函数在弹出的对话框输出信息:

package main

import (  
  "syscall/js"
)

func main() {  
  alert := js.Global().Get("alert")
  alert.Invoke("Hello wasm!")
}

在浏览器中,全局对象和window对象是同一个对象。因此全局对象的alert()函数就是window.alert()函数。虽然输出的方式有所不同,不过alertconsole.log的调用方式却是非常相似的。

在JavaScript语言中,还提供了可以运行JavaScript代码的eval()函数。我们可以通过Value类型的Call()函数直接调用全局对象提供的eval()函数:

func main() {  
  js.Global().Call("eval", '
    console.log("hello, wasm!");
  ')
}

上述代码通过在eval()函数,直接执行console.log()输出字符串。采用这种技术,可以方便地在JavaScript中进行复杂的初始化操作。

回调Go函数

在前一节中,我们已经了解了如何在Go语言中调用宿主提供的JavaScript函数的方法。在本节我们将讨论在JavaScript中回调Go语言实现的函数。

syscall/js包中提供的js.Callback回调类型可以将Go函数包装为JavaScript函数。JavaScript语言中函数是一种基本类型,可以将函数作为方法绑定到对象的属性中。

下面的代码展示了如何将Go语言的println()函数绑定到JavaScript语言的全局对象:

func main() {  
  var cb = js.NewCallback(func(args []js.Value) {
    println("hello callback")
  })
  js.Global().Set("println", cb)

  println := js.Global().Get("println")
  println.Invoke()
}

首先通过js.NewCallbackfunc(args []js.Value)类型的函数参数包装为js.Callback回调函数类型,在参数函数中调用了Go语言的println()函数。然后通过js.Global()获取JavaScript的全局对象,并通过全局对象的Set方法将刚刚构建的回调函数绑定到名为"println"的属性中。

当Go语言函数绑定到全局对象之后,就可以按照普通JavaScript函数的方式使用该函数了。在例子中,我们再次通过js.Global()返回的全局对象,并且获取刚刚绑定的println()函数。获取的println()函数也是js.Value类型,因此可以通过该类型的Invoke()方法调用函数。

上述代码虽然将Go语言函数绑定到JavaScript语言的全局对象,但是测试运行时可能并不能看到输出信息。这是因为JavaScript回调Go语言函数是在后台Goroutine中运行的,而当main()函数退出导致主Goroutine也退出时,程序就提前结束导致无法看到输出结果。

要想看到输出结果,一个临时的解决方案是在main()函数退出前休眠一段时间,让后台Goroutine有机会完成回调函数的执行:

func main() {  
  js.Global().Set("println", js.NewCallback(func(args []js.Value) {
    println("hello callback")
  }))

  println := js.Global().Get("println")
  println.Invoke()

  time.Sleep(time.Second)
}

一般情况下,现在就可以看到输出结果了。

我们刚刚包装的println()函数并不支持参数,只能输出固定的信息。下面我们继续改进println()函数,为参数增加可变参数支持:

js.Global().Set("println", js.NewCallback(func(args []js.Value) {  
  var goargs []interface{}
  for _, v := range args {
    goargs = append(goargs, v)
  }
  fmt.Println(goargs...)
}))

在新的实现中,我们首先将[]js.Value类型的参数转换为[]interface{}类型的参数,底层再调用fmt.Println()函数将可变的参数全部输出。

然后我们可以在JavaScript中直接使用println()函数进行输出:

js.Global().Call("eval", '  
  println("hello", "wasm");
  println(123, "abc");
')  

为了保证每个println()回调函数在后台Goroutine中完成输出工作,我们还需要为回调函数增加消息同步机制。下面是改进之后的完整代码:

func main() {  
  var g = js.Global()
  var wg sync.WaitGroup

  g.Set("println", js.NewCallback(func(args []js.Value) {
    defer wg.Done()

    var goargs []interface{}
    for _, v := range args {
      goargs = append(goargs, v)
    }

    fmt.Println(goargs...)
  }))

  wg.Add(2)
  g.Call("eval", '
    println("hello", "wasm");
    println(123, "abc");
  ')

  wg.Wait()
}

我们通过sync.WaitGroup()来确保每个回调函数都完成输出工作了。在每次回调函数返回前,通过wg.Done()调用标记完成一个等待事件。然后在JavaScript中执行两个println()函数调用之前,通过wg.Add(2)先注册两个等待事件。最后在main()函数退出前通过wg.Wait()确保全部调用已经完成。

syscall/js

当Go语言需要调用底层系统的功能时,需要通过syscall包提供的系统调用功能。而针对WebAssembly平台的系统调用在syscall/js包提供。要想灵活使用WebAssembly的全部功能,需要熟练了解syscall/js包的每个功能。

可以通过go doc命令查看包的文档,通过将GOARCHGOOS环境变量分别设置为wasmjs表示查看WebAssembly平台对应的包文档:

$ GOARCH=wasm GOOS=js go doc syscall/js
package js // import "syscall/js"

Package js gives access to the WebAssembly host environment when using the  
js/wasm architecture. Its API is based on JavaScript semantics.

This package is EXPERIMENTAL. Its current scope is only to allow tests to  
run, but not yet to provide a comprehensive API for users. It is exempt from  
the Go compatibility promise.

type Callback struct{ ... }  
funcNewCallback(fn func(args []Value)) Callback  
funcNewEventCallback(flags EventCallbackFlag, fn func(event Value)) Callback  
type Error struct{ ... }  
type EventCallbackFlag int  
  const PreventDefaultEventCallbackFlag = 1 << iota ...
type Type int  
  const TypeUndefined Type = iota ...
type TypedArray struct{ ... }  
funcTypedArrayOf(slice interface{}) TypedArray  
type Value struct{ ... }  
func Global() Value  
func Null() Value  
func Undefined() Value  
funcValueOf(x interface{}) Value  
type ValueError struct{ ... }  

syscall/js包提供的功能,其中最重要的是Value类型,可以表示任何的JavaScript对象。而Callback则表示Go语言回调函数,底层也是一种特殊的Value类型。而Type则用于表示JavaScript语言中8种基础数据类型,其中TypedArray则是对应JavaScript语言中的带类型的数组。最后ValueErrorError表示相关的错误类型。

我们先看看最重要的Value类型的文档:

$ GOARCH=wasm GOOS=js go doc syscall/js.Value
type Value struct {  
  // Has unexported fields.
}
  Value represents a JavaScript value.

func Global() Value  
func Null() Value  
func Undefined() Value  
funcValueOf(x interface{}) Value  
func (v Value) Bool() bool  
func (v Value) Call(m string, args ...interface{}) Value  
func (v Value) Float() float64  
func (v Value) Get(p string) Value  
func (v Value) Index(i int) Value  
func (v Value) InstanceOf(t Value) bool  
func (v Value) Int() int  
func (v Value) Invoke(args ...interface{}) Value  
func (v Value) Length() int  
func (v Value) New(args ...interface{}) Value  
func (v Value) Set(p string, x interface{})  
func (v Value) SetIndex(i int, x interface{})  
func (v Value) String() string  
func (v Value) Type() Type  

Value类型实现是一个结构体类型,结构体并没有导出内部成员。Value类型的Global()构造函数用于返回JavaScript全局对象,Null()构造函数用于构造null对象,Undefined()构造函数用于构造Undefined对象,ValueOf则是将Go语言类型的值转换为JavaScript对象。

ValueOf()构造函数中Go参数类型和JavaScript语言基础类型的对应关系如表所示。

表7-1

js.Value类型值表示一个JavaScript对象,js.TypedArray类型值对应JavaScript中的类型数组,js.Callback类型值对应JavaScript函数对象,nil对应null类型,bool类型对应boolean类型,整数或浮点数统一使用number双精度浮点数类型表示,Go语言的字符串对应JavaScript字符串类型。需要注意的是,JavaScript中的Undefined类型必须通过构造函数创建,而Symbol类型目前无法直接在Go语言中创建(可以通过JavaScript函数创建再返回)。

js.Value类型提供的方法比较多,我们选择比较重要的方法简单介绍下。其中CallInvoke均用于调用函数,区别是前者需要显式传入this参数。而New方法则用于构造类对象。Type方法用于返回对象的类型,InstanceOf方法用于判断是否是某种类型的对象实例。其中GetSet方法可以用于获取和设置对象的成员。

WebAssembly模块的导入函数

在WebAssembly技术规范中,模块可以导入外部的函数或导出内部实现的函数。不过Go语言生成的WebAssembly模块中并无办法定制或扩展导入函数,同时Go语言实现函数也无法以WebAssembly的方式导出供宿主环境使用。Go语言已经明确了需要导入的一组函数,通过学习这些导入函数的实现可以帮助了解Go语言在WebAssembly环境工作的方式。

在查看导入函数之前先看看Go语言生成的WebAssembly模块导出了哪些元素。通过查看Go语言生成的WebAssembly模块,可以发现只导出了两个元素:

(module
  (export "run" (func $_rt0_wasm_js))
  (export "mem" (memory 0))
)

其中run()是启动程序的函数,模块在Go语言包初始化就绪后会执行main.main()函数(如果是命令行含有-test参数则会进入单元测试流程)。而mem是Go语言内存对象。

在初始化WebAssembly模块实例时,需要通过导入函数的方式提供runtime包底层函数的一些实现。通过查看生成的WebAssembly模块文件,可以得知有以下的runtime包函数被导入了:

func runtime.wasmExit(code int32)  
func runtime.wasmWrite(fd uintptr, p unsafe.Pointer, n int32)  
func runtime.nanotime() int64  
func runtime.walltime() (sec int64, nsec int32)  
func runtime.scheduleCallback(delay int64) int32  
func runtime.clearScheduledCallback(id int32)  
func runtime.getRandomData(r []byte)  

其中wasmExit()函数表示退出Go语言实例,wasmWrite()函数向宿主环境的I/O设备输出数据,nanotime()walltime()函数分别对应时间操作,而scheduleCallback()clearScheduledCallback()函数则用于Go回调函数资源的调度和清理,getRandomData()函数用于生成加密标准的随机数据。

导入函数的参考实现由Go语言提供的wasm_exec.js文件定义。这些导入的runtime包的函数都是采用Go语言的内存约定实现,输入的参数必须通过当前栈寄存器相关位置的内存获取,函数执行的返回值也需要根据函数调用规范放到相应的内存位置。

例如,wasmWrite导入函数的实现如下:

this.importObject = {  
  go: {
    // funcwasmWrite(fd uintptr, p unsafe.Pointer, n int32)
    "runtime.wasmWrite": (sp) => {
      const fd = getInt64(sp + 8);
      const p = getInt64(sp + 16);
      const n = mem().getInt32(sp + 24, true);
      fs.writeSync(fd, new Uint8Ar(this._inst.exports.mem.buffer, p, n));
    }
  }
}

JavaScript实现的wasmWrite()函数只有一个sp参数,sp参数表示栈寄存器SP的状态。然后根据寄存器的值从内存相应的位置读取wasmWrite真实函数对应的3个参数。最后通过获取的3个参数调用宿主环境的fs.writeSync()函数实现输出操作。

导入函数除包含runtime包的一些基础函数之外,还包含了syscall/js包的某些底层函数的实现:

funcsyscall/js.stringVal(value string) ref  
funcsyscall/js.valueGet(v ref, p string) ref  
funcsyscall/js.valueSet(v ref, p string, x ref)  
funcsyscall/js.valueIndex(v ref, i int) ref  
funcsyscall/js.valueSetIndex(v ref, i int, x ref)  
funcsyscall/js.valueCall(v ref, m string, args []ref) (ref, bool)  
funcsyscall/js.valueNew(v ref, args []ref) (ref, bool)  
funcsyscall/js.valueLength(v ref) int  
funcsyscall/js.valuePrepareString(v ref) (ref, int)  
funcsyscall/js.valueLoadString(v ref, b []byte)  
funcsyscall/js.valueInstanceOf(v ref, t ref) bool  

这些底层函数和syscall/js对外提供的函数基本是对应的,它们通过JavaScript实现相关的功能。

例如,调用JavaScript函数的Invoke()函数实现如下:

this.importObject = {  
  go: {
    // func valueInvoke(v ref, args []ref) (ref, bool)
    "syscall/js.valueInvoke": (sp) => {
      try {
        const v = loadValue(sp + 8);
        const args = loadSliceOfValues(sp + 16);
        storeValue(sp + 40, Reflect.apply(v, undefined, args));
        mem().setUint8(sp + 48, 1);
      } catch (err) {
        storeValue(sp + 40, err);
        mem().setUint8(sp + 48, 0);
      }
    }
  }
}

Go语言函数参数依然用表示栈寄存器的sp参数传入。在将Go语言格式的参数转为JavaScript语言格式的参数之后,通过Reflect.apply调用JavaScript函数。最后通过mem().setUint8(sp+48, 1)将结果写入内存返回。

此外Go语言生成的WebAssembly模块还导入了一个debug()函数:

this.importObject = {  
  go: {
    "debug": (value) => {
      console.log(value);
    }
  }
}

debug()函数底层通过console.log()实现调试信息输出功能。

通过分析Go语言的运行机制可以发现,Go语言实现的WebAssembly模块必须有自己独立的运行时环境。当main启动时Go运行时环境就绪,当main()函数退出时Go运行时环境销毁。因此导出到JavaScript的Go语言函数只有在main()函数运行时才可以正常使用,当main()函数退出后Go回调函数将无法被使用。

而Go语言的main()函数是一个阻塞执行的函数,因此使用Go语言开发WebAssembly应用时最好在Go语言的main()函数处理消息循环,否则维持阻塞执行的main()函数运行状态将是一个问题。另一个解决思路是在一个独立的WebWorker中运行Go语言生成的WebAssembly模块,其他的JavaScript通过类似RPC的方式调用Go语言函数,而WebWorker之间的通信机制可以作为RPC数据的传输通道。

WebAssembly虚拟机

前面的内容我们主要介绍了如何用Go语言编写WebAssembly模块。本节我们将讨论如何在Node.js浏览器环境之外运行一个WebAssembly模块。

目前在开源社区已经有诸多WebAssembly虚拟机的项目出现。本节我们将演示如何通过Go语言启动一个WebAssembly虚拟机,然后通过虚拟机查看WebAssembly模块的信息并运行模块中导出的函数。

为了便于理解,我们通过WebAssembly汇编语言构造一个简单的模块,模块只有一个add()导出函数:

(module
  (export "add" (func $add))

  (func $add (param i32 i32) (result i32)
    get_local 0
    get_local 1
    i32.add
  )
)

汇编语言对应add.wat文件,可以通过wat2wasm add.wat命令构建一个add.wasm二进制模块。

在Go语言社区中已经存在很多和WebAssembly相关的项目,其中go-interpreter项目提供的虚拟机可以实现WebAssembly二进制模块的加载和执行:

import (  
  "github.com/go-interpreter/wagon/exec"
  "github.com/go-interpreter/wagon/wasm"
)

func main() {  
  f, err := os.Open("add.wasm")
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()

  m, err := wasm.ReadModule(f, nil)
  if err != nil {
    log.Fatal(err)
  }
}

main()函数的开始部分,先读取刚刚生成的add.wasm二进制模块。然后通过wasm.ReadModule()函数解析并加载文件中的模块内容。如果二进制模块还需要导入其他模块,则可以通过第二个参数传入的wasm.ResolveFunc类型的模块加载器实现。

模块成功加载之后就可以查看模块导出了哪些元素:

for name, e := range m.Export.Entries {  
fidx := m.Function.Types[int(e.Index)]  
  ftype := m.Types.Entries[int(fidx)]
fmt.Printf("%s: %#v: %v \n", name, e, ftype)  
}

首先是通过m.Export.Entries遍历全部导出的元素,然后通过每个元素中的e.Index成员确认元素的编号。元素的编号是最重要的信息,通过编号和对应的类型就可以解析元素更详细的信息。在add.wasm模块只导出了一个add()函数,因此我们可以通过编号从m.Function.Types查询信息得到函数的类型信息。

运行这个例子,将看到以下输出:

add: wasm.ExportEntry{FieldStr:"add", Kind:0x0, Index:0x0}: <func [i32 i32] -> [i32]>  

表示导出的是一个名字为add的函数,元素的类型为0表示是一个函数类型,元素对应的内部索引为0。函数的类型为&lt;func [i32 i32] -&gt; [i32]&gt;,表示输入的是两个int32类型的参数,有一个int32类型的返回值。

在获取到模块的导出函数信息之后,就可以根据函数的编号、输入参数类型和返回值类型调用该导出函数了。要调用add()函数,需要基于解析的模块构造一个虚拟机实例,然后通过add()函数的编号调用函数:

vm, err := exec.NewVM(m)  
if err != nil {  
  log.Fatal(err)
}

// result = add(100, 20)
result, err := vm.ExecCode(0, 100, 20)  
if err != nil {  
  log.Fatal(err)
}

fmt.Printf("result(%[1]T): %[1]v\n", result)  

其中exec.NewVM基于前面加载的模块构造一个虚拟机实例,然后vm.ExecCode调用索引为0的函数。索引0对应add()函数,输入参数是两个i32类型的整数。代码中以100和20作为参数调用add()函数,返回的结果就是add()函数的执行结果。

补充说明

Go1.11开始正式支持WebAssembly模块将对两大社区带来影响。首先是基于WebAssembly技术可以将Go语言的整个软件生态资源引入Node.js和浏览器,这将极大地丰富JavaScript语言的软件生态。其次WebAssembly作为一种跨语言的虚拟机标准,也将对Go语言社区带来重大影响。WebAssembly虚拟机很可能最终进化为Go语言中所有第三方脚本语言的公共平台,JavaScript、Lua、Python甚至Java最终可能通过WebAssembly虚拟机进入Go语言生态。我们预测WebAssembly技术将彻底打通不同语言的界限,这是一项非常值得期待的技术。

分享