Emscripten基础

Emscripten底层是LLVM编译器,作用就是将C/C++编译成asm.js

**TIPS: **asm.js是有Mozilla提出的JS子集,更大程度优化以提高执行速度,可以直接对应编译成机器指令执行

image-20250828185617583

0、VsCode配置Emscripten环境

以macOS为例,需要在项目根目录下创建一个.vscode文件夹并创建c_cpp_properties.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"configurations": [
{
"name": "Mac",
"includePath": [
"${workspaceFolder}/**",
"/emcc安装路径/upstream/emscripten/system/include"
],
"defines": [],
"macFrameworkPath": [],
"compilerPath": "/usr/bin/clang",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "macos-clang-arm64"
}
],
"version": 4
}

基于此配置即可在项目中引用emscripten.h并有相关的代码提示

1、C/C++调用JavaScript

使用EM_ASM是一个宏,可以在其中写js代码,如下

1
2
3
4
5
#include <emscripten.h>

int main () {
EM_ASM(alert('hello world'));
}

然后使用以下命令编译成asm.js并嵌入hello.html中

1
emcc test.cpp -s WASM=1 -o hello.html

可以使用vscode的Live Server插件开启一个本地服务器并打开该html,可以发现该alert

命令中的细节:

  • -s WASM=1表示指定编译目标为wasm,如果不设置改参数则默认生成纯js文件ams.js,其中WASM=1表示编译成wasm
  • -o hello.html表示会生成一个HTML页面来运行这段代码,会生成
    • hello.wasm二进制wasm模块代码
    • hello.js包含了用来在原生 C 函数js/wasm之间转换的胶水代码的js文件
    • hello.html用来加载,编译,实例化你的wasm代码并且将它输出在浏览器显示上的一个HTML文件

2、JavaScript调用C/C++

EM_ASM执行的JS代码中也可以使用C/C++函数,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <emscripten.h>

#ifdef __cplusplus
extern "C" {
#endif
int EMSCRIPTEN_KEEPALIVE TestFunc(int x) {
return x * x;
}
#ifdef __cplusplus
}
#endif

int main () {
EM_ASM({
TestFunc = Module.cwrap('TestFunc', 'number', ['number']);
let x = 10;
let result = TestFunc(x);
alert("result: " + result);
});
};

JS代码中需要使用C/C++函数需要使用Module.cwrap方法,该函数接受三个参数:

  • 函数名称,使用引号包裹
  • 该C函数返回值类型,若为void函数则将类型写作null(JS类型,而非C/C++类型)
  • 函数参数类型,数组形式(JS类型)

该代码以下命令编译成asm.js并嵌入test.html中

1
emcc test.cpp -s WASM=1 -s EXPORTED_FUNCTIONS='["_TestFunc","_main"]' -s EXPORTED_RUNTIME_METHODS='["cwrap"]' -o test.html

其中-s EXPORTED_FUNCTIONS表示需要输出的函数名称数组,并在函数名称前加下划线

-s EXPORTED_RUNTIME_METHODS='["cwrap"]'表示导出 cwrap 运行时方法,用于在JavaScript中包装C函数

等价的,还可以使用Module.ccall方法,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <emscripten.h>

#ifdef __cplusplus
extern "C" {
#endif
int EMSCRIPTEN_KEEPALIVE TestFunc(int x) {
return x * x;
}
#ifdef __cplusplus
}
#endif

int main () {
EM_ASM({
let x = 10;
let result = Module.ccall('TestFunc',
'number',
['number'],
[x]
);
alert("result: " + result);
});
};

Module.ccall方法也可以在EM_ASM中的JS方法中调用C方法,该函数接受四个参数:

  • 函数名称,使用引号包裹
  • 该C函数返回值类型,若为void函数则将类型写作null(JS类型,而非C/C++类型)
  • 函数参数类型,数组形式(JS类型)
  • 参数数组

该代码以下命令编译成asm.js并嵌入test.html中

1
emcc test.cpp -s WASM=1 -s EXPORTED_FUNCTIONS='["_TestFunc","_main"]' -s EXPORTED_RUNTIME_METHODS='["ccall"]' -o test.html

3、C/C++与JavaScript通信

C/C++可以通过传参的方式与JS函数进行通信,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <emscripten.h>
#include <iostream>

using namespace std;

int main () {
int x = 10;
int y = EM_ASM_INT({
let val = $0 * $0;
console.log("current: " + val);
return val;
}, x);
cout << "result: " << x << "," << y << endl;
}

其中EM_ASM_INT表示其中包含的JS函数返回值将是一个C/C++中int类型的数据

通过$n的方式表示第n-1个参数,第一个参数即为$0

ESM_ASM_INT方法第二个参数开始即为C/C++中需要传给JS函数的参数

多个参数的情况如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <emscripten.h>
#include <iostream>

using namespace std;

int main () {
int x = 10;
int y = 20;
int z = EM_ASM_INT({
let val = $0 * $1;
console.log("current: " + val);
return val;
}, x, y);
cout << "result: " << x << "," << y << "," << z << endl;
}