wasm系列之初探胶水代码
上一小节,我们完成了一个基本的Hello World的demo,我们知道在产物中有一段hello.js,一个包含了用来在原生 C 函数和 JavaScript/wasm 之间转换的胶水代码的 JavaScript 文件。
今天我们简单分析下这个js文件里面的一些关键代码。
createWasm
var asm = createWasm();
这里是发起调用的地方。
instantiateAsync
优先使用WebAssembly.instantiateStreaming()
方法创建wasm模块的实例;
var dataURIPrefix = 'data:application/octet-stream;base64,';
// Indicates whether filename is a base64 data URI.
function isDataURI(filename) {
// Prefix of data URIs emitted by SINGLE_FILE and related options.
return filename.startsWith(dataURIPrefix);
}
function isFileURI(filename) {
return filename.startsWith('file://');
}
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.
// 编译、实例化之后的结果在这里 result
// result 有两个属性
// module: WebAssembly.Module 对象表示编译完成的 WebAssembly 模块。这个Module能够再次被实例化 或 通过postMessage()共享。
// instance: WebAssembly.Instance 对象包含 WebAssembly 所有公开方法 Exported WebAssembly functions.
receiveInstance(result['instance']);
}
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') {
// 这里主要是做了一些条件判断,比如非nodejs环境、支持fetch、wasmBinaryFile的类型要求
// 步骤1 fetch:先拉取资源
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} */
// 步骤2 WebAssembly.instantiateStreaming() 方法直接从流式底层源编译和实例化 WebAssembly 模块。这是加载 wasm 代码一种非常有效的优化方式。
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);
}
}
实例化后的结果
instantiateArrayBuffer
如果流式创建失败或者失败,则改用WebAssembly.instantiate()
方法创建实例;
// 判断环境的一些方法
var ENVIRONMENT_IS_WEB = typeof window == 'object';
var ENVIRONMENT_IS_WORKER = typeof importScripts == 'function';
// N.b. Electron.js environment is simultaneously a NODE-environment, but
// also a web environment.
var ENVIRONMENT_IS_NODE = typeof process == 'object' && typeof process.versions == 'object' && typeof process.versions.node == 'string';
var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER;
readAsync = (url, onload, onerror) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.responseType = 'arraybuffer';
xhr.onload = () => {
if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0
onload(xhr.response);
return;
}
onerror();
};
xhr.onerror = onerror;
xhr.send(null);
}
// 这里主要是fetch 或者降级的 ajax下载文件
function getBinaryPromise() {
// If we don't have the binary yet, try to to load it asynchronously.
// Fetch has some additional restrictions over XHR, like it can't be used on a file:// url.
// See https://github.com/github/fetch/pull/92#issuecomment-140665932
// Cordova or Electron apps are typically loaded from a file:// url.
// So use fetch if it is available and the url is not a file, otherwise fall back to XHR.
if (!wasmBinary && (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER)) {
if (typeof fetch == 'function'
&& !isFileURI(wasmBinaryFile)
) {
return fetch(wasmBinaryFile, { credentials: 'same-origin' }).then(function(response) {
if (!response['ok']) {
throw "failed to load wasm binary file at '" + wasmBinaryFile + "'";
}
return response['arrayBuffer']();
}).catch(function () {
return getBinary(wasmBinaryFile);
});
}
else {
if (readAsync) {
// fetch is not available or url is file => try XHR (readAsync uses XHR internally)
return new Promise(function(resolve, reject) {
readAsync(wasmBinaryFile, function(response) { resolve(new Uint8Array(/** @type{!ArrayBuffer} */(response))) }, reject)
});
}
}
}
// Otherwise, getBinary should be able to get it synchronously
return Promise.resolve().then(function() { return getBinary(wasmBinaryFile); });
}
function instantiateArrayBuffer(receiver) {
return getBinaryPromise().then(function(binary) {
// 降级的方法 WebAssembly.instantiate() 允许你编译和实例化 WebAssembly 代码
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);
});
}
receiveInstance
成功实例化后的返回值交由receiveInstance()
方法处理。
function receiveInstance(instance, module) {
var exports = instance.exports;
// Module['asm']中保存了WebAssembly实例的导出对象——而导出函数恰是WebAssembly实例供外部调用最主要的入口。
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');
}
异步加载
我们通过之前命名就知道,wasm的下载、编译、实例化的过程都是异步的。
因此如果我们提前使用Module
中的方法,那么这个时候是会报错的,因为wasm 的 runtime尚未准备好。
<script async type="text/javascript" src="hello.js"></script>
<script>
Module._main();
</script>
那么我们可以怎么解呢?其实也很简单。
我们可以注册一个回调,等wasm runtime ready之后呢,就可以执行了。
<script async type="text/javascript" src="hello.js"></script>
<script>
Module.onRuntimeInitialized = function () {
Module._main();
};
</script>
这个在胶水代码中也是可以看到的
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();
}
wasm系列文章最后会同步在github,有需要的可以star关注下。