WebAssembly学习笔记 1

WebAssembly学习笔记 1

安装(win)

  1. 下载最新版本 python 并安装 官网 https://www.python.org/downloads/windows/
  2. 下载 emsdk 工具包 git clone https://github.com/juj/emsdk.git 或者访问https://github.com/juj/emsdk直接下载并解压
  3. 安装并激活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
    
  4. 其他环境安装类似——MacOs或者Linux用户只是把emsdk.bat 替换成 emsdk 即可,Docker环境略
    ./emsdk update
    ./emsdk install latest
    ./emsdk activate latest
    source ./emsdk_evn.sh
    
  5. 校验安装
    emcc -v
    
  6. 由于Emscripten v1.37.3才开始正式支持WebAssembly,因此已经安装过Emscripten旧版本的用户最好升级至最新版,本文更新时使用的是最新版本 v3.1.26

Hello World

  1. 新建一个名为 hello.cc 的C源文件(注意文件编码要为utf-8),代码如下:
    #include <stdio.h>
    
    int main() {
    	printf("Hello World ! \n");
    	return 0;
    }
    
  2. 进入控制台,切换至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文件的载入/编译/实例化、运行时初始化等繁杂的工作。
  3. 在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>
    
  4. WebAssembly程序通过网页发布后方可运行,可以使用自己熟悉的nginx/IIS/apache或者任意一种惯用的工具完成即可,打开test.html就能在控制台看到输出了
  5. 以html为目标输出编译与以js为目标输出的结果是一样的,html多生成了一个测试页面而已

胶水代码初探

  1. 打开由Emscripten生成的JavaScript胶水代码hello.js,可以看到大多数的操作都是围绕全局对象Module展开的,此对象正是Emscripten程序运行的核心
  2. 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
}

以上代码其实只完成了这几件事:

  1. 尝试使用 WebAssembly.instantiateStreaming 方法创建wasm模块的实例;
  2. 如果流式创建失败,改用WebAssembly.instantiate()方法创建实例;
  3. 成功实例化后的返回值交由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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值