使用 Go WebAssembly 实现密码学前端应用
WebAssembly
简称 WASM,是继 JavaScript 之后,下一个可以由浏览器直接解析的规范
WebAssembly 一个很重要的突破是允许直接在浏览器中运行二进制的字节码。因此,可以在编译前进行类型检查,加速运行速度。
同时,相对于基于文本的 JavaScript,WebAssembly 为二进制内容,可以降低要传输的文件大小
因此使用 WebAssembly 可以加速代码的执行效率。同时由于 WebAssembly 可以直接操作 DOM,因此原理上完全可以使用纯 C/C++、Go、Rust 写前端!
当然,实际使用中使用后端语言操纵 DOM 的意义可能并不大
另外要注意的是,虽然 WebAssembly 需要编译,但是与 C/C++、Go、Rust 相比,其更类似于 Java,需要运行在浏览器虚拟机中(当然,也可以直接把浏览器看成一个系统,不过没有一些原生应用权限)
Go WebAssembly
使用 Go 开发 WebAssembly,只需要修改环境变量即可
类似于在 Linux 编译 Windows 应用,只需要修改编译时的环境变量即可
GOARCH=wasm GOOS=js go build -o main.wasm main.go
编译出的文件main.wasm
就是需要的文件
不过,目前 Go 编译出的文件需要wasm_exec.js
进行引导,并不能直接运行(未来可能会更新)
直接使用 Go 编译出的文件,大小大概为 2M 左右,而使用 tinyGo 编译出的只有 10+K,相对于更适合于实际使用
目前 Go 还不支持将函数导出至 instance.exports
1,因此需要采用将要输出的函数和变量直接设置成 window.global
的成员来迂回实现。
同时,在这里还要求 Go(WASM) 类似于服务端保持运行,而非类似于普通的模块引入。
这里运行并非执行一个 Go 程序,而是保证 WASM 模块激活,比如在main()
函数中,使用死循环或通道来保证不会结束,从而确保 Go WASM 时刻保留在内存中。
将 Go 程序导出为 WebAssembly
使用下面的语句,即可导出变量和函数
js.Global().Set("key", "123456") js.Global().Set("sum", js.FuncOf( func(this js.Value, args []js.Value) interface{} { // xxx }, ))
类型转换
将已经实现好的代码导出,主要的工作在于类型的转换
大部分的类型都可以自动转换,如果需要显式的 Go 转换到 JavaScript,可以使用js.ValueOf()
来转换
Go | JavaScript |
---|---|
js.Value |
[its value] |
js.Func |
function |
nil |
null |
bool |
boolean |
int ,uint ,floats |
number |
string |
string |
[]interface{} |
new array |
map[string]interface{} |
new object |
在这里,虽然有interface{}
,但是实际上并不包括所有的 Go 类型,如:struct
、byte
、rune
对于struct
类型,要先将其转换为map[string]interface{}
,才能输出成 JavaScript 能识别的类型(否则会导致 panic)
而对于rune
,则要先转换为string
对于底层常用的[]byte
,则要使用专有的函数将其转换为Uint8Array
src := js.Global().Get("Uint8Array").New(len(bytes)) js.CopyBytesToJS(src, bytes)
而反向将Uint8Array
转换为[]byte
,则使用
dst := make([]byte, args[0].Length()) js.CopyBytesToGo(dst, args[0])
在这部分,需要特别注意的是:一定要初始化需要的长度
Go 封装
将上述过程,封装成OhYee/wasm
可以简单地实现函数的输出,并且借助下面的 js 模块即可方便地调用
JS 封装
export interface WASMFuncReturn<T> { success: string; return: T; } export interface WASMPackage<T> { exports: T; exit: () => void; } async function initialWASM<T>( url: string, pkgName: string, callback?: () => void, ): Promise<WASMPackage<T>> { if (!WebAssembly.instantiateStreaming) { // polyfill WebAssembly.instantiateStreaming = async ( resp: Promise<Response>, importObject: Record<string, Record<string, WebAssembly.ImportValue>>, ) => { const source = await (await resp).arrayBuffer(); return await WebAssembly.instantiate(source, importObject); }; } const go = new (window.global as any).Go(); let mod, inst; try { const result = await WebAssembly.instantiateStreaming(fetch(url), go.importObject); mod = result.module; inst = result.instance; go.run(inst); return (window.global as any)[pkgName]; } catch (err) { console.error(err); if (!!callback) callback(); throw err; } } export default initialWASM;