1. 前言
Webassembly 是一种可以在浏览器端运行二进制格式代码的技术,WebAssembly最大的优点莫过于可大幅度提升 Javascript 的性能。
WebAssembly 的设计目标:定义一个可移植,体积紧凑,加载迅速的二进制格式为编译目标,而此二进制格式文件将可以在各种平台(包括移动设备和物联网设备)上被编译,然后发挥通用的硬件性能以原生应用的速度运行。
这篇文章主要演示C代码如何编译成wasm文件,如何生成JS文件,JS代码如何调用wasm文件封装的C语言函数。分别编写了两个案例演示了整体流程,完成C函数的传参、返回值的接收等功能。
2. 导出自定义函数给JS调用
下面案例里编写一个C语言代码,提供两个函数接口给JS调用。
2.1 C代码
#include <emscripten.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int func_square(int x)
{
return x * x;
}
int func_sum(int x, int y)
{
return x + y;
}
说明:如果上面这样编写的C函数如果需要导出,在编译的时候需要加-s "EXPORTED_FUNCTIONS=['_func_square','_func_sum']"
参数指定导出的函数。
如果不想在编译命令里指定,也可以在编写C函数时,加上EMSCRIPTEN_KEEPALIVE
修饰。
如果是系统的的库函数,或者是第三方库的函数需要导出给前端调用,不能修改源码声明的情况,那么就在编译的时候加上`-s “EXPORTED_FUNCTIONS=[‘_xxxx’]” 声明即可,把要导出的函数名称在里面写好,编译就行。
#include <emscripten.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int EMSCRIPTEN_KEEPALIVE func_square(int x)
{
return x * x;
}
int EMSCRIPTEN_KEEPALIVE func_sum(int x, int y)
{
return x + y;
}
2.2 将C代码编译成wasm文件
emcc hello.c --no-entry -s "EXPORTED_FUNCTIONS=['_func_square','_func_sum']" -O3 -o hello.wasm
参数介绍:
(1)--no-entry
表示不需要导出main函数,也就是C代码里不用包含main函数,生成的wasm文件当做库给前端JS调用。
(2)"EXPORTED_FUNCTIONS=['_func_square','_func_sum']"
表示要导出的C函数名称,导出时需要在原C函数名称上加上_
(3)hello.wasm
表示指定生成的wasm文件名称
2.3 编写JS文件
这个JS代码用来加载wasm文件,做一些初始化设置。
测试时,新建一个名称为loader.js
的文件,将这段代码复制出来贴进去保存。
function loadWebAssembly(filename, imports = {}) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => {
imports.env = imports.env || {}
Object.assign(imports.env, {
memoryBase: 0,
tableBase: 0,
__memory_base: 0,
__table_base: 0,
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
})
return WebAssembly.instantiate(buffer, imports)
})
.then(result => result.instance )
}
function loadJS (url, imports = {}) {
return fetch(url)
.then(response => response.text())
.then(code => new Function('imports', `return (${code})()`))
.then(factory => ({ exports: factory(imports) }))
}
2.4 编写HTML文件
创建一个名为index.html
的HTML文件,将下面贴出的代码贴进去保存。
编写的这个HTML就是主要是测试代码,里面加载了loader.js
,调用loadWebAssembly
方法加载wasm
文件。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Compile C to WebAssembly</title>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="format-detection" content="telephone=no, email=no" />
<script src="./loader.js"></script>
</head>
<body>
<h1>Compile C to WebAssembly</h1>
<p>The test result can be found in console.</p>
<script>
loadWebAssembly('./hello.wasm')
.then(instance => {
const func_square = instance.exports.func_square
const func_sum = instance.exports.func_sum
var malloc_inputPtr = malloc(100);
console.log('10^2 =', func_square(2))
console.log('100+100 =', func_sum(200,100))
})
</script>
</body>
</html>
2.5 开启HTTP服务器
使用python快速开一个HTTP服务器,用于测试。在HTML文件、wasm文件、JS文件的同级目录下,打开CMD命令行,运行下面命令。
python -m http.server
2.6 打开谷歌浏览器测试
输入地址: http://127.0.0.1:8000/index.html 访问。
然后按下F12,打开控制台看输出结果。
2.7 查看成功导出的C函数有哪些
在浏览器控制台源代码页面可以看到wasm转换后的文本代码,能看到导出了那些可以调用的C函数接口。
如果JS报错找不到某某函数无法调用,可以打开这个文件看一下,函数是否成功导出。
3. 导出C函数给JS调用(方式2)
下面编写一个C代码案例,使用emcc生成js和wasm文件,自己编写一个HTML文件调用JS里提供的方法。
这个JS文件由emcc编译器自动生成,里面封装了C语言函数,可以直接通过JS文件里的方法调用C函数。
3.1 C代码
#include <emscripten.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int func_square(int x)
{
return x * x;
}
int func_sum(int x, int y)
{
return x + y;
}
void func_string(void)
{
printf("成功调用C语言func_string函数.\n");
}
3.2 将C代码编译成wasm和JS文件
emcc hello.c -o hello.js -s EXPORTED_FUNCTIONS="['_func_square','_func_sum','_func_string','_malloc','_free']" -s WASM=1
参数介绍:
hello.c
是将要编译的源文件。
-o hello.js
指定生成的js文件名称,并且会自动生成一个同名的wasm文件。
-s EXPORTED_FUNCTIONS="['_func_square','_func_sum','_func_string','_malloc','_free']"
需要导出的函数。
编译生成的js和wasm文件:
3.3 编写HTML文件
使用emcc编译时,JS文件和wasm文件已经生成了,接下来就编写个HTML代码,完成方法调用测试。
HTML代码里创建了3个按钮,分别调用了3个函数,测试调用C语言函数的。
注意: JS文件里导出的C函数在函数名称前面都是带了一个下划线,调用时要加上下划线。
**HTML代码源码:**新建一个index.html文件,将下面代码贴进去即可。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js调用c语言函数示例</title>
</head>
<body>
<script type='text/javascript'>
function run1()
{
console.log('10^10等于:',_func_square(10))
}
function run2()
{
console.log('10+500等于:',_func_sum(10,500))
}
function run3()
{
_func_string()
}
</script>
<input type="button" value="调用方法1" onclick="run1()" />
<input type="button" value="调用方法2" onclick="run2()" />
<input type="button" value="调用方法3" onclick="run3()" />
<script async type="text/javascript" src="hello.js"></script>
</body>
</html>
3.4 开启HTTP服务器
使用python快速开一个HTTP服务器,用于测试。在HTML文件、wasm文件、JS文件的同级目录下,打开CMD命令行,运行下面命令。
python -m http.server
3.5 打开谷歌浏览器测试
输入地址: http://127.0.0.1:8000/index.html 访问。
然后按下F12,打开控制台看输出结果。
4. 数组、字符串参数传递
前面的例子都是演示整数参数传递和返回值的接收,下面代码演示,C语言与JS代码之间传递int类型指针、字符串、实现内存数据交互。
4.1 C代码
先编写C代码,提供几个测试函数。
#include <emscripten.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int EMSCRIPTEN_KEEPALIVE func_square(int x)
{
return x * x;
}
int EMSCRIPTEN_KEEPALIVE func_sum(int x, int y)
{
return x + y;
}
void EMSCRIPTEN_KEEPALIVE func_string(void)
{
printf("成功调用C语言func_string函数.\n");
}
int* EMSCRIPTEN_KEEPALIVE int_array(int *buff)
{
int i=0;
for(i=0;i<10;i++)
{
buff[i]=i+88;
}
return buff;
}
void EMSCRIPTEN_KEEPALIVE str_print(char *str)
{
printf("C语言收到JS传入的字符串:%s\n",str);
}
char* EMSCRIPTEN_KEEPALIVE str_cpy(char *str)
{
strcpy(str,"我是C代码拷贝的字符串");
return str;
}
4.2 将C代码编译成wasm文件
emcc hello.c -o hello.js -s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString']" -s WASM=1
参数解释:
hello.c
是将要编译的源文件。
-o hello.js
指定生成的js文件名称,并且会自动生成一个同名的wasm文件。
-s EXPORTED_FUNCTIONS="['_malloc','_free','ccall','allocate','UTF8ToString']"
需要导出的函数。
注意: JS与C函数之间字符串交互打印调试时,需要用到一些转换函数。这些函数默认没有导出的,需要自己手动导出。
在生成的JS代码,第1830行这个位置,可以看到编译器内置的很多函数,这些函数默认是没有导出的,如果JS需要调用这些函数,那么编译代码时,加上``-s EXPORTED_FUNCTIONS` 选项导出这些函数。
4.3 编写HTML文件
使用emcc编译时,JS文件和wasm文件已经生成了,接下来就编写个HTML代码,完成方法调用测试。
HTML代码里创建了几个按钮,分别调用了C语言代码里提供的几个测试函数。
注意: JS文件里导出的C函数在函数名称前面都是带了一个下划线,调用时要加上下划线。
**HTML代码源码:**新建一个index.html文件,将下面代码贴进去即可。
<!doctype html>
<html lang="en-us">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>js调用c语言函数示例</title>
</head>
<body>
<script type='text/javascript'>
function run1()
{
console.log('10^10等于:',_func_square(10))
}
function run2()
{
console.log('10+500等于:',_func_sum(10,500))
}
function run3()
{
_func_string()
}
function run4()
{
//申请空间,存放字符串
var ptr1 = allocate(intArrayFromString("小米10至尊版"), ALLOC_NORMAL);
//传递给C代码
_str_print(ptr1)
}
function run5()
{
//申请空间
var buff=_malloc(50)
//调用函数
var out_p=_int_array(buff)
//打印结果
if (out_p == 0) return;
var str = '';
for (var i = 0; i < 10; i++)
{
str += Module.HEAP32[(out_p >> 2) + i];
str += ' ';
}
console.log(str);
//释放空间
_free(buff)
}
function run6()
{
//申请空间
var buff=_malloc(50)
var buff1=_str_cpy(buff);
console.log('返回值:',UTF8ToString(buff1));
//释放空间
_free(buff)
}
</script>
<input type="button" value="求平方:传递1个整数参数,返回整数" onclick="run1()" />
<input type="button" value="求和:传递两个整数参数,返回整数" onclick="run2()" />
<input type="button" value="无参数和返回值函数调用.内部打印日志到控制台" onclick="run3()" />
<input type="button" value="传入字符串参数,内部打印出来" onclick="run4()" />
<input type="button" value="传入int类型指针,赋值数据再返回地址" onclick="run5()" />
<input type="button" value="传入char类型字符串指针,赋值数据再返回地址" onclick="run6()" />
<script async type="text/javascript" src="hello.js"></script>
</body>
</html>
4.4 开启HTTP服务器
在js、wasm、html文件存放目录下运行cmd命令。
python -m http.server
4.5 打开浏览器测试
输入地址: http://127.0.0.1:8000/index.html 访问。
然后按下F12,依次按下页面上的按钮,打开控制台看输出结果。