使用 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.exports1,因此需要采用将要输出的函数和变量直接设置成 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 类型,如:structbyterune

对于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;

参考资料


  1. wasm: re-use //export mechanism for exporting identifiers within wasm modules ↩︎