Emscripten代码移植之Emscripten的运行时环境(二)

翻译:云荒杯倾

Emscripten运行时环境与大多数C/C++应用程序所期望的环境不同。Emscripten努力抽象和减轻这些差异,因此在一般的代码中,可以用很少或没有更改来编译。

本文描述了一些差异和由此产生的API限制,并概述了您可能需要对C/C++代码做出的一些更改。

1、输入/输出

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

不过,我们对glut, glfw, glew 和 xlib等库有一些支持上的限制。

不使用SDL或其他api的应用程序可以使用Emscripten特有的API来输入和输出,他们是:

  • html5.h,它定义了Emscripten底层胶水绑定以使得本地代码可以与HTML5事件进行交互,包括访问键盘、鼠标、滚轮、设备方向、电池级别、振动等。
  • 多媒体和图形API,包括OpenGL和EGL。

2、文件系统

很多C/C++代码使用libc和libcxx中的同步文件系统APIs来访问本地文件系统中的代码。这是有问题的,因为浏览器是不会让代码直接访问主机系统上的文件的,而且JavaScript只支持异步文件访问(在web worker之外)。

Emscripten 提供了libc和libcxx的一种实现,并且提供了一个虚拟文件系统,这样正常的C/C++代码可以在不需要修改的情况下被编译然后运行。开发者需要做的只是将文件集合在预加载阶段打包放到这个虚拟文件系统。

1
2
3
NOTE:
文件数据在 “编译期间” 打包,并在 “ 代码运行之前 ” 使用异步JavaScript api下载到虚拟文件系统中。
编译后的代码发出“文件”调用操作,实际上只是调用程序内存。

默认文件系统(MEMFS)在内存存储文件,如果文件做了修改但是页面reload了,则修改的工作就白做了。如果想永久地存储文件的更改,那么开发人员可以安装IDBFS文件系统,该系统允许在浏览器中持久化数据。
如果是在node.js中运行代码,开发人员可以安装NODEFS,它可以让代码直接访问本地文件系统。

Emscripten也有一个API支持同步文件获取。

更多信息

3、浏览器主循环

浏览器事件模型使用合作模式的多任务处理——每个事件都有一个运行的“turn”,然后必须将控制权返回给浏览器事件循环,这样其他事件就可以处理了。HTML页面挂起的一个常见原因是JavaScript未完成并且未将控制权返回给浏览器。

图形化C++应用程序通常在一个无限循环中运行。在循环的每个迭代中,应用程序执行事件获取、处理和渲染,接着是延迟(“等待”)以保持帧速率为常数。无限循环在浏览器环境中是一个问题,因为没有办法把控制权返回到浏览器以执行其他代码。通常是在一段时间之后,浏览器将通知用户该页面被卡住并提供停止或关闭它的操作。

同样,js中的webGL API也需要当上一个事件完成之后,才开始运行的,然后自动渲染并且交换缓冲区。不过OpenGL C++程序是要手动交换缓冲区的。

C/C++中实现异步主循环

这个问题的标准解决方案是定义一个C函数,它执行主循环的一次迭代(不包括“延迟”)。对于本机编译,可以在无限循环中调用这个函数,从而有效地保持行为不变。

但在Emscripten编译的代码中,我们使用emscripten_set_main_loop()来获得环境,从而以指定频率调用相同的函数。迭代仍然是“无限的”,但现在可以在迭代之间运行其他代码,而且浏览器不会挂起。

下面给个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main() {
...
#ifdef __EMSCRIPTEN__
// void emscripten_set_main_loop(em_callback_func func, int fps, int simulate_infinite_loop);
emscripten_set_main_loop(one_iter, 60, 1);
#else
while (1) {
one_iter();
// Delay to keep frame rate constant (using SDL)
SDL_Delay(time_to_next_frame());
}
#endif
}
// The "main loop" function.
void one_iter() {
// process input
// render to screen
}

1
2
3
4
5
6
7
NOTE:
当你使用SDL时,您可能需要设置主循环。你也应该注意:
1. As the page is shut, it will force a final direct call to the main loop, giving it a chance to
notice the SDL_QUIT event. If you do not use a main loop, your app will close before you have
had an opportunity to notice this event.
2.There are limitations to what you can do as the page shuts (in onunload). Some actions like
showing alerts are banned by browsers at this point.

4、执行生命周期

当你的Emscripten编译后程序已经加载完毕后,就开始预加载文件的阶段。(可以通过emcc –preload-file设置要预加载的文件,也可以使用 FS.createPreloadedFile()手动加载),在这个阶段进行文件安装。

您可以使用addRunDependency()添加额外的操作,这是一个依赖项计数器,他要在编译后的代码运行之前执行。当这些已经完成,您可以调用removeRunDependency()来删除已完成的依赖项。

1
一般来说,不需要添加额外的操作,预加载几乎能满足你的需求。

当满足所有依赖项时,Emscripten将调用run()函数,run()函数将调用您的main()函数。main()函数应该用于执行初始化任务,并且通常main()要调用emscripten_set_main_loop()(如上所述)。然后主循环函数将按请求的频率被调用。

您可以通过多种方式影响主循环的操作:

  • emscripten_push_main_loop_blocker()添加一个函数,该函数阻塞主循环,直到拦截器完成为止。举个例子,当你加载一个新的游戏级别的时候,这是有用的。一个游戏级别完成,你放一个一些阻塞块来做解包文件,生成数据结构
    等操作。当所有阻塞块完成,主循环就可以重新开始一个新等级的游戏。emscripten_push_main_loop_blocker()可以和emscripten_set_main_loop_expected_blockers()函数一起使用,这能让用户知道进度。

  • emscripten_pause_main_loop()会暂停主循环,emscripten_resume_main_loop()重新开始主循环。不推荐使用这两个函数,因为他们其实是blocker函数(上面那条)的更底层替代品。

*emscripten_async_call()让你在特定间隔调用一个函数。这个是在模拟requestAnimationFrame(默认)和setTimeout。

这里有很多其他方法来控制执行

5、Emscripten中内存表示

Emscripten中内存模型是Typed Arrays Mode 2。他使用单种类型数组表达内存,不过提供了多种类型视图来读取内存。(HEAPU8、HEAPU16、HEAPU32等)。

1
2
3
NOTE:
Typed Arrays Mode 2是Fastcomp编译器支持的唯一内存模型,它是旧编译器的默认内存模型。
与本项目所尝试的其他内存模型相比,它适用于许多的任意编译后的代码,而且速度相对较快。

这个模型以与普通C和C++相同的方式在内存中lays out items,它和C/C++使用相同数量的内存。

这个模型允许您使用违背 load-store consistency假设的代码。由于不同的视图显示了相同的数据,所以您可以(比方说)编写一个32位的整数,然后从中间读取一个字节,它就像在大多数平台上本地构建C或C++那样工作。


Emscripten代码移植系列文章

Emscripten代码移植主题系列文章是emscripten中文站点的一部分内容。
本文是第二个主题。
第一个主题介绍代码可移植性与限制
第二个主题介绍Emscripten的运行时环境
第三个主题第一篇文章介绍连接C++和JavaScript
第三个主题第二篇文章介绍embind
第四个主题介绍文件和文件系统
第六个主题介绍Emscripten如何调试代码