入门级浅尝WebAssembly

什么是WebAssembly

JS的弊端

JS代码在V8引擎会经历如下过程:
在这里插入图片描述

  • JS源码经过Parse解析器转换成AST;
  • 经过Ignition解释器,将AST转成字节码;
  • 通过Turbofan将字节码转成CPU直接执行的代码。

期间执行比较频繁的代码称为热代码,其编译后的机器码会被缓存下来,等到下次执行这段代码时直接执行机器码。但是JS是动态类型语言,如果变量类型改变了,这个时候就需要Turbofan反优化,将热代码回退给Iginition解释器才解释一次。

asm.js

于是 Mozilla 提出了 asm.js,它是js的子集,即asm.js的代码一直符合js的语法规范,但是反之则不一定。但是要注意的是,asm.js是一个编译目标,而不是手写出来的(你如果是无情的编码机器人当我没说)。

比如下面的文件,| 0 表示整型,+ 表示双精度浮点数。

function foo() {
    'use asm';
    let myInt = 0 | 0; // 表示整型0
    let myDouble = +1.1; // 表示双精度浮点数1.1
}

WebAssembly

asm.js跟TS一样,最终变成js执行,实际上还是要经过Parser等过程,但是思路挺不错的,于是Google、MicroSoft、Apple都参与其中,共建WebAssembly。

WebAssembly简称wasm,它是一种低级的类汇编语言——字节码标准,具有紧凑的二进制格式,具备接近原生的性能运行。可以依赖Emscripten等编译器将C++/Golang/Rust/Kotlin等强类型语言编译成为WebAssembly字节码(.wasm文件)。

实战

  • 安装CMake、Emscripten
brew install cmake emscripten
  • 编写C文件hello.c
#include <stdio.h>

int main() {
    printf("Hello World\n");
}
  • 执行Emscripten编译器,将c文件转变成wasm和胶水文件
emcc hello.c -s WASM=1 -o hello.html

-s WASM=1:表示编译目标为wasm,否则默认为asm.js

-o hello.html:表示输出一个可运行wasm和胶水文件的网页

  • 输出结果

一个wasm格式的文件hello.wasm

一个连接js/wasm和c函数的胶水文件hello.js

一个网页文件hello.html

胶水文件

胶水文件主要用户js和其他语言可以互相访问(执行代码),下面以C语言为例:

  • 让js支持访问c语言函数,对外暴露了两个方法:

    • Module.ccall(ident, returnType, argTypes, args)
      • ident :C导出函数的函数名(不含“_”下划线前缀);
  • returnType :C导出函数的返回值类型,可以为'boolean''number''string''null',分别表示函数返回值为布尔值、数值、字符串、无返回值;

  • argTypes :C导出函数的参数类型的数组。参数类型可以为'number''string''array',分别代表数值、字符串、数组;

  • args :参数数组。

  • Module.cwrap(ident, returnType, argTypes)

    • ident :C导出函数的函数名(不含“_”下划线前缀);
  • returnType :C导出函数的返回值类型,可以为'boolean''number''string''null',分别表示函数返回值为布尔值、数值、字符串、无返回值;

  • argTypes :C导出函数的参数类型的数组。参数类型可以为'number''string''array',分别代表数值、字符串、数组;

 // C calling interface.
 /** @param {string|null=} returnType
 @param {Array=} argTypes
 @param {Arguments|Array=} args
 @param {Object=} opts */
function ccall(ident, returnType, argTypes, args, opts) {
  // For fast lookup of conversion functions
  var toC = {
    'string': function(str) {
      var ret = 0;
      if (str !== null && str !== undefined && str !== 0) { // null string
        // at most 4 bytes per UTF-8 code point, +1 for the trailing '\0'
        var len = (str.length << 2) + 1;
        ret = stackAlloc(len);
        stringToUTF8(str, ret, len);
      }
      return ret;
    },
    'array': function(arr) {
      var ret = stackAlloc(arr.length);
      writeArrayToMemory(arr, ret);
      return ret;
    }
  };

  function convertReturnValue(ret) {
    if (returnType === 'string') return UTF8ToString(ret);
    if (returnType === 'boolean') return Boolean(ret);
    return ret;
  }

  var func = getCFunc(ident);
  var cArgs = [];
  var stack = 0;
  assert(returnType !== 'array', 'Return type should not be "array".');
  if (args) {
    for (var i = 0; i < args.length; i++) {
      var converter = toC[argTypes[i]];
      if (converter) {
        if (stack === 0) stack = stackSave();
        cArgs[i] = converter(args[i]);
      } else {
        cArgs[i] = args[i];
      }
    }
  }
  var ret = func.apply(null, cArgs);
  function onDone(ret) {
    if (stack !== 0) stackRestore(stack);
    return convertReturnValue(ret);
  }

  ret = onDone(ret);
  return ret;
}

 /** @param {string=} returnType
 @param {Array=} argTypes
 @param {Object=} opts */
function cwrap(ident, returnType, argTypes, opts) {
  return function() {
    return ccall(ident, returnType, argTypes, arguments, opts);
  }
}

Module['ccall'] = ccall;
Module['cwrap'] = cwrap;
  • 让c语言可以访问js的对象:
    • WebAssembly.instantiate(bufferSource, importObject): Promise
      • 使用wasm二进制代码,编译和实例化 WebAssembly 代码
    • WebAssembly.instantiate(module, importObject): Promise<WebAssembly.Instance>
      • 使用实例化的 WebAssembly.Module 对象,编译和实例化 WebAssembly 代码
    • WebAssembly.instantiateStreaming(source, importObject): Promise
      • 直接从流式底层源编译和实例化WebAssembly模块

Understanding the JS API - WebAssembly 里可以看到 importObejct会被转义,可被低级语言使用。

// simple.wasm,
(module
  // 编译导入对象中的i方法
  (func $i (import "imports" "i") (param i32))
  // 导出e方法,实际执行导入的i方法,入参被设置为42
  (func (export "e")
    i32.const 42
    call $i))

在实例化的时候传入importObject,供低级语言内部使用。(想象成JSONP)

var importObject = { imports: { i: arg => console.log(arg) } };

fetch('simple.wasm').then(response => response.arrayBuffer())
.then(bytes => instantiate(bytes, importObject))
.then(instance => instance.exports.e());

最终会在控制台输出42。

hello.js

 /** @type {function(...*):?} */
var _main = Module["_main"] = createExportWrapper("main");

function callMain(args) {
  assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on Module["onRuntimeInitialized"])');
  assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called');

  var entryFunction = Module['_main'];

  args = args || [];

  var argc = args.length+1;
  var argv = stackAlloc((argc + 1) * 4);
  HEAP32[argv >> 2] = allocateUTF8OnStack(thisProgram);

  for (var i = 1; i < argc; i++) {
    HEAP32[(argv >> 2) + i] = allocateUTF8OnStack(args[i - 1]);
  }
  HEAP32[(argv >> 2) + argc] = 0;

  try {
    var ret = entryFunction(argc, argv);
    // In PROXY_TO_PTHREAD builds, we should never exit the runtime below, as
    // execution is asynchronously handed off to a pthread.
    // if we're not running an evented main loop, it's time to exit
    exit(ret, /* implicit = */ true);
    return ret;
  }
  catch (e) {
    return handleException(e);
  } finally {
    calledMain = true;
  }
}

hello.html

Module = {
  // ...
  print: (function() {
      var element = document.getElementById('output');
      if (element) element.value = ''; // clear browser cache
      return function(text) {
        if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
        // These replacements are necessary if you render to raw HTML
        //text = text.replace(/&/g, "&amp;");
        //text = text.replace(/</g, "&lt;");
        //text = text.replace(/>/g, "&gt;");
        //text = text.replace('\n', '<br>', 'g');
        console.log(text);
        if (element) {
          element.value += text + "\n";
          element.scrollTop = element.scrollHeight; // focus on bottom
        }
      };
    })(),
}

快速启动一个静态资源服务器(默认端口8000,这里是8888):

Python2:python -m SimpleHTTPServer 8888
Python3:python3 -m http.server 8888

在这里插入图片描述

在控制输入_main(),可以直接看到输出Hello World

参考文档

JavaScript 引擎 V8 执行流程概述

v8 执行 js 的过程 - 政采云前端团队

理解 V8 的字节码「译」

《C/C++面向WebAssembly编程简介》

20分钟上手 webAssembly - 掘金

ArrayBuffer - JavaScript | MDN

Compiling a New C/C++ Module to WebAssembly - WebAssembly | MDN

WebAssembly完全入门–了解wasm的前世今身 - 掘金

https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775

猜你喜欢

转载自blog.csdn.net/sinat_36521655/article/details/121892744