WebAssembly学习笔记 1
- 安装(win)
- Hello World
- 胶水代码初探
安装(win)
- 下载最新版本 python 并安装 官网 https://www.python.org/downloads/windows/
- 下载 emsdk 工具包 git clone https://github.com/juj/emsdk.git 或者访问https://github.com/juj/emsdk直接下载并解压
- 安装并激活Emscripten 在emsdk目录下执行
// 安装并激活 激活之前要确定是否全局激活Emscripten
// 安装激活只需执行一次,如果使用的是非全局激活每次使用时需要执行设置环境变量脚本
emsdk.bat update
emsdk.bat install latest
// 非全局激活,需要在每次使用时设置环境变量
emsdk.bat activate latest
// 设置环境变量(临时)
emsdk_env.bat
// 全局激活需要以管理员身份运行以下命令,全局激活不需要再执行emsdk_env.bat
// 潜在问题:全局激活会将环境变量指向Emscripten内置 Node.js Python Java组件
// 如果系统中已经有这些组件的其他版本会引发冲突,自己打开环境变量解决冲突即可
emsdk.bat activate latest --global
- 其他环境安装类似——MacOs或者Linux用户只是把emsdk.bat 替换成 emsdk 即可,Docker环境略
./emsdk update
./emsdk install latest
./emsdk activate latest
source ./emsdk_evn.sh
- 校验安装
emcc -v
- 由于Emscripten v1.37.3才开始正式支持WebAssembly,因此已经安装过Emscripten旧版本的用户最好升级至最新版,本文更新时使用的是最新版本 v3.1.26
Hello World
- 新建一个名为 hello.cc 的C源文件(注意文件编码要为utf-8),代码如下:
#include <stdio.h>
int main() {
printf("Hello World ! \n");
return 0;
}
- 进入控制台,切换至hello.cc所在的目录,执行以下命令进行编译
// 无目标输出编译 生成 a.out.js a.out.wasm
emcc hello.ccc
// 以js为目标输出编译 生成 hello.js hello.wasm
emcc hello.cc -o hello.js
// 以html为目标输出编译 生成 hello.html hello.js hello.wasm
emcc hello.cc -o hello.html
- 其中wasm后缀文件是C源文件编译后形成的WebAssembly汇编文件;js后缀是Emscripten生成的胶水代码,其中包含了Emscripten的运行环境和.wasm文件的封装,导入js后缀文件即可自动完成.wasm文件的载入/编译/实例化、运行时初始化等繁杂的工作。
- 在hello.js所在目录下新建一个test.html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>hello world</title>
</head>
<body>
<script src="hello.js"></script>
</body>
</html>
- WebAssembly程序通过网页发布后方可运行,可以使用自己熟悉的nginx/IIS/apache或者任意一种惯用的工具完成即可,打开test.html就能在控制台看到输出了
- 以html为目标输出编译与以js为目标输出的结果是一样的,html多生成了一个测试页面而已
胶水代码初探
- 打开由Emscripten生成的JavaScript胶水代码hello.js,可以看到大多数的操作都是围绕全局对象Module展开的,此对象正是Emscripten程序运行的核心
- WebAssembly汇编模块(即.wasm文件)的载入是在createWasm函数中完成的。其核心部分如下:
// Create the wasm instance.
// Receives the wasm imports, returns the exports.
function createWasm() {
// prepare imports
var info = {
'env': asmLibraryArg,
'wasi_snapshot_preview1': asmLibraryArg,
};
// Load the wasm module and create an instance of using native support in the JS engine.
// handle a generated wasm instance, receiving its exports and
// performing other necessary setup
/** @param {WebAssembly.Module=} module*/
function receiveInstance(instance, module) {
var exports = instance.exports;
Module['asm'] = exports;
wasmMemory = Module['asm']['memory'];
assert(wasmMemory, "memory not found in wasm exports");
// This assertion doesn't hold when emscripten is run in --post-link
// mode.
// TODO(sbc): Read INITIAL_MEMORY out of the wasm file in post-link mode.
//assert(wasmMemory.buffer.byteLength === 16777216);
updateGlobalBufferAndViews(wasmMemory.buffer);
wasmTable = Module['asm']['__indirect_function_table'];
assert(wasmTable, "table not found in wasm exports");
addOnInit(Module['asm']['__wasm_call_ctors']);
removeRunDependency('wasm-instantiate');
}
// we can't run yet (except in a pthread, where we have a custom sync instantiator)
addRunDependency('wasm-instantiate');
// Prefer streaming instantiation if available.
// Async compilation can be confusing when an error on the page overwrites Module
// (for example, if the order of elements is wrong, and the one defining Module is
// later), so we save Module and check it later.
var trueModule = Module;
function receiveInstantiationResult(result) {
// 'result' is a ResultObject object which has both the module and instance.
// receiveInstance() will swap in the exports (to Module.asm) so they can be called
assert(Module === trueModule, 'the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?');
trueModule = null;
// TODO: Due to Closure regression https://github.com/google/closure-compiler/issues/3193, the above line no longer optimizes out down to the following line.
// When the regression is fixed, can restore the above USE_PTHREADS-enabled path.
receiveInstance(result['instance']);
}
function instantiateArrayBuffer(receiver) {
return getBinaryPromise().then(function(binary) {
return WebAssembly.instantiate(binary, info);
}).then(function (instance) {
return instance;
}).then(receiver, function(reason) {
err('failed to asynchronously prepare wasm: ' + reason);
// Warn on some common problems.
if (isFileURI(wasmBinaryFile)) {
err('warning: Loading from a file URI (' + wasmBinaryFile + ') is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing');
}
abort(reason);
});
}
function instantiateAsync() {
if (!wasmBinary &&
typeof WebAssembly.instantiateStreaming == 'function' &&
!isDataURI(wasmBinaryFile) &&
// Don't use streaming for file:// delivered objects in a webview, fetch them synchronously.
!isFileURI(wasmBinaryFile) &&
// Avoid instantiateStreaming() on Node.js environment for now, as while
// Node.js v18.1.0 implements it, it does not have a full fetch()
// implementation yet.
//
// Reference:
// https://github.com/emscripten-core/emscripten/pull/16917
!ENVIRONMENT_IS_NODE &&
typeof fetch == 'function') {
return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) {
// Suppress closure warning here since the upstream definition for
// instantiateStreaming only allows Promise<Repsponse> rather than
// an actual Response.
// TODO(https://github.com/google/closure-compiler/pull/3913): Remove if/when upstream closure is fixed.
/** @suppress {checkTypes} */
var result = WebAssembly.instantiateStreaming(response, info);
return result.then(
receiveInstantiationResult,
function(reason) {
// We expect the most common failure cause to be a bad MIME type for the binary,
// in which case falling back to ArrayBuffer instantiation should work.
err('wasm streaming compile failed: ' + reason);
err('falling back to ArrayBuffer instantiation');
return instantiateArrayBuffer(receiveInstantiationResult);
});
});
} else {
return instantiateArrayBuffer(receiveInstantiationResult);
}
}
// User shell pages can write their own Module.instantiateWasm = function(imports, successCallback) callback
// to manually instantiate the Wasm module themselves. This allows pages to run the instantiation parallel
// to any other async startup actions they are performing.
// Also pthreads and wasm workers initialize the wasm instance through this path.
if (Module['instantiateWasm']) {
try {
var exports = Module['instantiateWasm'](info, receiveInstance);
return exports;
} catch(e) {
err('Module.instantiateWasm callback failed with error: ' + e);
return false;
}
}
instantiateAsync();
return {}; // no exports yet; we'll fill them in later
}
以上代码其实只完成了这几件事:
- 尝试使用 WebAssembly.instantiateStreaming 方法创建wasm模块的实例;
- 如果流式创建失败,改用WebAssembly.instantiate()方法创建实例;
- 成功实例化后的返回值交由receiveInstantiationResult,receiveInstantiationResult调用了receiveInstance()方法。receiveInstance方法中的执行指令如下:
var exports = instance.exports;
Module['asm'] = exports;
将wasm模块实例的导出对象传给Module的子对象asm。可以手动添加打印实例看看输出
上述一系列代码运行后,Module['asm]中保存了WebAssembly实例的导出对象,而导出函数恰是WebAssembly实例供外部调用的最主要入口。
3. 导出函数封装
为了方便调用,Emscripten为C/C++中的导出函数提供了封装。在hello.js中,我们可用找到大量这样的封装代码
/** @type {function(...*):?} */
var ___wasm_call_ctors = Module["___wasm_call_ctors"] = createExportWrapper("__wasm_call_ctors");
/** @type {function(...*):?} */
var _main = Module["_main"] = createExportWrapper("main");
/** @type {function(...*):?} */
var ___errno_location = Module["___errno_location"] = createExportWrapper("__errno_location");
/** @type {function(...*):?} */
var _fflush = Module["_fflush"] = createExportWrapper("fflush");
...
/** @param {boolean=} fixedasm */
function createExportWrapper(name, fixedasm) {
return function() {
var displayName = name;
var asm = fixedasm;
if (!fixedasm) {
asm = Module['asm'];
}
assert(runtimeInitialized, 'native function `' + displayName + '` called before runtime initialization');
if (!asm[name]) {
assert(asm[name], 'exported native function `' + displayName + '` not found');
}
return asm[name].apply(null, arguments);
};
}
在Emscripten中,C函数导出时,函数名称前会添加下划线。上述代码中的_main()对应的是hello.cc中的main函数。我们可以手动在控制台中执行Module._main()和_main(),都会调用hello.cc中的main函数
4. 异步加载
WebAssembly实例是通过 WebAssembly.instantiateStreaming 或者 WebAssembly.instantiate() 方法创建的,而这两个方法均为异步调用,这意味着.js文件加载完成时Emscripten的运行时并未准备就绪。倘若修改test.html,载入hello.js后立即执行Module._main()控制台会报错
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>hello world</title>
</head>
<body>
<script src="hello.js"></script>
<script>
Module._main();
</script>
</body>
</html>
控制台输出报错信息
Uncaught RuntimeError: Aborted(Assertion failed: native function ‘main’ called before runtime initialization)
解决这一问题需要建立一种运行时准备就绪的通知机制,为此Emscripten提供了多种解决方案,最简单的方法是在main()函数中发出通知。但是对于多数纯功能的模块来说main函数不是必须的,因此常使用的方法是不依赖main函数的onRuntimeInitialized回调,具体使用方法如下
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>hello world</title>
</head>
<body>
<script>
Module = {};
Module.onRuntimeInitialized = function() {
Module._main();
}
</script>
<script src="hello.js"></script>
</body>
</html>
基本思路就是在Module初始化之前,向Module中注入一个名为onRuntimeInitialized的方法,当Emscripten的运行时准备就绪时就会回调该方法。在hello.js中我们可以看到如下代码:
/** @type {function(Array=)} */
function run(args) {
args = args || arguments_;
if (runDependencies > 0) {
return;
}
stackCheckInit();
preRun();
// a preRun added a dependency, run will be called later
if (runDependencies > 0) {
return;
}
function doRun() {
// run may have just been called through dependencies being fulfilled just in this very frame,
// or while the async setStatus time below was happening
if (calledRun) return;
calledRun = true;
Module['calledRun'] = true;
if (ABORT) return;
initRuntime();
preMain();
if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized']();
if (shouldRunNow) callMain(args);
postRun();
}
if (Module['setStatus']) {
Module['setStatus']('Running...');
setTimeout(function() {
setTimeout(function() {
Module['setStatus']('');
}, 1);
doRun();
}, 1);
} else
{
doRun();
}
checkStackCookie();
}
其中这两行代码就能看出运行时的调用情况,如果有定义onRuntimeInitialized就会调用,并且也揭开了Hello World的执行过程——引用了hello.js并且有main函数就会默认调用一次main函数——callMain
if (Module['onRuntimeInitialized']) Module['onRuntimeInitialized']();
if (shouldRunNow) callMain(args);