webassembly快速应用入门
本文章只讲如何将C/C++语言代码通过webassembly移植到浏览器上运行。
适合阅读人群如下:
1、希望快速掌握webassembly简易开发的人群。
为啥写这文章呢?因为当初做webassembly毕业设计的时候,光是耗费在筛查资料,学习webassembly底层知识,deadline就已经到头了。如今做完毕业论文,回头一看,其实好多webassembly的偏底层基础的知识可以先不学(一些知识在我毕设开发中完全没用上)。因此,为后入坑的朋友们写下这篇文章,希望有助于你们快速掌握C/C++面向webassembly的开发基本核心知识。好歹能做完工程交差。
文章将解决如下三个问题:
- JavaScript、C/C++与WASM之间的关系。
- JavaScript如何调用wasm模块中对应的C/C++函数。(简单例子与代码实现)
①Module对象方法调用
②cwrap - JavaScript与WASM之间如何进行通信。(简单例子与代码实现)
①cwrap
②内存
③JavaScript函数植入wasm(暂时不讲,平时也不用)
第一个问题:
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、新建文本,写入以下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include <emscripten.h> #include <stdio.h> 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的文件夹。
执行如下命令:
1 | 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=’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | ’ :表示启用’[ccall, cwrap]'函数,若想用cwrap调用wasm中的函数,必须添加该项;如果不用Cwrap,那就删掉这一段。</p> <p><strong>3、编写对应JavaScript代码来调用wasm。</strong><br> <strong>第一种方式:Module方法调用。</strong></p> [cc]<!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> |
代码解释:
1 2 3 | Module.onRuntimeInitialized = function () { ... ... } |
表示在wasm模块加载完毕时,就执行花括号内代码。Module对象是js胶水代码加载wasm时定义的。
调用C中的函数,方式如下:
1 | Module._functionName(p1,p2,...,pn) |
functionName是被调用的C中的函数名。
1 | var d =Module._square(5); |
填入的参数与C函数所需的参数一一对应,可以多,但不能少。
如果没有参数或者返回值,调用时直接写为:
1 | Module._functoinName(); |
但该方式只能向C函数传递数值类型参数,而不能传递字符串等其他类型参数。
第二种方式:Cwrap
通过该方式可以向C函数传递’boolean’、‘number’、‘string’、'null’等类型参数。
当然,使用的前提是,emcc编译C代码时添加了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | '</wyn></p> <p>下面介绍JavaScript如何使用Cwrap调用C中的函数。</p> [cc]<!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函数。
1 | 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!)
下图为数据流向。
通过内存向WASM传递数据的具体步骤:
1、本地文件数据传递给JavaScript,暂时保存到Array数组中。
html代码:
1 2 3 4 | <form> <p>请选择一个文件</p> <input type="file" required name="form_1" > </form> |
定义了一个输入窗口,用于上传文件。
JavaScript代码:
1 2 3 4 5 6 | fileReader = new FileReader();//创建一个FileReader对象 file = form.form_1.files[0];//从form表单的名为form_1的input标签获取URL(大概) if(!file){ console.log("未检测到输入视频"); } fileReader.readAsArrayBuffer(file);//读取文件,保存为ArrayBuffer。 |
2、按照文件大小,在wasm中申请内存并获取内存位置。
1 2 3 4 5 | fileReader.onload = function () { let buffer = new Uint8Array(this.result); let offset = Module._malloc(buffer.length); //... } |
解释:
承接第一步,
fileReader.onload = function () {。。。}
在文件从本地读取完毕后,自动执行花括号内代码。
1 | let buffer = new Uint8Array(this.result); |
上一行代码,将readAsArrayBuffer(file)执行后的结果——this.result 按照字节读取。
1 | let offset = Module._malloc(buffer.length); |
上一行代码将通过Module对象调用C函数 malloc
申请一块与文件字节大小buffer.length相同的内存。
注意:
你所编译的C代码一定要包含malloc的头文件,否则无法通过 Module._malloc()申请内存。
3、将数据拷贝到内存中,并传递给WASM中的函数
1 2 | 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模块传递文件的过程。
WASM通过内存向JS传递数据的具体步骤:
概要步骤
1、在WASM中(也就是在C/C++中),申请一块内存,并将数据保存到内存中。
2、返回该内存位置以及数据大小。
3、在JavaScript中按照内存位置以及数据大小读取。
具体步骤:
由于js调用wasm模块中的函数,其返回值为一个number。那么如何同时获得内存位置以及数据大小两个number呢?
答案:C/C++中创建一个结构体来储存内存位置以及数据大小。例如:
1 2 3 4 | typedef struct { uint32_t length;//保存数据大小 uint8_t *ptr;//保存内存起始位置 } return_struct; |
1、创建一个结构体 return_struct A;
2、向该结构体写入内存位置以及数据大小。
3、向JS返回该结构体指针。
以上C/C++部分的步骤完成。
下面介绍JavaScript如何从该结构体指针获取所需数据。
假设:
1 | var ptr = Module._function(offset, buffer.length); |
javascript将WASM返回的结构体指针保存在ptr变量中。
那么有如下:
1 2 3 | 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到本地