WASM 基础概念及实践

141 阅读4分钟

WASM 是来取代 javascript 的吗?

WebAssembly 是一种与 JavaScript 不同的语言,但并非旨在取代它。相反,它旨在作为 JavaScript 的补充并与其协同工作,使 Web 开发人员能够充分利用两种语言各自的优势:

  • JavaScript 是一种高级语言,足够灵活和表达力强以编写 Web 应用程序。它有许多优点 — 动态类型、无需编译步骤,并拥有庞大的生态系统,提供强大的框架、库和其他工具。

  • WebAssembly 和 JavaScript 可以灵活的互相调用。

由于这两个语言的特点,页面、操作逻辑用 Javascript 实现,CPU 密集型的计算使用 wasm。

WASM 是如何工作的

C、C++ 和 Rust 等源语言编译成 .wasm 文件,.wasm 文件中存储的是二进制字节码,通过 JavaScript API 加载 .wasm 文件后浏览器会将其编译成可执行机器码。

  • JavaScript 代码能够调用 wasm 导出的代码。
  • 通过把 JavaScript 函数导入到 WebAssembly 实例中,任意的 JavaScript 函数都能被 WebAssembly 代码同步调用。

当下主流的 WebAssembly 生态:

  • 使用 Emscripten 移植一个 C/C++ 应用程序。

  • 直接在汇编层,编写或生成 WebAssembly 代码。

  • 编写 Rust 程序,将 WebAssembly 作为它的输出。

  • 使用 AssemblyScript,它类似于 TypeScript 并且可编译成二进制 WebAssembly 代码。

直接编写 WebAssembly 的实践

我们可以通过编写 wat(WebAssembly Text Format) 代码,再将 wat 转换成 wasm 的方式来直观的编写和理解 WebAssembly 模块。

wat 示例如下:

;; wat 转换工具: https://webassembly.github.io/wabt/demo/wat2wasm/
(module
  ;; 从 JavaScript 导入 consoleLogWithPrefix 方法
  (import "env" "consoleLogWithPrefix" (func $consoleLogWithPrefix (param i32) (param i32)))
  ;; 和 js 共享的内存
  (import "env" "memory" (memory $mem 1))

  ;; 定义一个 helloWorld 方法并返回一个 String
  (func $helloWorld (result i32)
    ;; 定义要返回的字符串
    (i32.const 0)
    (i32.const 11)
    (call $consoleLogWithPrefix)
    ;; 返回字符串的内存地址
    (i32.const 0)
  )

  ;; 导出 helloWorld 方法给 
  (export "helloWorld" (func $helloWorld))

  ;; 定义 helloWorld 方法返回的字符串
  (data (i32.const 0) "hello wasm")
)

Javascript 引入 wasm 示例:

<!DOCTYPE html>
<html>

<head>
  <title>WASM Example</title>
</head>

<body>
  <script>
    // 定义导入给 wasm 的方法
    const consoleLogWithPrefix = (ptr, len) => {
      const memory = new Uint8Array(wasmMemory.buffer, ptr, len);
      const string = new TextDecoder('utf8').decode(memory);
      console.log("[wasm]: " + string);
    }

    const wasmModule = fetch('test.wasm');

    // 初始化内存为 10 页,一页 64KB
    const wasmMemory = new WebAssembly.Memory({ initial: 10 });

    // 内存扩大 10 页
    wasmMemory.grow(10);

    WebAssembly.instantiateStreaming(fetch('test.wasm'), {
      env: {
        consoleLogWithPrefix,
        memory: wasmMemory, // 共享内存
      },
    }).then((result) => {
      const { instance } = result;
      const ptr = instance.exports.helloWorld();
      const memory = new Uint8Array(wasmMemory.buffer, ptr, 10);
      const message = new TextDecoder('utf8').decode(memory);
      console.log(message);
    });
  </script>
</body>

</html>

以为 Emscripten 为例的实践

从上述直接编写 WebAssembly 的实践来看,直接写 wat 文件、自己实现内存管理、自己实现胶水代码是十分繁琐的。同时还要重新了解 WebAssembly 写法,所以在开发领域常用的还是 Emscripten 类的完整工具链,自动帮我们实现和优化这些繁琐的东西。

Emscripten 的工作流程:

  1. Emscripten 首先把 C/C++ 提供给 clang+LLVM。

  2. Emscripten 将 clang+LLVM 编译的结果转换为 Wasm 二进制文件。

  3. 加载 WASM,以及生成 JavaScript 和 WASM 交互的胶水代码,方便互相调用。

编译 C/C++ 为 WebAssembly (结合代码):

参考:developer.mozilla.org/zh-CN/docs/… emsdk(Emscripten 工具链 sdk) 源码,并安装、激活工具链,完成后 emcc 指令就安装好了。

常用指令示例:

// 基于 test.cc 生成 test.wasm 以 test.js(胶水代码)
emcc test.cc -o test.js

// 给 c++ 代码中导入 javascript 方法
emcc test.cc --js-library pkg.js -o test.js

// 按需导出内置的工具方法,如 UTF8ToString 是将 ptr 专成 string 的方法
// 默认只导出必须的,需要啥要在这里设置,详情需看 emscriptern 的文档
emcc test.cc -s EXPORTED_RUNTIME_METHODS='["UTF8ToString"]' -o test.js

// 指定栈内存、堆内存
emcc index.cc -s TOTAL_MEMORY=67108864 -s TOTAL_STACK=3145728 -o index.js

// 指定 64 位,默认 32 位
emcc index.cc -sMEMORY64=2 -o index.js

// 指定内存模式
emcc mem.cc -s MALLOC="emmalloc" -o mem.js
emcc mem.cc -s MALLOC="dlmalloc" -o mem.js

// 设置优化等级, 不仅仅 C++,javascript 胶水代码也会同步压缩
emcc -O1 mem.cc -o mem.js
emcc -O2 mem.cc -o mem.js
emcc -O3 mem.cc -o mem.js

运行在非浏览器环境

由于 wasm 的跨平台特性,它可以运行在任意平台,通过 WASI(WebAssembly System Interface) 和原生系统进行交互实现业务功能:

主流解决方案:wasmedge、wasmtime、wasmer、WAVM 。

WASM 运行时

WebAssembly 运行时由模块加载和解析器执行引擎以及与宿主的系统交互接口(WASI,浏览器环境) 等关键部分组成,类似于 JVM。

参考

developer.mozilla.org/zh-CN/docs/…

github.com/3dgen/cppwa…

docs.wasmtime.dev/lang-rust.h…