译文:云凰北青本文为Emscripten-WebAssembly专栏系列之一。更多文章请查看专栏。也可以去作者的博客阅读文章。Emscripten运行时环境不同于大多数C/C++应用程序所期望的。Emscripten努力抽象和减轻这些差异,以便一般代码可以在很少或没有更改的情况下编译。本文介绍了一些差异和由此产生的API限制,并概述了您可能需要对C/C++代码进行的一些更改。1.输入/输出Emscripten为浏览器环境实现了一个简单的DirectMedia层API(SDL),它提供了对音频、键盘、鼠标、操纵杆和图形硬件的低级访问。使用SDL的应用程序通常不需要更改I/O即可在浏览器中运行。但是,我们对glut、glfw、glew和xlib等库有一些支持限制。不使用SDL或其他API的应用程序可以使用Emscripten特定的API进行输入和输出。它们是:html5.h,它定义了Emscripten的底层胶水绑定,以便本机代码可以与HTML5事件交互,包括访问键盘、鼠标、滚轮、设备方向、电池电量、振动等。多媒体和图形API,包括OpenGL和EGL。2.文件系统许多C/C++代码使用libc和libcxx中的同步文件系统API来访问本地文件系统中的代码。这是有问题的,因为浏览器不允许代码直接访问主机系统上的文件,而JavaScript只支持异步文件访问(在网络工作者之外)。Emscripten提供了libc和libcxx的实现,并提供了一个虚拟文件系统,使得普通的C/C++代码无需修改即可编译运行。开发人员需要做的就是在预加载阶段将文件集合打包到这个虚拟文件系统中。注意:文件数据在“编译期间”打包,并使用异步JavaScriptapi“在代码运行之前”下载到虚拟文件系统。编译后的代码发出“文件”调用操作,实际上只是对程序内存的调用。默认文件系统(MEMFS)将文件存储在内存中。如果修改了文件,但是重新加载了页面,那么修改工作就白费了。如果你想永久存储对文件的更改,开发人员可以安装IDBFS文件系统,它可以让数据持久保存在浏览器中。如果在node.js中运行代码,开发人员可以安装NODEFS,它可以让代码直接访问本地文件系统。Emscripten还有一个支持同步文件获取的API。更多信息3.浏览器主循环浏览器事件模型使用协作式多任务处理——每个事件都有一个运行的“回合”,然后必须将控制返回给浏览器事件循环,以便其他事件可以得到处理。HTML页面挂起的一个常见原因是JavaScript未完成且未将控制权返回给浏览器。图形C++应用程序通常在无限循环中运行。在循环的每次迭代中,应用程序执行事件获取、处理和渲染,然后延迟(“等待”)以保持帧速率恒定。无限循环是浏览器环境中的一个问题,因为无法将控制返回给浏览器以执行其他代码。通常一段时间后,浏览器会通知用户页面卡住,并提出停止或关闭。同样,js中的webGLAPI也需要在前面的事件完成后开始运行,然后自动渲染和交换缓冲区。但是,OpenGLC++程序需要手动交换缓冲区。在C/C++中实现异步主循环问题的标准解决方案是定义一个C函数来执行主循环的一次迭代(不包括“延迟”)。对于本机编译,可以在无限循环中调用此函数,从而有效地保持行为不变。但是在Emscripten编译的代码中,我们使用emscripten_set_main_loop()来让环境以指定的频率调用同一个函数。迭代仍然是“无限”的,但现在可以在迭代之间运行其他代码而不会导致浏览器挂起。这是一个例子:intmain(){...#ifdef__EMSCRIPTEN__//voidemscripten_set_main_loop(em_callback_funcfunc,intfps,intsimulate_infinite_loop);emscripten_set_main_loop(one_iter,60,1);#elsewhile(1){one_iter();//延迟以保持帧速率恒定(使用SDL)SDL_Delay(time_to_next_frame());}#endif}//“主循环”function.voidone_iter(){//处理输入//渲染到屏幕}注意:当您使用SDL时,您可能需要设置主循环。您还应该注意:1.当页面关闭时,它会强制最终直接调用主循环,让它有机会注意到SDL_QUIT事件。如果您不使用主循环,您的应用程序将在您有机会注意到此事件之前关闭。2.页面关闭时(在onunload中)您可以做什么是有限制的。此时浏览器禁止显示警报等某些操作。4.当你的Emscripten编译好的程序已经加载完成后,预加载文件的阶段就开始了。(可以通过emcc--preload-file设置文件预加载,或者使用FS.createPreloadedFile()手动加载),这个阶段安装文件。您可以使用addRunDependency()添加其他操作,它是在运行已编译代码之前要执行的依赖项计数器。当这些都完成后,您可以调用removeRunDependency()来删除已完成的依赖项。总的来说,不需要增加额外的操作,预加载几乎可以满足你的需求。当满足所有依赖项时,Emscripten将调用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()将重新启动主循环。不推荐使用这两个函数,因为它们实际上是阻塞函数(上面那个)的低级替代品。*emscripten_async_call()允许您以特定时间间隔调用函数。这是模拟requestAnimationFrame(默认)和setTimeout。还有很多其他的方式来控制执行5.Emscripten中的内存表示Emscripten中的内存模型是TypedArraysMode2,他使用单一类型的数组来表示内存,但是提供了多种类型的视图来读取内存。(HEAPU8、HEAPU16、HEAPU32等)。注意:TypedArraysMode2是Fastcomp编译器支持的唯一内存模型,它是旧编译器的默认内存模型。它适用于大量任意编译代码,并且与本项目中尝试的其他内存模型相比速度相对较快。该模型以与普通C和C++相同的方式在内存中布置项目,并且它使用与C/C++相同的内存量。该模型允许您使用违反加载-存储一致性假设的代码。由于不同的视图显示相同的数据,您可以(比如说)写入一个32位整数,然后从中间读取一个字节,它在大多数平台上的工作方式类似于C或C++的本机构建。Emscripten代码移植系列文章Emscripten代码移植主题系列是emscripten中文站点的一部分。第一篇介绍代码的可移植性和局限性第二篇介绍Emscripten的运行环境第三篇第一篇介绍连接C++和JavaScript第三篇第二篇介绍embind第四篇介绍文件和文件系统第六篇介绍Emscripten如何调试代码
