使用Emscripten+CMake构建代码的踩坑
大型项目不免要使用CMakeLists.txt来构建项目,本文以编译一个简单的代码库(无引入第三方库)为例,演示直到获取无漏洞构建结果的过程,并阐述了过程中遇到的问题(包括如何处理异常的抛出、如何将本地文件加载到虚拟文件系统等)以及是如何逐步解决的
使用Emscripten+CMake构建代码的踩坑
一、基本编译流程
- 关于Emscripten的安装与环境配置,详见这篇:在Windows安装并测试Emscripten全流程
- 此处以我的一个数据结构代码库中编写完成的CMakeLists.txt为例(两链接均索引到我写该博客时使用的相同提交版本,不受最新提交版本所影响,以便读者复刻),将该仓库克隆到本地
1
git clone https://github.com/WhythZ/DataStructure.git
- 然后在项目根目录打开cmd,新建一个
build
目录后进入
1
2
mkdir build
cd build
- 此时使用如下命令以使用Emscripten的CMake工具链文件来配置项目(
emcmake
是Emscripten提供的一个包装器,它会自动设置CMake使用Emscripten的工具链)
1
emcmake cmake ..
- 然后使用
emmake
来调用make
进行编译(emmake
是另一个Emscripten提供的包装器,它会确保使用Emscripten的工具链进行编译)
1
emmake make
- 此时运行上图中的
Main.js
即可得到程序的输出结果,若修改了代码则需重新编译后运行
二、关于异常捕获
- 如果发现报错了如下内容,这是由于代码中抛出了异常,但Emscripten默认禁用了异常捕获功能,启用异常捕获会增加生成的WebAssembly文件的大小,并可能影响运行时性能
- 若无需异常处理则检查代码逻辑避免抛出异常,也可在
CMakeLists.txt
中的add_executable
前添加如下内容强制启用异常捕获,然后重新执行正常的步骤编译代码,最终得到异常输出
1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sNO_DISABLE_EXCEPTION_CATCHING")
三、文件加载方法
3.1 加载单个文件
- 如上图所示,报错源于如下代码,WebAssembly环境中的使用的是虚拟文件系统,若程序尝试打开本地文件则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SortingManager::SortingManager()
{
//加载测试案例文件中的整数列表
LoadTestCase("../Codes/03-Sorting/IntTestCase.csv");
}
void SortingManager::LoadTestCase(std::string _path)
{
//加载路径下的文件
std::ifstream _file(_path);
//检查是否成功加载
if (!_file.good())
throw std::runtime_error("ERROR: File \"IntTestCase.csv\" Not Found!");
//通过循环来遍历每行内容;getline函数第一参数位接收被读取的文件,第二参数位是存放读取的数据的对象,第三参数位以逗号为分隔符
std::string _gridBuf;
while (std::getline(_file, _gridBuf, ','))
{
srcList.emplace_back(stoi(_gridBuf));
}
//关掉文件
_file.close();
}
- 其中一种解决方式是使用Emscripten的文件预加载功能将所需文件预先加载到虚拟文件系统,首先将被加载的文件放入
build
目录下 - 然后在编译时使用
--preload-file
选项将文件加载到虚拟文件系统中,在CMakeLists.txt
中的add_executable
前添加如下内容
1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sFORCE_FILESYSTEM --preload-file IntTestCase.csv")
- 由于此时的被加载文件处于
build
目录, 但不应将加载路径改为build/IntTestCase.csv
,这是相对于本地文件系统的路径而非Emscripten虚拟文件系统的路径,后者的根目录是/
,所有文件路径都需要相对于这个根目录,所以应将代码中的路径修改为如下形式
1
2
3
4
5
SortingManager::SortingManager()
{
//加载测试案例文件中的整数列表
LoadTestCase("/IntTestCase.csv");
}
- 然后重新执行
emcmake cmake ..
和emmake make
后再运行node Main.js
,程序即可成功加载目标文件并运行,得到如下结果,可以看到基于该.csv
文件运行的代码成功运行了一部分,这表明该文件被成功读取了
- 但是仍有新的报错出现,经搜索得出这大概率是由于代码中出现了内存泄漏或是数组越界等问题,经过排查这是某处数组越界导致的(该漏洞于此条提交中被修复),我们修复该代码后重新编译,再
node Main.js
即可得到无报错的优美输出
- 可以发现成功构建所得的若干文件中,多了一个
Main.data
文件,其包含了预加载到虚拟文件系统中的数据,除了可以包含此处的.csv
配置文件外,还可以是图片、音频等资源文件
3.2 文件放在子目录
- 当资源或配置文件多起来时,首先我不希望将它们零散地放在
build
目录下,那么可以在build
内创建一个Assets
目录,然后将被加载的文件放入,同时修改CMakeLists.txt
内的加载路径
1
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sFORCE_FILESYSTEM --preload-file Assets/IntTestCase.csv")
- 最后修改代码中加载文件的路径,从
/
即虚拟文件系统根目录索引到Assets
目录
1
2
3
4
5
SortingManager::SortingManager()
{
//加载测试案例文件中的整数列表
LoadTestCase("/Assets/IntTestCase.csv");
}
- 然后重新执行必要的两条指令后即可同样构建运行成功
3.3 加载多个文件
- 如果有很多需要被加载的文件,我们最好不应该在
CMakeLists.txt
中为每个文件都进行如下的配置,这样十分繁琐且耦合
1
`set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sFORCE_FILESYSTEM --preload-file Assets/xxx.yyy")`
- 只需通过如下语句加载整个目录即可,无论
Assets/
中有多少文件CMake都会自动加载它们
1
2
# 使用--preload-file加载整个Assets目录
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -sFORCE_FILESYSTEM --preload-file Assets")
四、生成HTML
- 完成上述流程并成功编译后,遂尝试将代码编译为HTML,需在
CMakeLists.txt
中添加如下指令
1
set(CMAKE_EXECUTABLE_SUFFIX ".html")
- 然后同样在
build
中的终端执行完以下两句后,便可得到生成的额外一个Main.html
文件
1
2
emcmake cmake ..
emmake make
- 然后同理先启用本地HTTP服务器,然后在浏览器中打开
http://localhost:8080/Main.html
网页(而不是直接打开文件)即可看到输出结果
1
emrun --no_browser --port 8080 Main.html
本文由作者按照 CC BY-NC-SA 4.0 进行授权