webAssembly学习
webAssembly设计的目的不是为了手写代码而是为诸如C、C++和Rust等低级源语言提供一个高效的编译目标。为客户端app提供了一种在网络平台以接近本地速度的方式运行多种语言编写的代码的方式
优势:JS先通过解释器-优化器,wasm是先编译器-优化器
一些概念
- 模块:表示一个已经被浏览器编译为可执行机器码的WebAssembly二进制代码。一个模块是无状态的,并且像一个二进制大对象(Blob)一样能够被缓存到IndexedDB中或者在windows和workers之间进行共享(通过postMessage()函数)。一个模块能够像一个ES2015的模块一样声明导入和导出。
- 内存:ArrayBuffer,大小可变。本质上是连续的字节数组,WebAssembly的低级内存存取指令可以对它进行读写操作。
- 表格:带类型数组,大小可变。表格中的项存储了不能作为原始字节存储在内存里的对象的引用(为了安全和可移植性的原因)。
- 实例:一个模块及其在运行时使用的所有状态,包括内存、表格和一系列导入值。一个实例就像一个已经被加载到一个拥有一组特定导入的特定的全局变量的ES2015模块。
编译过程:(环境配置参照https://developer.mozilla.org/zh-CN/docs/WebAssembly/C_to_wasm,如果不想安装可以跳到末尾使用studio)
- .c和.cpp文件通过emcc编译成.wasm类型文件
- 在JavaScript文件中import.wasm,初始化成实例instance,对外暴露instance.exports的一些接口
- 在JavaScript中可以同时操作DOM和访问接口
大致示意图
image-20201209135247222.png一个使用的实例,后面会解释:
//在html中:<script async type="text/javascript" src="hello.js"></script>
// 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;
console.log(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");
removeRunDependency('wasm-instantiate');
}
Emscripten编译有很多种选择,其中有两种:
- 编译生成HTML,并添加JavaScript
- 编译仅生成JavaScript
转到一个已经配置过Emscripten编译环境的终端窗口中,进入刚刚保存hello.c文件的文件夹中,然后运行下列命令:emcc hello.c -s WASM=1 -o hello.html
image-20201207163419037.png image-20201207165319307.png image-20201207165354403.png在JavaScript中主要的调用方法为:
var result = Module.ccall('myFunction', // name of C function
null, // return type
null, // argument types
null); // arguments
加载和运行WebAssembly代码
为了在JavaScript中使用WebAssembly,在编译/实例化之前,你首先需要把模块放入内存。
当前并没有内置的方式让浏览器获取模块,唯一的方式是创建一个包含webAssembly模块的二进制代码的ArrayBuffer并且使用WebAssembly.instantiate()
编译它
fetch('module.wasm').then(response => //从'module.wasm'获取response并用arrayBuffer转换为带类型数组的promise
response.arrayBuffer()
).then(bytes => //使用WebAssembly.instantiate一步实现编译和实例化带类型数组
WebAssembly.instantiate(bytes, importObject)
).then(results => {
// Do something with the compiled results!
});
/**
bufferSource
一个包含你想编译的wasm模块二进制代码的 typed array(类型数组) or ArrayBuffer(数组缓冲区)
importObject 可选
一个将被导入到新创建实例中的对象,它包含的值有函数、WebAssembly.Memory 对象等等。编译的模块中,对于每一个导入的值都要有一个与其匹配的属性与之相对应,否则将会抛出 WebAssembly.LinkError。
返回值
解析为包含两个字段的 ResultObject 的一个 Promise:
module: 一个被编译好的 WebAssembly.Module 对象. 这个模块可以被再次实例化,通过 postMessage() 被分享,或者缓存到 IndexedDB。
instance: 一个包含所有 Exported WebAssembly functions的WebAssembly.Instance对象。
*/
//XMLHttpRequest这种更清晰
request = new XMLHttpRequest();
request.open('GET', 'simple.wasm');
request.responseType = 'arraybuffer';
request.send();
request.onload = function() {
var bytes = request.response;
WebAssembly.instantiate(bytes, importObject).then(results => {
results.instance.exports.exported_func();
});
};
JavaScript的Promise相关:https://blog.csdn.net/u013967628/article/details/86569262
indexDB缓存查找:(可以不看)
return openDatabase().then(
db=> {
//module => 相当于一个匿名函数function(module){return WebAssembly.instantiate(module, importObject);},lookupInDatabase函数返回resolve的时候开始执行这个匿名函数
return lookupInDatabase(db).then(
module => {
//找到缓存的${url}
return WebAssembly.instantiate(module, importObject);
},
errMsg => {
//没找到缓存,从url获取,然后WebAssembly.instantiate
/* 从给定的url获取数据,将其编译成一个模块,并且使用给定的导入对象实例化该模块
function fetchAndInstantiate() {
return fetch(url).then(response =>
response.arrayBuffer()
).then(buffer =>
WebAssembly.instantiate(buffer, importObject)
)
}
*/
return fetchAndInstantiate().then(
results =>{
/*触发一个异步操作,从而在给定的数据库中存储给定的wasm模块
function storeInDatabase(db, module) {
var store = db.transaction([storeName], 'readwrite').objectStore(storeName);
var request = store.put(module, url);// key, value形式存储
request.onerror = err => { console.log(`Failed to store in wasm cache: ${err}`) };
request.onsuccess = err => { console.log(`Successfully stored ${url} in wasm cache`) };
}
*/
storeInDatabase(db, results.module);//参数是fetchAndInstantiate结果中的module
return results.instance;//返回fetchAndInstantiate结果中的实例
}
);
}
)
},
errMsg => {
return fetchAndInstantiate().then(
results => results.instance
);
}
);
}
缓存的版本号(可以不用),当任何wasm模块发生更新或者移动到不同的URL,你都需要更新它。
const wasmCacheVersion = 1;
instantiateCachedURL(wasmCacheVersion, 'test.wasm').then(instance =>
console.log("Instance says the answer is: " + instance.exports.answer())
).catch(err =>
console.error("Failure to instantiate: " + err)
);
instance.exports函数的特性:
- length属性 = 参数数量
- name属性 = wasm模块中的索引调用toString()的返回值
- 不支持i64类型
.wasm文件的结构:
使用S-表达式,是一个相对比较平的AST
(module (memory 1) (func)) //根是module,子树1是memory1,子树2是func
函数的结构
( func <signature参数以及函数的返回值> <locals> <body> )
signature和locals可以通过get_local(int)读取:
(func (param i32) (param f32) (local f64)
get_local 0
get_local 1
get_local 2)
get_local 0会得到i32类型的参数
get_local 1会得到f32类型的参数
get_local 2会得到f64类型的局部变量
c语言函数
WASM_EXPORT
int add(int a, int b){
return a+b;
}
.wasm文件部分
(type $t2 (func (param i32 i32) (result i32)))
...
//(export "add")表示这个函数要被export让JavaScript来调用它
(func $add (export "add") (type $t2) (param $p0 i32) (param $p1 i32) (result i32)
get_local $p1 //这里使用了别名,包括上面的$t2代替数字,可读性更强
get_local $p0
i32.add //虚拟机栈来执行指令,声明了返回值,指向到这里时栈中只剩余一个i32值来return
)
..
一个.wasm的示例:
#define WASM_EXPORT __attribute__((visibility("default")))
WASM_EXPORT
int main() {
return 42;
}
WASM_EXPORT //对应后面的export"add"
int add(int a, int b){
return a+b;
}
------------------------------------------------------
(module //所有的都在module内部
(type $t0 (func))
(type $t1 (func (result i32))) //main function
(type $t2 (func (param i32 i32) (result i32))) //add function
(func $__wasm_call_ctors (type $t0))
(func $main (export "main") (type $t1) (result i32)
i32.const 42)
(func $add (export "add") (type $t2) (param $p0 i32) (param $p1 i32) (result i32)
get_local $p1
get_local $p0
i32.add)
(table $T0 1 1 anyfunc) //表格 存储anyfunction
(memory $memory (export "memory") 2) //memory也可以被js访问
(global $g0 (mut i32) (i32.const 66560))
(global $__heap_base (export "__heap_base") i32 (i32.const 66560))
(global $__data_end (export "__data_end") i32 (i32.const 1024)))
如何传递字符串给JavaScript:我们所需要做的就是把字符串在线性内存中的偏移量,以及表示其长度的方法传递出去。
consoleLogString(offset, length) {
var bytes = new Uint8Array(memory.buffer, offset, length);
var string = new TextDecoder('utf8').decode(bytes);
console.log(string);
}
如何传递函数:WebAssembly可以增加一个anyfunc类型("any"的含义是该类型能够持有任何签名的函数),但是,不幸的是,由于安全原因,这个anyfunc类型不能存储在线性内存中。线性内存会把存储的原始内容作为字节暴露出去,并且这会使得wasm内容能够任意的查看和修改原始函数地址,而这在网络上是不被允许的。
解决方案是在一个表格中存储函数引用,然后作为 代替,传递表格索引——它们只是i32类型值。因此,call_indirect的操作数可以是一个i32类型索引值。
(type $return_i32 (func (result i32))) ;;call_indirect调用函数在调用前会检查签名
(func (export "callByIndex") (param $i i32) (result i32)
get_local $i ;;参数压栈
call_indirect $return_i32) ;;根据参数来call对应teble中的函数
APIs:
-
WebAssembly.instantiateStreaming: [IE、Safari、nodejs目前不可用]
var importObject = { imports: { imported_func: arg => console.log(arg) } }; WebAssembly.instantiateStreaming(fetch('simple.wasm'), importObject) .then(obj => obj.instance.exports.exported_func());
-
WebAssembly.instantiate()
:此方法不是获取(fetch)和实例化wasm模块的最具效率方法。 如果可能的话,您应该改用较新的WebAssembly.instantiateStreaming()
方法,该方法直接从原始字节码中直接获取,编译和实例化模块,因此不需要转换为ArrayBuffer
。
importObject :可选, 一个将被导入到新创建实例中的对象,它包含的值有函数、WebAssembly.Memory
对象等等。编译的模块中,对于每一个导入的值都要有一个与其匹配的属性与之相对应,否则将会抛出 WebAssembly.LinkError。
总结,编写c类代码,使用WASM_EXPORT--> 编译成.wasm文件-->在js中import该文件,instantiate得到instance-->使用js操作instance.exports对外暴露的接口
参考:
-
studio(推荐自己尝试):https://webassembly.studio/