你好WebAssembly

1,622 阅读6分钟
原文链接: blog.keyboardman.me

概述

上篇介绍了如何成功执行了Go编译的第一个WebAssembly(以下简称wasm)二进制文件,接着进一步测试下Gowasm的能实现的功能。

从Go调用JS

Go的标准库有一个新的包syscall/js,先看下js.go文件。里面定义了个新的类型js.Value,它表示一个JavaScript值。它提供了一个简单的API来操纵任何类型的JavaScript值并与之交互:

  • js.Value.Get()js.Value.Set()检索并设置Object值的属性
  • js.Value.Index()js.Value.SetIndex()检索并设置Array值中的值
  • js.Value.Call()在一个Object值上调用一个方法
  • js.Value.Invoke()调用一个函数值
  • js.Value.New()在代表JS类型的引用上调用new运算符
  • 在相应的Go类型中检索JavaScript值的其他方法(如js.Value.Int()js.Value.Bool()

一个js.ValueOf()函数,它接受任何Go基本类型并返回相应的js.Value

最后是一些有趣的变量:

  • js.Undefinedjsundefined对应的js.Value
  • js.Nulljsnull对应的js.Value
  • js.Global允许访问js全局范围的js.Value

尝试调用下jswindow.alert()将消息其显示在对话框中,而不是发送到console

由于在浏览器中,global就是window,从global中检索alert(),于是有了一个alert类型的js.Value变量,它是对jswindow.alert的引用,在其上使用js.Value.Invoke()。可以发现在将参数传递给Invoke之前不需要调用js.ValueOf(),它接受interface{}参数,并通过调用ValueOf去运行。

package main

import (
"syscall/js"
)

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

现在,当点击按钮时,会弹出一条包含Hello wasm!消息的对话框。

完整代码

从JS调用Go

如上从Go调用js非常简单,接着看callback.go文件。里面定义了一个新的js.Callback类型,它代表一个Gofunc包装以便用作js回调。一个js.NewCallback()函数,它接受一个js.Value切片(并且不返回任何内容)并返回一个js.Callback。并提供一些机制来管理活动回调,以及一个js.Callback.Close()函数,当不再使用回调时必须调用它来释放相应资源。另外还有一个js.NewEventCallback()函数来接受js事件。

先试着做一些简单的事情,从js端触发Gofmt.Println

当前执行wasm二进制文件的run()函数如下所示,需要在wasm_exec.html中进行一些调整,让它能够从Go接收回调并调用它。

async function run() {
console.clear()
await go.run(ist)
inst = await WebAssembly.instantiate(mod, go.importObject)

它启动wasm二进制文件并等待它终止,然后重新实例化它以便下次运行。添加一个新的函数,它将接收并存储Go回调,并在完成后立即解析Promise

let printMessage
let printMessageReceived
let resolvePrintMessageReceived
function setPrintMessage(callback) {
printMessage = callback
resolvePrintMessageReceived()
}

现在调整run()函数以使用回调:

async function run() {
console.clear()
printMessageReceived = new Promise(resolve => {
resolvePrintMessageReceived = resolve
})
const run = go.run(inst)
await printMessageReceived
printMessage('Hello Wasm!')
await run
inst = await WebAssembly.instantiate(mod, go.importObject)

现在Go部分需要创建回调,将其发送给js端并等待它被调用。需要一个channel来通知回调被调用了,然后编写实际的printMessage()``func

var done = make(chan struct{})

func printMessage(args []js.Value) {
message := args[0].String()
fmt.Println(message)
done <- struct{}{}
}

正如所看到的,参数是在js.Value的切片中接收到的,在第一个元素上调用js.Value.String()转化为Gostring来获取message。现在可以在回调中包装这个func,然后调用jssetPrintMessage()函数,就像调用window.alert()时一样,最后就是等待回调被调用,这个很重要,因为回调是在goroutine中执行的,因此主goroutine必须等待回调被调用,否则wasm二进制会提前终止。

callback := js.NewCallback(printMessage)
defer callback.Close()

setPrintMessage := js.Global().Get("setPrintMessage")
setPrintMessage.Invoke(callback)

<-done

完整的Go程序应如下所示:

import (
"fmt"
"syscall/js"
)

var done = make(chan struct{})

func main() {
callback := js.NewCallback(printMessage)
defer callback.Close()

setPrintMessage := js.Global().Get("setPrintMessage")
setPrintMessage.Invoke(callback)
<-done
}

func printMessage(args []js.Value) {
message := args[0].String()
fmt.Println(message)
done <- struct{}{}
}

编辑wasm_exec.html,继续重用wasm_exec.js。现在,当点击按钮时,和之前的hello world类似Hello Wasm!消息被输出在console中。

完整代码

持续运行

js调用Go比从Go调用js更麻烦一些,特别是在js部分。这主要是因为需要等待Go回调传递给js,而且执行完就终止了,如何让wasm不会在调用回调之后终止,却继续运行并接收其他调用?

这一次从Go开始,同样需要创建一个回调并将它发送给js端。并添加一个调用计数器,以便跟踪回调被调用的次数。新的printMessage()函数将打印接收到的消息和调用计数器的值:

var no int

func printMessage(args []js.Value) {
message := args[0].String()
no++
fmt.Printf("Message no %d: %s\n", no, message)
}

创建回调并将其发送给js端与我们前面的示例中完全相同,但是这一次没有完成的channel来通知什么时候终止主goroutine。一种方法是使用空select无限制地阻塞主goroutine。这不是很优雅,wasm二进制文件永远不会完全关闭,并且可能会在浏览器关闭wasm_exec.html时被kill。另一种方法就是监听页面事件来终止主goroutine

创建回调来接收页面的beforeunload事件并通过一个channel通知主goroutine。这次新的beforeUnload()函数将只接受一个js.Value参数用来接受事件:

var beforeUnloadCh = make(chan struct{})

func beforeUnload(event js.Value) {
beforeUnloadCh <- struct{}{}
}

然后可以使用js.NewEventCallback()将它包装在一个回调中,并将其注册到js端:

beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Close()
addEventListener := js.Global.Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)

最后用beforeUnloadCh通道上的接收替换空select

<-beforeUnloadCh
fmt.Println("Bye Wasm !")

最终Go程序如下所示:

package main

import (
"fmt"
"syscall/js"
)

var (
no int
beforeUnloadCh = make(chan struct{})
)

func main() {
callback := js.NewCallback(printMessage)
defer callback.Close() // This is a good practice
setPrintMessage := js.Global.Get("setPrintMessage")
setPrintMessage.Invoke(callback)

beforeUnloadCb := js.NewEventCallback(0, beforeUnload)
defer beforeUnloadCb.Close()
addEventListener := js.Global.Get("addEventListener")
addEventListener.Invoke("beforeunload", beforeUnloadCb)

<-beforeUnloadCh
fmt.Println("Bye Wasm !")
}

func printMessage(args []js.Value) {
message := args[0].String()
no++
fmt.Printf("Message no %d: %s\n", no, message)
}

func beforeUnload(event js.Value) {
beforeUnloadCh <- struct{}{}
}

现在在js部分,这是wasm二进制文件的加载:

const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
});

修改让它在加载后直接启动wasm二进制文件:

let run
(async function() {
const go = new Go()
const { instance } = await WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject)
run = go.run(instance)
})()

通过输入框和按钮来替换我们的Run按钮来触发printMessage()

<input id="messageInput" type="text" value="Hello Wasm!">
<button
onClick="printMessage(document.querySelector('#messageInput').value)"
id="printMessageButton"
disabled
>
Print message
</button>

接收和存储回调的setPrintMessage()函数变得简单了:

let printMessage
function setPrintMessage(callback) {
printMessage = callback
document.querySelector('#printMessageButton').disabled = false
}

现在,当点击Print message按钮时,应该看到输入的信息和计数器输出在console中。然后,如果勾选浏览器控制台的Preserve log选项并刷新页面,则应该在console中看到Bye Wasm !

完整代码

最后

上面用简单的例子和较少的代码测试了syscall/jsAPI,Gojs之间更容易的相互调用了。如果感兴趣的可以做一些基准测试比较下Gowasm与等效的纯js代码的性能。