wasm系列之初探胶水代码

832 阅读4分钟

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);
  }
}

实例化后的结果

截屏2022-12-10 11.00.52.png

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>

截屏2022-12-10 11.28.48.png

那么我们可以怎么解呢?其实也很简单。

我们可以注册一个回调,等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关注下。

截屏2022-12-10 11.56.19.png