WebAssembly是一门新的浏览器技术。可以取代一部分js的角色,并且从性能上来说,要比js好很多。WebAssembly目前还处于早期的发展阶段,仍然不够成熟。go语言对WebAssembly的支持也是在1.11版本上刚刚加入。虽然不能投入生产环境,但是可以用来做一些很有意思的事情。
官方WebAssembly的文档:https://github.com/golang/go/wiki/WebAssembly。详细的介绍可以看官方的文档。
一个go WebAssembly的例子
main.go
package main
import "fmt"
func main() {
fmt.Println("hello WebAssembly")
}
编译这个go文件: GOOS=js GOARCH=wasm go build -o main.wasm main.go
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>
<body>
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch("main.wasm"),go.importObject).then((result) => {
go.run(result.instance)
});
</script>
</body>
</html>
这里还需要一个wasm_exec.js
。这个文件是官方提供的,可以理解为是go编译出来的二进制文件和js间的连接桥梁。这个时候打开浏览器,可以看到控制台下有:hello WebAssembly
。
需要说明的是,在go里面的所有STDOUT,都会在浏览器的控制台打印出来。
wasm的初始化过程
WebAssembly.instantiateStreaming(fetch("main.wasm"),go.importObject)
。浏览器执行这样一段代码来加载main.wasm
,并返回一个Promise对象。在加载完毕之后,调用go.run(result.instance)
来执行wasm中的代码。
加载一个wasm文件就是这样的简单。
go WebAssembly如何和js交互
上面的例子非常简单。但是WebAssembly技术绝非这样简单。我们可以在go中轻松的调用js中的方法。比如下面这句话:
js.Global().Get("document").Call("getElementById", "maxCubes").Set("value", 256)
相当于js中的
document.getElementById("maxCubes).setAttribute('value', 256)
同样的,我们也可以在go中定义js方法,这样就可以在js中直接调用了。
api.onMemInitCb = js.NewCallback(func(args []js.Value) {
length := args[0].Int()
api.console.Call("log", "length", length) // 调用js的console.log("length", length)
api.inBuf = make([]uint8, length)
// 拿到这个slice的SliceHeader
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
ptr := uintptr(unsafe.Pointer(hdr.Data))
api.console.Call("log", "ptr:", ptr)
js.Global().Call("gotMem", ptr)
fmt.Println("初始化Mem成功")
})
js.Global().Set("initMem", api.onMemInitCb)
这样,我们就可以在js中直接使用initMem
方法了。这样子,就相当于打通了go和js之间的通道,使得go WebAssembly几乎无所不能。
go和js之间如何通过内存传值。
这个部分是我在看go WebAssembly部分时最关注的。因为大多数时候,我们传参都不仅仅是256
这样的字面值。比如我们在做图片处理的时候,浏览器加载图片后传给wasm去处理,这种时候传递的肯定是一个指向一段内存的指针。
go代码中:
api.onMemInitCb = js.NewCallback(func(args []js.Value) {
length := args[0].Int()
api.console.Call("log", "length", length) // 调用js的console.log("length", length)
api.inBuf = make([]uint8, length)
// 拿到这个slice的SliceHeader
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&api.inBuf))
ptr := uintptr(unsafe.Pointer(hdr.Data))
api.console.Call("log", "ptr:", ptr)
js.Global().Call("gotMem", ptr)
fmt.Println("初始化Mem成功")
})
js代码中:
function gotMem(pointer) {
console.log("pointer", pointer)
memoryBytes.set(bytes, pointer);
// Now the image can be loaded from the slice.
console.log("load image")
loadImage();
}
......
let reader = new FileReader();
reader.onload = (ev) => {
bytes = new Uint8Array(ev.target.result);
initMem(bytes.length);
let blob = new Blob([bytes], {'type': imageType});
document.getElementById("sourceImg").src = URL.createObjectURL(blob);
};
imageType = this.files[0].type;
reader.readAsArrayBuffer(this.files[0]);
上面有两段代码,js部分代码是在加载一张图片,然后转换为Uint8Array
数组bytes,然后调用initMem
方法,传递一个数组bytes的长度作为参数。而initMem
是在go代码中定义的,initMem
负责调用make([]uint8, length)
去初始化需要的内存。然后通过一系列的转换获得申请的内存区域的指针ptr
。然后调用js的gotMem
将这个ptr
传递给js代码。在gotMem
中,memoryBytes.set(bytes, pointer)
这句代码初始化了这块内存。
这里值得指出的是,memoryBytes
是一段在wasm初始化时申请的内存,保存在一个Uint8Array
数组中。如果我们打印出来的话,可以发现这段内存有1G。然后我们的go代码中调用make([]uint8, length)
申请一块内存时,其实是在这段内存中申请的。比如说申请的区域为201883648~(201883648+107003)
。我们只要在js中向memoryBytes
的数组中给这块区域赋值,就把值传递给go的对象了。
再考虑一个问题。1G的初始化内存是不是太大了?这个内存是在编译go代码时由编译工具指定的。但是如果我们使用浏览器(比如chrome)的任务管理器查看这个窗口的占用内存时就会发现,实际占用并不会这么大。
在我的理解中(并不一定正确),这和C语言的malloc
类似。malloc
可以申请大于物理内存的虚拟内存,但是只要你不实际占用这么大内存,是不会有问题的。所以虽然go WebAssembly打印出来有1G的初始化内存,但是如果不是真的会使用,是不会占用这么大物理内存的。
关于初始化内存过大的问题,可以参考这个issues:cmd/compile: wasm code causes out of memory error on Chrome and Firefox for Android
一些关于WebAssembly内存的设计:Finer-grained control over memory
一个用go WebAssembly实现中位切分法处理图片的例子
关于中切分法可以参考我之前的这篇文章:中位切分法颜色量化
可以在这个地址进行预览: 预览地址
在浏览器中运行的效果(这里只展示了FastMap的效果,BestMap会好很多):
代码的github地址是:WebAssembly-MedianCut
里面大多数的代码都是用的我之前的代码。可见go WebAssembly还可以非常舒服的复用之前的代码。