什么是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导出函数的函数名(不含“_”下划线前缀);
- Module.ccall(ident, returnType, argTypes, args)
-
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模块
- WebAssembly.instantiate(bufferSource, importObject): Promise
从 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, "&");
//text = text.replace(/</g, "<");
//text = text.replace(/>/g, ">");
//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
。
参考文档
ArrayBuffer - JavaScript | MDN
Compiling a New C/C++ Module to WebAssembly - WebAssembly | MDN
WebAssembly完全入门–了解wasm的前世今身 - 掘金
https://medium.com/dailyjs/understanding-v8s-bytecode-317d46c94775