# 第1章 Emscripten快速入门

# 1.1 安装Emscripten

# 什么是 WebAssembly

乍一看,可能会以为 WebAssembly 是 Web 版本的汇编语言,但其实是其他语言通过 Emscripteng 工具链编译为 WebAssembly 二进制格式.wasm,进而导入网页中供 JavaScript 调用

虽然 WebAssembly 是一项比较新的技术,属于 HTML5 标准中的一部分,但是现在很多浏览器的最新版本已经支持了。兼容性情况如下:

# 历史由来

2012 年,Mozilla 的工程师 Alon Zakai 在研究 LLVM 编译器时突发奇想:许多 3D 游戏都是用 C / C++ 语言写的,如果能将 C / C++ 语言编译成 JavaScript 代码,它们不就能在浏览器里运行了吗?众所周知,JavaScript 的基本语法与 C 语言高度相似。

于是,他开始研究怎么才能实现这个目标,为此专门做了一个编译器项目 Emscripten。这个编译器可以将 C / C++ 代码编译成 JS 代码,但不是普通的 JS,而是一种叫做 asm.js 的 JavaScript 变体。

# 安装

# Linux 安装

官方教程

# Get the emsdk repo
git clone https://github.com/emscripten-core/emsdk.git

# Enter that directory
cd emsdk

# Fetch the latest version of the emsdk (not needed the first time you clone)
git pull

# Download and install the latest SDK tools.
./emsdk install latest

# Make the "latest" SDK "active" for the current user. (writes .emscripten file)
./emsdk activate latest

# Activate PATH and other environment variables in the current terminal
source ./emsdk_env.sh

适用于新手

  1. 建立第三方环境文件夹

# 这里我一般在系统根目录创建一个文件夹用于安装第三方环境
cd /
mkdir env
  1. 安装

cd /env
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

./emsdk install latest
./emsdk activate latest
# 每次启动执行命令
source /env/emsdk/emsdk_env.sh
  1. 配置自启

每次启动都要执行命令,太麻烦了

echo 'source "/env/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile

# 编译一个 Hello World

  1. c

vi hello.c
#include <stdio.h>
int main(int argc, char ** argv) {
  printf("Hello World\n");
  return 0;
}
# -s WASM=1 — 指定我们想要的wasm输出形式
emcc hello.c -s WASM=0 -o helloc.js
emcc hello.c -s WASM=1 -o helloc.html
node helloc.js
  1. cpp

vi hello.cpp
#include <iostream>
int main() {
  std::cout << "Hello World!" << std::endl;
  return 0;
}
  1. em++

em++ hello.cpp -o hellocpp.js
node hellocpp.js

# 胶水代码

大多数的操作,都围绕全局对象 Module 展开,而该对象正是 Emscripten 程序运行时的核心所在。

# 模块载入和实例化

function instantiateAsync() {
  if (
    !wasmBinary &&
    typeof WebAssembly.instantiateStreaming === "function" &&
    !isDataURI(wasmBinaryFile) &&
    !isFileURI(wasmBinaryFile) &&
    typeof fetch === "function"
  ) {
    // 1. 加载Wasm文件
    return fetch(wasmBinaryFile, { credentials: "same-origin" }).then(function (response) {
      // 2. 加载成功 开始实例化
      var result = WebAssembly.instantiateStreaming(response, info);
      //  3.1 先进行实例化Streaming
      return result.then(receiveInstantiationResult, function (reason) {
        // 3.2 如果实例化Streaming失败则实例化二进制ArrayBuffer
        return instantiateArrayBuffer(receiveInstantiationResult);
      });
    });
  } else {
    // 加载失败
    return instantiateArrayBuffer(receiveInstantiationResult);
  }
}
function receiveInstantiationResult(result) {
  assert(
    Module === trueModule,
    "the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?"
  );
  trueModule = null;
  // 3.1.1 实例化成功,接收实例
  receiveInstance(result["instance"]);
}
function instantiateArrayBuffer(receiver) {
  return getBinaryPromise()
    .then(function (binary) {
      // 3.2.1 使用 WebAssembly.instantiate 实例化
      var result = WebAssembly.instantiate(binary, info);
      return result;
      // 3.2.2 实例化成功执行回调
    })
    .then(receiver, function (reason) {
      err("failed to asynchronously prepare wasm: " + reason);
      abort(reason);
    });
}
  1. 尝试使用 WebAssembly.instantiateStreaming()方法创建 wasm 模块的实例;
  2. 如果流式创建失败,则改用 WebAssembly.instantiate()方法创建实例;
  3. 成功实例化后的返回值交由 receiveInstantiatedSource()方法处理。

# 模块函数封装

为了方便调用,Emscripten 为 C/C++中导出的函数提供了封装,在 hello.js 中,我们可以找到

// Returns the C function with a specified identifier (for C++, you need to do manual name mangling)
function getCFunc(ident) {
  var func = Module["_" + ident]; // closure exported function
  assert(func, "Cannot call unknown function " + ident + ", make sure it is exported");
  return func;
}

TIP

由此可见在 Emscripten 中,C 函数导出时,函数名前会添加下划线“_”

# 异步加载

WebAssembly 实例是通过 WebAssembly.instantiateStreaming()和 WebAssembly.instantiate()方法创建的。 而这两个方法均为异步调用,这意味着.js 文件加载完成时 Emscripten 的 Runtime 并未准备就绪。

function doRun() {
  // ...
  // 初始化Runtime
  initRuntime();
  preMain();
  // 如果有回调onRuntimeInitialized则执行回调onRuntimeInitialized
  if (Module["onRuntimeInitialized"]) Module["onRuntimeInitialized"]();
  if (shouldRunNow) callMain(args);
  postRun();
}

可以看到 Emscripten 编译的 JS,在 Runtime 就绪后,会尝试调用 onRuntimeInitialized 方法,因此我们需要在 Module 初始化前注入这个回调方法

<script>
  Module = {};
  Module.onRuntimeInitialized = function () {
    // Runtime就绪, you can  do something
    Module._main();
  };
</script>
<!-- 然后引入hello.js -->
<script src="hello.js"></script>

# 编译目标和流程

# 编译选择

# 编译asm.js
emcc hello.c -s WASM=0 -o hello_asm.js
# 编译wasm
emcc hello.c -s WASM=1 -o helloc.js

# 编译流程

C/C++代码首先通过 Clang 编译为 LLVM 字节码,然后根据不同的目标编译为 asm.js 或 wasm

# 预览

emcc hello.c -s -o helloc.js -s EXPORT_NAME="'Module1'"
<script>
  Module1 = {};
  Module1.onRuntimeInitialized = function () {
    //do sth.
    Module1._main(); // 在js中调用
  };
</script>
<script src="https://static.gausszhou.top/data/text/wasm/helloc.js"></script>

打印结果如下:

hello, world!
hello, world!