webassembly快速应用入门

webassembly快速应用入门

本文章只讲如何将C/C++语言代码通过webassembly移植到浏览器上运行。

适合阅读人群如下:
1、希望快速掌握webassembly简易开发的人群。

为啥写这文章呢?因为当初做webassembly毕业设计的时候,光是耗费在筛查资料,学习webassembly底层知识,deadline就已经到头了。如今做完毕业论文,回头一看,其实好多webassembly的偏底层基础的知识可以先不学(一些知识在我毕设开发中完全没用上)。因此,为后入坑的朋友们写下这篇文章,希望有助于你们快速掌握C/C++面向webassembly的开发基本核心知识。好歹能做完工程交差。

文章将解决如下三个问题:

  1. JavaScript、C/C++与WASM之间的关系。
  2. JavaScript如何调用wasm模块中对应的C/C++函数。(简单例子与代码实现)
    ①Module对象方法调用
    ②cwrap
  3. JavaScript与WASM之间如何进行通信。(简单例子与代码实现)
    ①cwrap
    ②内存
    ③JavaScript函数植入wasm(暂时不讲,平时也不用)

第一个问题:

JavaScript、C/C++与WASM之间的关系。

在阅读这篇文章之前,你应该简单了解webassembly的基本意义。它可以被认为是一个应用跨平台的解决方案之一。在浏览器上,它当前的定位是辅助JavaScript,承担计算量,因此,你可以认为它是”通常比JavaScript高效的计算器“。
webassembly快速应用入门_第1张图片两者之间的信息交互存在一定耗时,所以频繁的信息交互反而可能降低性能。

WASM是webassembly的缩写,在本文章中,指的是通过Emscripten工具,编译C/C++ 代码而产生的文件。与此同时还会产生一个后缀为.js的文件(称为:js胶水代码)。
js胶水代码包含自动加载WASM模块的JavaScript代码。

webassembly快速应用入门_第2张图片webassembly快速应用入门_第3张图片为方便理解,从第二张图可见:
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这个宏在中定义。emscripten工具在编译C代码时,该宏将提示编译器,宏后面的函数是可以被JavaScript调用的(可被导出的),并且在编译时防止该函数被优化掉。

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快速应用入门_第4张图片
(上图来源:WebAssembly Interface Types: Interoperate with All the Things!)

下图为数据流向。

webassembly快速应用入门_第5张图片

通过内存向WASM传递数据的具体步骤:

1、本地文件数据传递给JavaScript,暂时保存到Array数组中。

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。

2、按照文件大小,在wasm中申请内存并获取内存位置。

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()申请内存。

3、将数据拷贝到内存中,并传递给WASM中的函数

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++中创建一个结构体来储存内存位置以及数据大小。例如:

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到本地

你可能感兴趣的:(WebAssembly,WASM,Web,前端,C++,javascript,html,WebAssembly,wasm)