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)

  1. .c和.cpp文件通过emcc编译成.wasm类型文件
  2. 在JavaScript文件中import.wasm,初始化成实例instance,对外暴露instance.exports的一些接口
  3. 在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对外暴露的接口


参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容