Emscripten 运行时环境与大多数 C/C++ 应用程序所期望的不同。Emscripten 尽力抽象和缓解这些差异,因此一般情况下代码可以编译,几乎不需要更改。
本文将扩展一些差异以及由此产生的 API 限制,并概述您可能需要对 C/C++ 代码进行的少量更改。
Emscripten 为浏览器环境实现了 Simple DirectMedia Layer API (SDL),它提供了对音频、键盘、鼠标、操纵杆和图形硬件的底层访问。使用 SDL 的应用程序通常不需要进行任何输入/输出更改即可在浏览器中运行。
此外,我们对 glut、glfw、glew 和 xlib 的支持有限。
不使用 SDL 或其他 API 的应用程序可以使用 Emscripten 专用的 API 进行输入和输出
很多 C/C++ 代码使用 libc 和 libcxx 中的同步文件系统 API 来访问本地文件系统中的代码。这是有问题的,因为浏览器阻止代码直接访问主机系统上的文件,并且因为 JavaScript 仅在 Web 工作者之外支持异步文件访问。
Emscripten 提供了 libc 和 libcxx 的实现以及 虚拟文件系统,以便可以编译和运行普通的 C/C++ 代码,而无需更改。大多数开发人员只需指定要 打包的文件集,以便在运行时预加载到虚拟文件系统中。
注意
使用虚拟文件系统可以绕过上述限制。文件数据在编译时打包,并在编译代码被允许运行之前使用 异步 JavaScript API 下载到文件系统中。然后,编译代码进行“文件”调用,这些调用实际上只是对程序内存的调用。
默认文件系统 (MEMFS) 将文件存储在内存中,因此页面重新加载时任何更改都会丢失。如果需要更持久地存储文件更改,则开发人员可以挂载 IDBFS 文件系统,该文件系统允许数据在浏览器中持久化。在 node.js 中运行代码时,开发人员可以挂载 NODEFS,以使代码可以直接访问本地文件系统。
Emscripten 还提供了一个 API 来支持 异步文件访问.
有关更多信息和示例,请参阅 文件和文件系统.
浏览器事件模型使用协作式多任务处理——每个事件都有一个“轮次”来运行,然后必须将控制权返回给浏览器,以便处理其他事件。HTML 页面挂起的常见原因是 JavaScript 未完成并返回控制权给浏览器。
图形 C++ 应用程序通常在无限循环中运行。在循环的每次迭代中,应用程序执行事件处理、处理和渲染,然后延迟(“等待”)以保持帧速率恒定。这个无限循环在浏览器环境中是一个问题,因为没有办法将控制权返回给浏览器,以便其他代码可以运行。经过一段时间后,浏览器会通知用户页面卡住,并提供停止或关闭页面的选项。
同样,WebGL 等 JavaScript API 只能在当前“轮次”结束后运行,并且会在该时刻自动渲染和交换缓冲区。这与您需要手动交换缓冲区的 OpenGL C++ 应用程序形成对比。
解决此问题的标准方法是定义一个 C 函数来执行主循环的一次迭代(不包括“延迟”)。对于本机构建,此函数可以在无限循环中调用,从而有效地保持行为不变。
在 Emscripten 编译的代码中,我们使用 emscripten_request_animation_frame_loop()
来获取环境以在渲染帧的适当频率下调用此相同函数(也就是说,如果浏览器以 60fps 渲染,它将每秒调用它 60 次)。迭代仍然“无限”地运行,但现在其他代码可以在迭代之间运行,并且浏览器不会挂起。
通常您将有一小部分使用 #ifdef __EMSCRIPTEN__
用于这两种情况。例如
#include <emscripten.h>
#include <emscripten/html5.h>
#include <stdio.h>
// Our "main loop" function. This callback receives the current time as
// reported by the browser, and the user data we provide in the call to
// emscripten_request_animation_frame_loop().
bool one_iter(double time, void* userData) {
// Can render to the screen here, etc.
puts("one iteration");
// Return true to keep the loop running.
return true;
}
int main() {
#ifdef __EMSCRIPTEN__
// Receives a function to call and some user data to provide it.
emscripten_request_animation_frame_loop(one_iter, 0);
#else
while (1) {
one_iter();
// Delay to keep frame rate constant (using SDL).
SDL_Delay(time_to_next_frame());
}
#endif
}
注意
emscripten_set_main_loop()
提供了更全面的 API,它允许您指定调用该函数的频率以及其他内容。
注意
只是渲染单个帧并停止。您还应该注意
当前 Emscripten 的 SDL_QUIT
实现如果使用 emscripten_set_main_loop()
将起作用。当页面关闭时,它将强制对主循环进行最终的直接调用,使其有机会注意到 SDL_QUIT
事件。如果您没有使用主循环,您的应用程序将在您有机会注意到此事件之前关闭。
页面关闭时(在 onunload
中),您可以执行的操作有限制。浏览器在此时禁止某些操作,例如显示警报。
另一个选择是使用 Asyncify,它将重写程序,以便它可以通过简单地调用 emscripten_sleep()
返回浏览器的主要事件循环。请注意,这种重写会导致大小和速度开销,而前面提到的 emscripten_request_animation_frame_loop / emscripten_set_main_loop
则不会。
加载 Emscripten 编译的应用程序后,它将首先在 preloading
阶段准备数据。您标记为 预加载 的文件(使用 emcc --preload-file
,或者使用 JavaScript 手动使用 FS.createPreloadedFile()
)将在此阶段设置。
您可以使用 addRunDependency()
添加其他操作,这是一个在编译代码可以运行之前执行的所有依赖项的计数器。当这些操作完成后,您可以调用 removeRunDependency()
来删除已完成的依赖项。
注意
通常不需要添加其他操作——预加载适用于几乎所有用例。
当所有依赖项都满足时,Emscripten 将调用 run()
,进而调用您的 main()
函数。 main()
函数应用于执行初始化任务,通常会调用 emscripten_set_main_loop()
(如 上文所述)。主循环函数将以请求的频率被调用。
您可以通过多种方式影响主循环的操作
emscripten_push_main_loop_blocker()
添加一个函数,该函数会 **阻塞** 主循环,直到阻塞器完成。
例如,这对于管理加载新游戏关卡非常有用。关卡完成后,您可以为每个参与的操作(解压缩文件、生成数据结构等)推入阻塞器。当所有阻塞器都完成时,主循环将恢复,游戏将运行新关卡。您也可以将此函数与 emscripten_set_main_loop_expected_blockers()
结合使用,以便让用户了解进度。
emscripten_pause_main_loop()
会暂停主循环,而 emscripten_resume_main_loop()
会恢复它。这些是阻塞器函数的低级(不太推荐)替代方案。
emscripten_async_call()
允许您在特定时间间隔后调用函数。这将使用 requestAnimationFrame
(默认情况下)或 setTimeout
(如果请求了特定时间间隔)。
浏览器执行环境参考 (emscripten.h) 描述了一些其他用于控制执行的方法。
在 asm.js 和 WebAssembly 中,Emscripten 都以类似于原生架构的方式表示内存。指针代表内存中的偏移量,结构体使用与通常相同量的地址空间,等等。
在 WebAssembly 中,这是使用 WebAssembly.Memory
实现的,它专为此目的而设计。在 asm.js 中,Emscripten 使用单个 类型化数组,不同的 *视图* 提供对不同类型的访问(HEAPU32
用于 32 位无符号整数,等等)。
Emscripten 过去曾尝试过其他内存表示方式,最终选择了如上所述的 JS 和 asm.js 的“类型化数组模式 2”方法,然后 WebAssembly 实现了一些类似的东西。