什么是WebAssembly?
- 是一個可移植、體積小缩焦、加載快并且兼容 Web 的全新格式
- wasm是體積小且加載快的二進制格式址儒, 其目標就是充分發(fā)揮硬件能力以達到原生執(zhí)行效率
- 運行在一個沙箱化的執(zhí)行環(huán)境中,甚至可以在現(xiàn)有的 JavaScript 虛擬機中實現(xiàn)敞葛。在web環(huán)境中,WebAssembly將會嚴格遵守同源策略以及瀏覽器安全策略。
- 中被設(shè)計成無版本毒租、特性可測試、向后兼容的漓骚。WebAssembly 可以被 JavaScript 調(diào)用蝌衔,進入 JavaScript 上下文榛泛,也可以像 Web API 一樣調(diào)用瀏覽器的功能。當然噩斟,WebAssembly 不僅可以運行在瀏覽器上曹锨,也可以運行在非web環(huán)境下。
- 支持語言: c/c++ 剃允、rust沛简、原始的webassembly S表達式文本、AssemblyScript(TypeScript-like)斥废、Go 椒楣。 其他語言(python,java,scala,kotlin等諸多語言也有工具實驗性支持)。
webassembly官網(wǎng)
mdn相關(guān)文檔
瀏覽器支持情況
webassembly 特性
- 計算速度快牡肉,性能高捧灰,編譯成wasm后的代碼性能接近原生
- 可以使用c++/c/go 眾多的三方庫來前端處理復雜任務(wù)與計算 (opencv、FFmpeg等)
- 不需要垃圾回收機制统锤,手動管理內(nèi)存
- 通過wasm的內(nèi)存與JavaScript 通信
應(yīng)用場景
- 將 C毛俏、C++、Rust 等語言編寫的程序移植到瀏覽器
- 圖形圖像處理領(lǐng)域(如OCR識別)饲窿,如頁游煌寇、數(shù)據(jù)可視化等
- 音視頻編解碼識別、AI等等
- 解壓逾雄、壓縮 等對性能要求高的需求
webassembly 基本概念及使用
幾個概念
-
Module
一個“代碼單元”阀溶。包含編譯好的二進制代碼⊙挥荆可以高效的緩存银锻、共享。未來可以像一個ES2015模塊一樣導入/導出 -
Memory
內(nèi)存辽故,連續(xù)的徒仓,可變大小的字節(jié)數(shù)組緩沖區(qū)√芄福可以理解為一個“堆” -
Table
連續(xù)的掉弛,可變大小的類型數(shù)組緩沖區(qū) 現(xiàn)在table只支持函數(shù)引用類型,可以類比為一個“椢棺撸” -
Instance
在Module基礎(chǔ)上殃饿,包含所有運行時所需狀態(tài)的實例,如果把Module類比為一個cpp文件芋肠,那么Instance就是鏈接了dll的exe文件
c代碼-->借助工具編譯為wasm
#include<stdio.h>
void fibonacci(int n)
{
int first = 0, second = 1, next;
for (int i = 0; i < n; i++)
{
next = first + second;
first = second;
second = next;
}
}
load wasm 文件乎芳,獲取 instance 實例
function load(path) {
return fetch(path)
// 獲取二進制buffer
.then(res => res.arrayBuffer())
// 編譯&實例化,導入js對象
.then(bytes => WebAssembly.instantiate(bytes, importObj))
// 返回實例
.then(res => res.instance)
}
從instance中獲取導出的文件
const fibonacci_wasm = instance.exports._fibonacci
上述代碼重復計算一百萬次斐波那契數(shù)列46項(47項會溢出),結(jié)果如下:
- C:3ms
- JS: 70ms
- WebAssembly:11ms
** 引用自 - [1] https://blog.csdn.net/m549393829/article/details/81839822
emscripten 封裝好了上述獲取實例的方法使用更簡單奈惑,如 emcc 編譯后的js文件 abc.js
import myModule from "../asm/abc.js";
myModule().then(zModule => {
this.zModule = zModule;
});
// 此時 zModule 就包含你導出的c方法及吭净,emscripten 導出的常用方法
使用Emscripten編譯并使用流程
- 安裝Emscripten 環(huán)境 (略) 詳見
emscripten官網(wǎng)
- 閱讀C/C++ 三方庫文檔
- 編寫C函數(shù),用于調(diào)用庫中的方法
- emcc命令編譯c語言為wasm 及 封裝的js膠水代碼
- 編寫膠水代碼肴甸,用于C語言與js通信寂殉,js中調(diào)用c函數(shù) (直接調(diào)用只能傳遞int值,傳遞其他類型值需要借助內(nèi)存處理)
Vue中使用
方式一: 將wasm 與 js文件放到如cdn或服務(wù)器
方式二: 將wasm 與 js封裝 為庫原在,發(fā)布到 npm使用
方式三: 本地使用友扰,
- wasm文件并不會被webpack打包進dist,使用 url-loader 將 wasm 只作文靜態(tài)文件路徑 注意:import 下wasm文件確保被打包進dist
- 可以放在vue的public文件下
Emscripten 編譯命令
emcc -
優(yōu)化flag庶柿,它們-O0村怪,-O1,-O2浮庐,-Os甚负,-Oz,-O3审残。 對應(yīng)不同優(yōu)化級別
-s OPTION=VALU 傳給編譯器的所有涉及到JavaScript代碼生成的選項
emcc simple/helloword.c -o output/hellow.js \
-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall','abc,'_malloc','_free']" \ #導出的函數(shù)腊敲,abc為自己寫的c語中的函數(shù),其他為emscriten自帶的
-s MODULARIZE=1 # 模塊化维苔,生成閉包函數(shù)
-s ENVIRONMENT="web" \
-s ALLOW_MEMORY_GROWTH=1 \ #開啟可變內(nèi)存
-s FORCE_FILESYSTEM=1 # 強制啟用em的虛擬文件系統(tǒng)
-s RESERVED_FUNCTION_POINTERS #保留函數(shù)表指針
JS 類型化數(shù)組 與 buffer,與Blob
ArrayBuffer是一個構(gòu)造函數(shù),可以分配一段可以存放數(shù)據(jù)的連續(xù)內(nèi)存區(qū)域
var buffer = new ArrayBuffer(16); //創(chuàng)建一個連續(xù)16字節(jié)的內(nèi)存緩沖
視圖類型 | 說明 | 字節(jié)大小 |
---|---|---|
Uint8Array | 8位無符號整數(shù) | 1字節(jié) |
Int8Array | 8位有符號整數(shù) | 1字節(jié) |
Uint8ClampedArray | 8位無符號整數(shù)(溢出處理不同) | 1字節(jié) |
Uint16Array | 16位無符號整數(shù) | 2字節(jié) |
Int16Array | 16位有符號整數(shù) | 2字節(jié) |
Uint32Array | 32位無符號整數(shù) | 4字節(jié) |
Int32Array | 32位有符號整數(shù) | 4字節(jié) |
Float32Array | 32位IEEE浮點數(shù) | 4字節(jié) |
Float64Array | 64位IEEE浮點數(shù) | 8字節(jié) |
// 創(chuàng)建一個視圖懂昂,此視圖把緩沖內(nèi)的數(shù)據(jù)格式化為一個32位(4字節(jié))有符號整數(shù)數(shù)組
var int32View = new Int32Array(buffer);
// 我們可以像普通數(shù)組一樣訪問該數(shù)組中的元素
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
-
Blob
對象表示一個不可變介时、原始數(shù)據(jù)的類文件對象。它的數(shù)據(jù)可以按文本或二進制的格式進行讀取凌彬,也可以轉(zhuǎn)換成ReadableStream
來用于數(shù)據(jù)操作沸柔。Blob 表示的不一定是JavaScript原生格式的數(shù)據(jù)。
File
接口基于Blob
铲敛,繼承了 blob 的功能并將其擴展使其支持用戶系統(tǒng)上的文件褐澎。
var aBlob = new Blob( array, options )
/** 例如 */
const blob = new Blob([int32View], {
type: "application/zip"
});
array 是一個由
ArrayBuffer
,ArrayBufferView
,Blob
,DOMString
等對象構(gòu)成的Array
,或者其他類似對象的混合體伐蒋,它將會被放進Blob
工三。DOMStrings會被編碼為UTF-8。-
options
是一個可選的
BlobPropertyBag
字典先鱼,它可能會指定如下兩個屬性:
-
type
俭正,默認值為""
,它代表了將會被放入到blob中的數(shù)組內(nèi)容的MIME類型焙畔。 -
endings
掸读,默認值為"transparent"
,用于指定包含行結(jié)束符\n
的字符串如何被寫入。 它是以下兩個值中的一個:"native"
儿惫,代表行結(jié)束符會被更改為適合宿主操作系統(tǒng)文件系統(tǒng)的換行符澡罚,或者"transparent"
,代表會保持blob中保存的結(jié)束符不變
-
1. 在C中調(diào)用JS函數(shù)之a(chǎn)ddFunction
Emscripten提供了多種在C環(huán)境調(diào)用JavaScript的方法肾请,包括:
JavaScript函數(shù)注入(更準確的描述為:“Implement C API in JavaScript”留搔,既在JavaScript中實現(xiàn)C函數(shù)API)
-
使用addFunction將函數(shù)指針傳到C代碼中調(diào)用
第一個字符表示函數(shù)的返回類型,其余字符表示參數(shù)類型
-
'v'
: void type -
'i'
: 32-bit integer type -
'j'
: 64-bit integer type (currently does not exist in JavaScript) -
'f'
: 32-bit float type -
'd'
: 64-bit float type
-
☆☆☆ webassembly 與 c 的通信
js 與 c的通信主要借助 webassembly 中的內(nèi)存完成筐喳,基本思想是將一段數(shù)據(jù)的內(nèi)存地址與長度傳遞到C中催式,c根據(jù)地址和長度取出內(nèi)容。
ccall 與 ccwrap
如果直接使用C導出的函數(shù)避归,只能傳遞 number 的數(shù)據(jù)荣月,如果使用了其他類型的需要借助ccall/cwrap
以下摘自https://emscripten.org/docs/api_reference/preamble.js.html
ccall(ident,returnType梳毙,argTypes哺窄,args,opts )
從JavaScript調(diào)用已編譯的C函數(shù)账锹。
該函數(shù)從JavaScript執(zhí)行已編譯的C函數(shù)萌业,并返回結(jié)果。C ++名稱處理意味著無法調(diào)用“正臣榧恚”的C ++函數(shù)生年。該函數(shù)必須在.c文件中定義,或者是使用定義的C ++函數(shù)廓奕。extern "C"
returnType并argTypes讓您指定參數(shù)的類型和返回值抱婉。可能的類型是"number"桌粉,"string"蒸绩,"array",或"boolean"铃肯,其對應(yīng)于相應(yīng)的JavaScript類型患亿。使用"number"任何數(shù)值類型或C指針,string對于Cchar*表示字符串押逼,"boolean"對于一個布爾類型步藕,"array"為JavaScript陣列和類型數(shù)組,含有8位整數(shù)數(shù)據(jù)-即宴胧,數(shù)據(jù)被寫入的8位整數(shù)的C數(shù)組; 特別是如果您在此處提供類型化數(shù)組漱抓,則它必須是Uint8Array或Int8Array。如果要接收其他類型的數(shù)據(jù)數(shù)組恕齐,則可以手動分配內(nèi)存并對其進行寫入乞娄,然后在此處提供一個指針(作為"number"瞬逊,因為指針只是數(shù)字)。
// Call C from JavaScript
var result = Module.ccall('c_add', // name of C function
'number', // return type
['number', 'number'], // argument types
[10, 20]); // arguments
總結(jié): 傳遞的參數(shù) 只能為 字符串仪或,數(shù)字确镊,及Uint8Array或Int8Array
cwrap(ident,returnType范删,argTypes )
返回C函數(shù)的本機JavaScript包裝器蕾域。
這類似于,但是返回一個JavaScript函數(shù)到旦,該函數(shù)可以根據(jù)需要多次重復使用旨巷。C函數(shù)可以在C文件中定義,也可以是使用(防止名稱修改)定義的C兼容C ++函數(shù)添忘。ccall()extern "C"
// Call C from JavaScript
var c_javascript_add = Module.cwrap('c_add', // name of C function
'number', // return type
['number', 'number']); // argument types
// Call c_javascript_add normally
console.log(c_javascript_add(10, 20)); // 30
console.log(c_javascript_add(20, 30)); // 50
emscripten cwrap 的膠水文件源碼如下
function cwrap(ident, returnType, argTypes, opts) {
return function() {
return ccall(ident, returnType, argTypes, arguments, opts);
}
}
可以看出采呐,其本質(zhì) 還是ccall,只是返回了函數(shù)方便調(diào)用
ccall源碼如下
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);
ret = convertReturnValue(ret);
if (stack !== 0) stackRestore(stack);
return ret;
}
可以看出搁骑,傳遞字符串及數(shù)組的本質(zhì)是 1.申請一定長度的空間(單位字節(jié))斧吐,得到空間的初始地址 2.將數(shù)據(jù)寫入內(nèi)存
接收數(shù)據(jù) 借助c中的指針(地址),從內(nèi)存取出仲器,
emscripten 封裝了一堆根據(jù)指針(地址) 從內(nèi)存中 寫入煤率、取出 字符串、文件 數(shù)據(jù)的放法乏冀,需要時自行文檔及源碼查閱蝶糯。
例子:傳遞復雜的數(shù)據(jù),如字符串數(shù)組到 c函數(shù)
循環(huán)申請空間辆沦,得到每個字符串的指針裳涛,并寫入內(nèi)存
const nameList = ['ssdf','dsfsd','sdfs']; // 字符串數(shù)組
const namePtrList = []; // 用于存放name指針
nameList.forEach(v=>{
const maxLen = nameList[i].length * 4 + 1; //c中字符串有 \0 為標志的結(jié)束符所以+1
const namePtr = this.zModule._malloc(maxLen);
namePtrList.push(namePtr);
this.zModule.stringToUTF8(nameList[i], namePtr, maxLen); //emscripten 封裝好的寫入字符串到內(nèi)存的方法
})
借助指針把namePtrList當做普通數(shù)組傳遞到c
/**
* 傳遞數(shù)據(jù)的時候要借助上文提到的類型化數(shù)組,對應(yīng)大小的众辨,轉(zhuǎn)化為對應(yīng)的類型化數(shù)組
* 這里指針(地址)是32位,且不需要符號舷礼,所以用 32位無符號的 Uint32Array
*/
const namePtrListArr = new Uint32Array(namePtrList);
const namePtrListPtr = this.zModule._malloc(namePtrListArr.length * 4); // ...
/**
* @type {Int8Array} - HEAP8
* @type {Uint8Array} -HEAPU8
* ... 同理
*/
this.zModule.HEAPU32.set(namePtrListArr, namePtrListPtr / 4); //寫入內(nèi)存鹃彻,第二個參數(shù)32位/4 ,16位/2 同理
// xxFun為c導出的函數(shù)
xxxFun(namePtrListPtr);
傳遞文件可以借助 emscripten的writefile 方法寫入 虛擬文件系統(tǒng),也可以將文件轉(zhuǎn)化為類型化數(shù)組借助指針寫入內(nèi)存妻献,方法同上
釋放空間
emscripten 導出的 _malloc
,_free
用于申請及釋放空間蛛株,
在一段寫入內(nèi)存的數(shù)據(jù)不再使用后釋放空間,相當于垃圾回收
this.zModule._free(namePtrListPtr); // 釋放指針內(nèi)存
問題與優(yōu)化
內(nèi)存是非常珍貴的硬件資源育拨,用內(nèi)存模擬文件系統(tǒng)是非常奢侈的行為谨履。應(yīng)考慮減少內(nèi)存使用
2. [在web worker中使用webassembly](https://www.cntofu.com/book/150/zh/ch6-threads/ch6-02-sample.md)
由于加載wasm的過程是同步耗時的,因此大的wasm文件可以借助web worker開啟多線程使用
Worker 接口是 Web Workers API 的一部分熬丧,指的是一種可由腳本創(chuàng)建的后臺任務(wù)笋粟,任務(wù)執(zhí)行中可以向其創(chuàng)建者收發(fā)信息