# 第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
适用于新手
- 建立第三方环境文件夹
# 这里我一般在系统根目录创建一个文件夹用于安装第三方环境
cd /
mkdir env
- 安装
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
- 配置自启
每次启动都要执行命令,太麻烦了
echo 'source "/env/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile
# 编译一个 Hello World
- 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
- cpp
vi hello.cpp
#include <iostream>
int main() {
std::cout << "Hello World!" << std::endl;
return 0;
}
- 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);
});
}
- 尝试使用 WebAssembly.instantiateStreaming()方法创建 wasm 模块的实例;
- 如果流式创建失败,则改用 WebAssembly.instantiate()方法创建实例;
- 成功实例化后的返回值交由 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://www.gausszhou.top/static/text/wasm/helloc.js"></script>
打印结果如下:
hello, world!
hello, world!