0. 前提知识点
- 导出C中的函数给JS调用:主要是
EMSCRIPTEN_KEEPALIVE这个Emscripten环境特有的宏。
#include <stdio.h>
#ifndef EM_PORT_API
# if defined(__EMSCRIPTEN__)
# include <emscripten.h>
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype EMSCRIPTEN_KEEPALIVE
# else
# define EM_PORT_API(rettype) rettype EMSCRIPTEN_KEEPALIVE
# endif
# else
# if defined(__cplusplus)
# define EM_PORT_API(rettype) extern "C" rettype
# else
# define EM_PORT_API(rettype) rettype
# endif
# endif
#endif
EM_PORT_API (int) sum(int *ptr, int count) {
int total = 0;
for(int i = 0; i < count; i++) {
total += ptr[i];
}
return total;
}
// js
Module._sum(ptr, 10);
- 使用Emscripten编译C代码到wasm。Emscripten的环境比较难配置,主要是网络问题,但好在有docker环境可以直接使用trzeci/emscripten。推荐写个
build.sh文件方便修改编译脚本
# build.sh
docker run \
--rm \
-v $(pwd):$(pwd) \
-u $(id -u):$(id -g) \
trzeci/emscripten \
emcc helloworld.c -o helloworld.js
- 使用编译后的wasm。编译成功后有会
.wasm文件和其对应的.js胶水文件,其目录:
.
├── build.sh
├── index.html
├── test.cc
├── test.js
└── test.wasm
在index.html写测试代码
<script>
var Module = {};
Module.onRuntimeInitialized = () => {
// 在这个回调中调用wasm中的方法
};
</script>
<!-- 胶水层代码要放在后台 -->
<script src="test.js"></script>
- WASM Table: 存储函数
这是一个包装了WebAssemble Table 的Javascript包装对象,具有类数组结构,存储了多个函数引用。在Javascript或者WebAssemble中创建Table 对象可以同时被Javascript或WebAssemble 访问和更改。
1. 在C中调用JS函数之addFunction
Emscripten提供了多种在C环境调用JavaScript的方法,包括:
- EM_JS/EM_ASM宏内联JavaScript代码
- emscripten_run_script函数
- JavaScript函数注入(更准确的描述为:“Implement C API in JavaScript”,既在JavaScript中实现C函数API)
- 使用addFunction将函数指针传到C代码中调用
前3种方法点击链接就可以查看详细的使用说明
下面着重描述下第4种方法,主要结合Calling JavaScript functions as function pointers from C实践一下
You can use addFunction to return an integer value that represents a function pointer. Passing that integer to C code then lets it call that value as a function pointer, and the JavaScript function you sent to addFunction will be called.
你可以使用
addFunction这个函数的返回值(数字)来代表这个函数的指针。然后将该指针(数字)传递给C代码,然后让其将该值作为函数指针进行调用,发送给addFunction的JavaScript函数将被调用。
由上面的说明可以推测出Module有一个addFunction的方法,返回值是一个数字类型。
在尝试调用的时候,发现提示说要在编译的时候导出这个函数
docker run \
--rm \
-v $(pwd):/src \
-u $(id -u):$(id -g) \
emscripten/emsdk \
emcc test.cc -o test.js \
# 要在这里加上
-s EXTRA_EXPORTED_RUNTIME_METHODS="['addFunction']"
再次调用时又发现要设置wasm table成为可以grow的
此时要在编译脚本中再加上一行
-s ALLOW_TABLE_GROWTH
You should build with -s ALLOW_TABLE_GROWTH to allow new functions to be added to the table. Otherwise by default the table has a fixed size.
加上编译后,再次运行,发现叕报错了,缺少函数签名
查看文档
When using addFunction on LLVM wasm backend, you need to provide an additional second argument, a Wasm function signature string. Each character within a signature string represents a type. The first character represents the return type of a function, and remaining characters are for parameter types.
- '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
原来是addFunction的第二个参数需要标明函数的返回值类型,及参数类型,再次修改
终于成功了,此时已得到了函数的指针,将其传入到C代码中就可以调用了。
下面看下C代码的实现:
// 声明函数签名,在JS中调用addFunction时,第二个函数的签名要与此声明保持一致
typedef void testExternalJSMethod(int p);
// 导出一个接收函数
EM_PORT_API (void) pass_fn_ptr(int ptr) {
((testExternalJSMethod*)ptr)(1);
}
// js
Module.onRuntimeInitialized = () => {
function jsFunction(i) {
console.log('定义在js中的function');
console.log('从c中传来的参数: ', i);
}
// 这里说明下,C语言是先声明函数返回的类型,所以这里要先写返回值的类型,再写其他参数的类型
var fPtr = Module.addFunction(jsFunction, 'vi');
Module._pass_fn_ptr(fPtr);
};
最后看下输出结果:
2. addFunction的优点
- 相对于第3种方式来说比较灵活,第3种在js中函数的实现的参与到编译的过程中,而真实应用时给c调用的函数往往混合在业务代码中,或者是用ts去实现的,这样第3种方式就会很麻烦;
- EM_ASM emscripten_run_script这种方式直接将js代码内联到C中,没有什么维护性;
#include <emscripten.h>
int main() {
EM_ASM(console.log('你好,Emscripten!'));
return 0;
}