本文章只讲如何将C/C++语言代码通过webassembly移植到浏览器上运行。
适合阅读人群如下:
1、希望快速掌握webassembly简易开发的人群。
为啥写这文章呢?因为当初做webassembly毕业设计的时候,光是耗费在筛查资料,学习webassembly底层知识,deadline就已经到头了。如今做完毕业论文,回头一看,其实好多webassembly的偏底层基础的知识可以先不学(一些知识在我毕设开发中完全没用上)。因此,为后入坑的朋友们写下这篇文章,希望有助于你们快速掌握C/C++面向webassembly的开发基本核心知识。好歹能做完工程交差。
文章将解决如下三个问题:
JavaScript、C/C++与WASM之间的关系。
在阅读这篇文章之前,你应该简单了解webassembly的基本意义。它可以被认为是一个应用跨平台的解决方案之一。在浏览器上,它当前的定位是辅助JavaScript,承担计算量,因此,你可以认为它是”通常比JavaScript高效的计算器“。
两者之间的信息交互存在一定耗时,所以频繁的信息交互反而可能降低性能。
WASM是webassembly的缩写,在本文章中,指的是通过Emscripten工具,编译C/C++ 代码而产生的文件。与此同时还会产生一个后缀为.js的文件(称为:js胶水代码)。
js胶水代码包含自动加载WASM模块的JavaScript代码。
为方便理解,从第二张图可见:
1、Js胶水代码导入到JavaScript中执行,将会实现自动加载和解析WASM模块。此后,你可以在JavaScript中,很方便地调用WASM模块中包含的函数功能。
2、WASM模块是由C/C++代码通过工具编译生成的,因此,JavaScript调用WASM模块中的函数功能,逻辑上,就是调用C/C++代码中的函数。
例如:
C中有函数为:ADD(int a,int b)
将C编译为wasm模块后,使用JavaScript加载。
在JavaScript中,编写代码如下:
var c = Module._ADD(4,9);
此时就实现了对ADD函数的调用,但你会发现,完全不需要理会ADD函数在WASM模块中是如何实现的。
综上,你可以无需理解WASM内部构成以及Javascript和wasm之间具体是如何实现的,你只需要知道,JavaScript和C/C++之间在逻辑上如何进行相互调用和通信。
JavaScript如何调用C函数。
有两种方式:
1、Module方法调用
2、Cwrap
在讲解之前,你需要确保安装了webassembly的环境。能够实现调用emcc命令将C/C++代码编译成WASM模块。
你可以参考链接。
安装Emscripten
并完成 你好,世界!中的实例。
在linux安装好环境,并确认终端窗口可以执行
emcc -v 命令后。
1、新建文本,写入以下:
#include
#include
int main () {
fprintf(stdout, "wasm init done\n");
return 0;
}
EMSCRIPTEN_KEEPALIVE//这个宏表示这个函数要作为导出的函数
int add (int x, int y) {
return x + y;
}
EMSCRIPTEN_KEEPALIVE//这个宏表示这个函数要作为导出的函数
int square (int x) {
return x * x;
}
保存,并将文件名改成 math.c
这是简单的例子,EMSCRIPTEN_KEEPALIVE这个宏在
2、从终端窗口打开到包含math.c的文件夹。
执行如下命令:
emcc math.c -Os -s WASM=1 -o index.html -s EXTRA_EXPORTED_RUNTIME_METHODS='[ccall, cwrap]'
你将会得到三个文件:index.wasm、index.html、index.js
math.c :所要编译的代码文件;
index.html :将产生index.html。如果替换为 index.wasm,那么只会产生index.wasm、index.js这两个文件;
EXTRA_EXPORTED_RUNTIME_METHODS=’[ccall, cwrap]’ :表示启用’[ccall, cwrap]'函数,若想用cwrap调用wasm中的函数,必须添加该项;如果不用Cwrap,那就删掉这一段。
3、编写对应JavaScript代码来调用wasm。
第一种方式:Module方法调用。
<!DOCType html>
<html>
<head>
<meta charset="utf-8">
<title>WASM test</title>
</head>
<body>
<script src="index.js"></script> //调用js胶水代码,加载wasm模块,这一步会产生Module这个对象。
<script>
Module.onRuntimeInitialized = function () {
var d =Module._square(5);
console.log("square",d);
d =Module._add(5,10);
console.log("add ",d};
</script>
</body>
</html>
代码解释:
Module.onRuntimeInitialized = function () {
... ...
}
表示在wasm模块加载完毕时,就执行花括号内代码。Module对象是js胶水代码加载wasm时定义的。
调用C中的函数,方式如下:
Module._functionName(p1,p2,...,pn)
functionName是被调用的C中的函数名。
var d =Module._square(5);
填入的参数与C函数所需的参数一一对应,可以多,但不能少。
如果没有参数或者返回值,调用时直接写为:
Module._functoinName();
但该方式只能向C函数传递数值类型参数,而不能传递字符串等其他类型参数。
第二种方式:Cwrap
通过该方式可以向C函数传递’boolean’、‘number’、‘string’、'null’等类型参数。
当然,使用的前提是,emcc编译C代码时添加了EXTRA_EXPORTED_RUNTIME_METHODS='[ccall, cwrap]'
下面介绍JavaScript如何使用Cwrap调用C中的函数。
<!DOCType html>
<html>
<head>
<meta charset="utf-8">
<title>WASM test</title>
</head>
<body>
<script src="index.js"></script> //调用js胶水代码,加载wasm模块,这一步会产生Module这个对象。
<script>
Module.onRuntimeInitialized = function () {
//初始化wasm中的函数
add233 =Module.cwrap('add','number', ['number', 'number']);
square233 =Module.cwrap('square','number', ['number']);
console.log('WASM initialized done!');
var d =square233(5);
console.log("square",d);
d =add233(5,10);
console.log("square",d};
</script>
</body>
</html>
add233 =Module.cwrap(‘add’,‘number’, [‘number’, ‘number’]);
使用cwrap函数先对C中的函数进行封装,再调用。
cwrap()有三个参数:
第一个是 C代码中希望被JavaScript调用的函数名。
第二个是 返回值类型,这里为number。
第三个是 参数类型,对应C代码中add函数有两个数值参数,因此,这里是一个数组 [‘number’, ‘number’]。就square而言,只有一个参数,因此是[‘number’].如果参数为字符串,就写为[‘string’]
add233 为封装后的函数名称。
此后,调用square233函数,就是调用C中的square函数。
var d =add233(5,90);
关于cwrap的详细介绍,你可一参考:C/C++面向WebAssembly编程
JavaScript与WASM之间如何进行通信
①cwrap
②内存
③函数植入
JavaScript和wasm之间的通信(也就是和C之间的通信),先前已经举例说明了 通过函数调用的方式传递参数进行通信。
但,如果要传递的数据量较大时,通过函数调用直接传递数据,会很麻烦。
例如:传递300K以上的字符串,传递一个媒体文件给wasm模块。
因此,需要通过内存方式传递。
概要步骤:
1、在WASM中申请一段内存,获取内存位置和长度。
2、将文件数据通过Module对象的某个方法,写入到该内存中。
3、将内存位置和长度作为参数,传递给WASM中的函数。
4、WASM中的函数依据内存位置和长度读取数据。
(上图来源:WebAssembly Interface Types: Interoperate with All the Things!)
下图为数据流向。
html代码:
<form>
<p>请选择一个文件</p>
<input type="file" required name="form_1" >
</form>
定义了一个输入窗口,用于上传文件。
JavaScript代码:
fileReader = new FileReader();//创建一个FileReader对象
file = form.form_1.files[0];//从form表单的名为form_1的input标签获取URL(大概)
if(!file){
console.log("未检测到输入视频");
}
fileReader.readAsArrayBuffer(file);//读取文件,保存为ArrayBuffer。
fileReader.onload = function () {
let buffer = new Uint8Array(this.result);
let offset = Module._malloc(buffer.length);
//...
}
解释:
承接第一步,
fileReader.onload = function () {。。。}
在文件从本地读取完毕后,自动执行花括号内代码。
let buffer = new Uint8Array(this.result);
上一行代码,将readAsArrayBuffer(file)执行后的结果——this.result 按照字节读取。
let offset = Module._malloc(buffer.length);
上一行代码将通过Module对象调用C函数 malloc
申请一块与文件字节大小buffer.length相同的内存。
你所编译的C代码一定要包含malloc的头文件,否则无法通过 Module._malloc()申请内存。
Module.HEAP8.set(buffer, offset);
var ptr = Module._function(offset, buffer.length);
Module.HEAP8.set(buffer, offset);
将buffer中包含的数据按字节拷贝到以offset为开头的内存中。
Module._function(offset, buffer.length);
将内存起始位置和文件大小buffer.length传递给函数function。
到此,完成了向WASM模块传递文件的过程。
概要步骤
1、在WASM中(也就是在C/C++中),申请一块内存,并将数据保存到内存中。
2、返回该内存位置以及数据大小。
3、在JavaScript中按照内存位置以及数据大小读取。
具体步骤:
由于js调用wasm模块中的函数,其返回值为一个number。那么如何同时获得内存位置以及数据大小两个number呢?
答案:C/C++中创建一个结构体来储存内存位置以及数据大小。例如:
typedef struct {
uint32_t length;//保存数据大小
uint8_t *ptr;//保存内存起始位置
} return_struct;
1、创建一个结构体 return_struct A;
2、向该结构体写入内存位置以及数据大小。
3、向JS返回该结构体指针。
以上C/C++部分的步骤完成。
下面介绍JavaScript如何从该结构体指针获取所需数据。
假设:
var ptr = Module._function(offset, buffer.length);
javascript将WASM返回的结构体指针保存在ptr变量中。
那么有如下:
let length = Module.HEAPU32[ptr / 4],
Ptr = Module.HEAPU32[ptr /4 + 1],
returnBuffer = Module.HEAPU8.subarray(Ptr, Ptr +length+1);
解释:
length = Module.HEAPU32[ptr / 4];
该语句中HEAPU32,你可以理解为一个宽度为32位的窗口(或者盒子),按照32位对内存标序号。
ptr / 4为社么要对ptr除4?
因为返回值ptr是按8位对内存标号而读取的坐标,转化为32位的坐标时,需要除4.
假如,一段内存的某个位置,按8位二进制为一个单位进行标号,若该位置坐标为X。那么表示该位置距离内存开头8X比特。现在按照32位为一个单位进行标号,则该位置新坐标为 8X/32。
最终新坐标为 X/4.
所以,Module.HEAPU32[ptr / 4];表示按照32位读取ptr / 4位置的数据。
对应结构体中的 uint32_t length。
Ptr = Module.HEAPU32[ptr /4 + 1]
该语句将坐标+1,也就是将窗口往后移动一个单位读取数据。读取到结构体中的 uint8_t *ptr;
当然,也可以写成Ptr = Module.HEAPU8[ptr + 4]
returnBuffer = Module.HEAPU8.subarray(Ptr, Ptr +length+1);
获取了内存起始位置Ptr和长度length后,使用Module.HEAPU8.subarray()方法,将Ptr到 Ptr +length+1之间的数据,按照字节读取,并以returnBuffer 应用。
returnBuffer对象类型为 uint8array。
到此,完成了WASM向Js传递数据的过程。
若想将数据保存到本地,可以参考以下链接:
下载ArrayBuffer到本地