什么是WebAssembly?
- 是一个可移植、体积小、加载快并且兼容 Web 的全新格式
- wasm是体积小且加载快的二进制格式, 其目标就是充分发挥硬件能力以达到原生执行效率
- 运行在一个沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在web环境中,WebAssembly将会严格遵守同源策略以及浏览器安全策略。
- 中被设计成无版本、特性可测试、向后兼容的。WebAssembly 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。当然,WebAssembly 不仅可以运行在浏览器上,也可以运行在非web环境下。
- 支持语言: c/c++ 、rust、原始的webassembly S表达式文本、AssemblyScript(TypeScript-like)、Go 。 其他语言(python,java,scala,kotlin等诸多语言也有工具实验性支持)。
浏览器支持情况
webassembly 特性
- 计算速度快,性能高,编译成wasm后的代码性能接近原生
- 可以使用c++/c/go 众多的三方库来前端处理复杂任务与计算 (opencv、FFmpeg等)
- 不需要垃圾回收机制,手动管理内存
- 通过wasm的内存与JavaScript 通信
应用场景
- 将 C、C++、Rust 等语言编写的程序移植到浏览器
- 图形图像处理领域(如OCR识别),如页游、数据可视化等
- 音视频编解码识别、AI等等
- 解压、压缩 等对性能要求高的需求
webassembly 基本概念及使用
几个概念
-
Module
一个“代码单元”。包含编译好的二进制代码。可以高效的缓存、共享。未来可以像一个ES2015模块一样导入/导出 -
Memory
内存,连续的,可变大小的字节数组缓冲区。可以理解为一个“堆” -
Table
连续的,可变大小的类型数组缓冲区 现在table只支持函数引用类型,可以类比为一个“栈” -
Instance
在Module基础上,包含所有运行时所需状态的实例,如果把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
上述代码重复计算一百万次斐波那契数列46项(47项会溢出),结果如下:
- 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 环境 (略) 详见
emscripten官网
- 阅读C/C++ 三方库文档
- 编写C函数,用于调用库中的方法
- emcc命令编译c语言为wasm 及 封装的js胶水代码
- 编写胶水代码,用于C语言与js通信,js中调用c函数 (直接调用只能传递int值,传递其他类型值需要借助内存处理)
Vue中使用
方式一: 将wasm 与 js文件放到如cdn或服务器
方式二: 将wasm 与 js封装 为库,发布到 npm使用
方式三: 本地使用,
- wasm文件并不会被webpack打包进dist,使用 url-loader 将 wasm 只作文静态文件路径 注意:import 下wasm文件确保被打包进dist
- 可以放在vue的public文件下
Emscripten 编译命令
emcc -
优化flag,它们-O0,-O1,-O2,-Os,-Oz,-O3。 对应不同优化级别
-s OPTION=VALU 传给编译器的所有涉及到JavaScript代码生成的选项
emcc simple/helloword.c -o output/hellow.js \
-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall','abc,'_malloc','_free']" \ #导出的函数,abc为自己写的c语中的函数,其他为emscriten自带的
-s MODULARIZE=1 # 模块化,生成闭包函数
-s ENVIRONMENT="web" \
-s ALLOW_MEMORY_GROWTH=1 \ #开启可变内存
-s FORCE_FILESYSTEM=1 # 强制启用em的虚拟文件系统
-s RESERVED_FUNCTION_POINTERS #保留函数表指针
JS 类型化数组 与 buffer,与Blob
ArrayBuffer是一个构造函数,可以分配一段可以存放数据的连续内存区域
var buffer = new ArrayBuffer(16); //创建一个连续16字节的内存缓冲
视图类型 | 说明 | 字节大小 |
---|---|---|
Uint8Array | 8位无符号整数 | 1字节 |
Int8Array | 8位有符号整数 | 1字节 |
Uint8ClampedArray | 8位无符号整数(溢出处理不同) | 1字节 |
Uint16Array | 16位无符号整数 | 2字节 |
Int16Array | 16位有符号整数 | 2字节 |
Uint32Array | 32位无符号整数 | 4字节 |
Int32Array | 32位有符号整数 | 4字节 |
Float32Array | 32位IEEE浮点数 | 4字节 |
Float64Array | 64位IEEE浮点数 | 8字节 |
// 创建一个视图,此视图把缓冲内的数据格式化为一个32位(4字节)有符号整数数组
var int32View = new Int32Array(buffer);
// 我们可以像普通数组一样访问该数组中的元素
for (var i = 0; i < int32View.length; i++) {
int32View[i] = i * 2;
}
-
Blob
对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成ReadableStream
来用于数据操作。Blob 表示的不一定是JavaScript原生格式的数据。
File
接口基于Blob
,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。
var aBlob = new Blob( array, options )
/** 例如 */
const blob = new Blob([int32View], {
type: "application/zip"
});
array 是一个由
ArrayBuffer
,ArrayBufferView
,Blob
,DOMString
等对象构成的Array
,或者其他类似对象的混合体,它将会被放进Blob
。DOMStrings会被编码为UTF-8。-
options
是一个可选的
BlobPropertyBag
字典,它可能会指定如下两个属性:
-
type
,默认值为""
,它代表了将会被放入到blob中的数组内容的MIME类型。 -
endings
,默认值为"transparent"
,用于指定包含行结束符\n
的字符串如何被写入。 它是以下两个值中的一个:"native"
,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者"transparent"
,代表会保持blob中保存的结束符不变
-
1. 在C中调用JS函数之addFunction
Emscripten提供了多种在C环境调用JavaScript的方法,包括:
JavaScript函数注入(更准确的描述为:“Implement C API in JavaScript”,既在JavaScript中实现C函数API)
-
第一个字符表示函数的返回类型,其余字符表示参数类型
-
'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 中的内存完成,基本思想是将一段数据的内存地址与长度传递到C中,c根据地址和长度取出内容。
ccall 与 ccwrap
如果直接使用C导出的函数,只能传递 number 的数据,如果使用了其他类型的需要借助ccall/cwrap
以下摘自https://emscripten.org/docs/api_reference/preamble.js.html
ccall(ident,returnType,argTypes,args,opts )
从JavaScript调用已编译的C函数。
该函数从JavaScript执行已编译的C函数,并返回结果。C ++名称处理意味着无法调用“正常”的C ++函数。该函数必须在.c文件中定义,或者是使用定义的C ++函数。extern "C"
returnType并argTypes让您指定参数的类型和返回值。可能的类型是"number","string","array",或"boolean",其对应于相应的JavaScript类型。使用"number"任何数值类型或C指针,string对于Cchar*表示字符串,"boolean"对于一个布尔类型,"array"为JavaScript阵列和类型数组,含有8位整数数据-即,数据被写入的8位整数的C数组; 特别是如果您在此处提供类型化数组,则它必须是Uint8Array或Int8Array。如果要接收其他类型的数据数组,则可以手动分配内存并对其进行写入,然后在此处提供一个指针(作为"number",因为指针只是数字)。
// Call C from JavaScript
var result = Module.ccall('c_add', // name of C function
'number', // return type
['number', 'number'], // argument types
[10, 20]); // arguments
总结: 传递的参数 只能为 字符串,数字,及Uint8Array或Int8Array
cwrap(ident,returnType,argTypes )
返回C函数的本机JavaScript包装器。
这类似于,但是返回一个JavaScript函数,该函数可以根据需要多次重复使用。C函数可以在C文件中定义,也可以是使用(防止名称修改)定义的C兼容C ++函数。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);
}
}
可以看出,其本质 还是ccall,只是返回了函数方便调用
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;
}
可以看出,传递字符串及数组的本质是 1.申请一定长度的空间(单位字节),得到空间的初始地址 2.将数据写入内存
接收数据 借助c中的指针(地址),从内存取出,
emscripten 封装了一堆根据指针(地址) 从内存中 写入、取出 字符串、文件 数据的放法,需要时自行文档及源码查阅。
例子:传递复杂的数据,如字符串数组到 c函数
循环申请空间,得到每个字符串的指针,并写入内存
const nameList = ['ssdf','dsfsd','sdfs']; // 字符串数组
const namePtrList = []; // 用于存放name指针
nameList.forEach(v=>{
const maxLen = nameList[i].length * 4 + 1; //c中字符串有 \0 为标志的结束符所以+1
const namePtr = this.zModule._malloc(maxLen);
namePtrList.push(namePtr);
this.zModule.stringToUTF8(nameList[i], namePtr, maxLen); //emscripten 封装好的写入字符串到内存的方法
})
借助指针把namePtrList当做普通数组传递到c
/**
* 传递数据的时候要借助上文提到的类型化数组,对应大小的,转化为对应的类型化数组
* 这里指针(地址)是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); //写入内存,第二个参数32位/4 ,16位/2 同理
// xxFun为c导出的函数
xxxFun(namePtrListPtr);
传递文件可以借助 emscripten的writefile 方法写入 虚拟文件系统,也可以将文件转化为类型化数组借助指针写入内存,方法同上
释放空间
emscripten 导出的 _malloc
,_free
用于申请及释放空间,
在一段写入内存的数据不再使用后释放空间,相当于垃圾回收
this.zModule._free(namePtrListPtr); // 释放指针内存
问题与优化
内存是非常珍贵的硬件资源,用内存模拟文件系统是非常奢侈的行为。应考虑减少内存使用
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 的一部分,指的是一种可由脚本创建的后台任务,任务执行中可以向其创建者收发信息