Emscripten 运行时环境

Emscripten 运行时环境与大多数 C/C++ 应用程序所期望的不同。Emscripten 尽力抽象和缓解这些差异,因此一般情况下代码可以编译,几乎不需要更改。

本文将扩展一些差异以及由此产生的 API 限制,并概述您可能需要对 C/C++ 代码进行的少量更改。

输入/输出

Emscripten 为浏览器环境实现了 Simple DirectMedia Layer API (SDL),它提供了对音频、键盘、鼠标、操纵杆和图形硬件的底层访问。使用 SDL 的应用程序通常不需要进行任何输入/输出更改即可在浏览器中运行。

此外,我们对 glutglfwglewxlib 的支持有限。

不使用 SDL 或其他 API 的应用程序可以使用 Emscripten 专用的 API 进行输入和输出

  • html5.h,它定义了 Emscripten 与本机代码交互以处理 HTML5 事件的底层粘合绑定,包括对键、鼠标、滚轮、设备方向、电池电量、振动等的访问。

  • 多媒体和图形 API,包括 OpenGLEGL.

文件系统

很多 C/C++ 代码使用 libclibcxx 中的同步文件系统 API 来访问本地文件系统中的代码。这是有问题的,因为浏览器阻止代码直接访问主机系统上的文件,并且因为 JavaScript 仅在 Web 工作者之外支持异步文件访问。

Emscripten 提供了 libclibcxx 的实现以及 虚拟文件系统,以便可以编译和运行普通的 C/C++ 代码,而无需更改。大多数开发人员只需指定要 打包的文件集,以便在运行时预加载到虚拟文件系统中。

注意

使用虚拟文件系统可以绕过上述限制。文件数据在编译时打包,并在编译代码被允许运行之前使用 异步 JavaScript API 下载到文件系统中。然后,编译代码进行“文件”调用,这些调用实际上只是对程序内存的调用。

默认文件系统 (MEMFS) 将文件存储在内存中,因此页面重新加载时任何更改都会丢失。如果需要更持久地存储文件更改,则开发人员可以挂载 IDBFS 文件系统,该文件系统允许数据在浏览器中持久化。在 node.js 中运行代码时,开发人员可以挂载 NODEFS,以使代码可以直接访问本地文件系统。

Emscripten 还提供了一个 API 来支持 异步文件访问.

有关更多信息和示例,请参阅 文件和文件系统.

浏览器主循环

浏览器事件模型使用协作式多任务处理——每个事件都有一个“轮次”来运行,然后必须将控制权返回给浏览器,以便处理其他事件。HTML 页面挂起的常见原因是 JavaScript 未完成并返回控制权给浏览器。

图形 C++ 应用程序通常在无限循环中运行。在循环的每次迭代中,应用程序执行事件处理、处理和渲染,然后延迟(“等待”)以保持帧速率恒定。这个无限循环在浏览器环境中是一个问题,因为没有办法将控制权返回给浏览器,以便其他代码可以运行。经过一段时间后,浏览器会通知用户页面卡住,并提供停止或关闭页面的选项。

同样,WebGL 等 JavaScript API 只能在当前“轮次”结束后运行,并且会在该时刻自动渲染和交换缓冲区。这与您需要手动交换缓冲区的 OpenGL C++ 应用程序形成对比。

在 C/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,它允许您指定调用该函数的频率以及其他内容。

注意

使用 SDL 时,您通常需要设置主循环,除非您

只是渲染单个帧并停止。您还应该注意

  • 当前 Emscripten 的 SDL_QUIT 实现如果使用 emscripten_set_main_loop() 将起作用。当页面关闭时,它将强制对主循环进行最终的直接调用,使其有机会注意到 SDL_QUIT 事件。如果您没有使用主循环,您的应用程序将在您有机会注意到此事件之前关闭。

  • 页面关闭时(在 onunload 中),您可以执行的操作有限制。浏览器在此时禁止某些操作,例如显示警报。

使用 Asyncify 向浏览器让步

另一个选择是使用 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) 描述了一些其他用于控制执行的方法。

Emscripten 内存表示

在 asm.js 和 WebAssembly 中,Emscripten 都以类似于原生架构的方式表示内存。指针代表内存中的偏移量,结构体使用与通常相同量的地址空间,等等。

在 WebAssembly 中,这是使用 WebAssembly.Memory 实现的,它专为此目的而设计。在 asm.js 中,Emscripten 使用单个 类型化数组,不同的 *视图* 提供对不同类型的访问(HEAPU32 用于 32 位无符号整数,等等)。

Emscripten 过去曾尝试过其他内存表示方式,最终选择了如上所述的 JS 和 asm.js 的“类型化数组模式 2”方法,然后 WebAssembly 实现了一些类似的东西。