尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Emscripten API 限制全解析:浏览器环境下的网络、文件系统与主循环移植指南

Emscripten API 限制全解析:浏览器环境下的网络、文件系统与主循环移植指南 Emscripten API 限制全解析浏览器环境下的网络、文件系统与主循环移植指南【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscriptenEmscripten 是一个 LLVM-to-WebAssembly 编译器能够让 C/C 代码运行在浏览器和 Node.js 环境中。然而浏览器运行时与原生环境存在根本性差异这给原生 API 的调用方式带来了诸多限制。本文以官方文档 API Limitations 为骨架结合 Emscripten 源码与配套文档系统梳理网络、文件系统、应用主循环三大方面的限制及其应对方案帮助你写出真正可移植、可落地的 C/C 代码。限制的根源浏览器运行时与原生环境的差异Emscripten 编译出的代码运行在浏览器沙箱中其运行时环境与大多数 C/C 应用所预期的环境不同。官方文档在 API Limitations 中明确指出浏览器环境和 JavaScript 与 C/C 通常运行的原生环境不同这些差异对原生 API 的调用方式施加了限制。Emscripten 做了大量抽象来缓解这些差异使得大部分代码无需修改即可编译运行但理解这些限制依然是移植工作的第一步。这些差异主要可以归结为三点JavaScript 的网络与文件 API 本质上是异步的不存在真正的同步阻塞式系统调用浏览器沙箱禁止代码直接访问宿主机的本地文件系统取而代之的是一个虚拟文件系统浏览器事件模型采用协作式多任务co-operative multitasking每个事件只能运行一个回合turn必须把控制权交还给浏览器事件循环否则页面会挂起。下面逐一展开这三类限制以及对应的实战解决方案。网络限制只能使用异步非阻塞操作为什么必须异步文档 API Limitations 指出Emscripten 支持 libc 的网络函数但你必须把自己限制在异步非阻塞操作上因为底层 JavaScript 网络函数是异步的。从实现上看浏览器中不存在真正的同步 TCP/UDP 系统调用。文档 Networking 详细列出了 Emscripten 提供的几种网络方案每一种都建立在异步模型之上Emscripten WebSockets API面向连接、消息分帧、双向异步的通信方式是网页能访问的最接近 TCP 的方案。通过系统头文件system/include/emscripten/websocket.h暴露给 C/C链接时需要加-lwebsocket.js。Emulated POSIX TCP Sockets over WebSockets默认构建模式将现有 POSIX Sockets 代码模拟成 WebSocket 协议传输服务端需要 WebSockify 之类的代理来接收 WebSocket 连接。官方同时提醒这种模拟目前并不完整很可能需要修改代码来适配其限制。POSIX Sockets on Node.js使用-sNODERAWSOCKETS链接标志在 Node.js 环境下用真实的主机 TCP、UDP 和AF_UNIX流式套接字支撑 POSIX sockets API。值得注意的是阻塞套接字无法真正阻塞——一个需要等待的操作会以EAGAIN失败connect()会提前返回0send()则无限缓冲。官方建议应用使用非阻塞套接字配合poll()或epoll详见 networking.rst。Full POSIX Sockets over WebSocket Proxy Server位于tools/websocket_to_posix_proxy/目录的代理服务器把浏览器的 POSIX Sockets 调用逐一转发给代理执行原生 TCP/UDP 调用。这种方式可以支持完整的 TCP/UDP 连接、监听端口甚至主机名解析但每个 API 调用都被单独代理因此可能很慢主要用于测试基础设施和调试。参考客户端示例见 test/websocket/tcp_echo_client.c。XHR 与 FetchHTTP 传输使用浏览器内置的 XmlHttpRequest 和 Fetch APIEmscripten 提供了emscripten_async_wget*()C API 和 Fetch API 作为透传封装其函数声明位于system/include/emscripten/wget.h。WebRTC 与 WebTransport浏览器没有直接 UDPWebRTC Data Channel 可作为 UDP 类通信的替代但 Emscripten 目前未提供 C/C API。实战要点移植网络代码时核心原则是把同步阻塞的recv()/accept()/connect()调用重写为基于回调或事件驱动poll()/epoll的异步模式。在 pthread 中调用poll()/epoll等待是可行的使用 Asyncify 也能让阻塞式代码在遇到等待时自动让出事件循环。文件系统限制虚拟文件系统与打包策略同步文件 API 如何在沙箱中存活文档 API Limitations 指出Emscripten 支持 libc 文件系统函数C/C 代码可以用正常方式编写。浏览器中运行的代码被沙箱化无法直接访问本地文件系统。Emscripten 的解决方案是创建一个虚拟文件系统它可以被预加载数据也可以链接到 URL 进行惰性加载。这套架构在 File System Overview 中有详细说明可移植的原生代码通常调用libc和libcxx中的同步文件 API这些调用最终落到 File System API默认使用MEMFS虚拟文件系统。运行时初始化时MEMFS被挂载在/要加入的文件在编译期用emcc指定页面首次加载时由 JavaScript 用同步 XHR 异步下载编译后的代码只有在异步下载完成、文件就绪之后才会被允许运行。以下架构图清晰地展示了这一分层关系从源码可以印证这一分层src/lib/libmemfs.js中MEMFS.mount()在根目录创建节点createNode()为目录、文件、链接、字符设备分别注册node_ops与stream_ops文件数据实际存储在一个Uint8Array类型的contents字段中libmemfs.js。也就是说编译后的代码对文件的调用本质上是对程序内存中字节数组的操作。三种虚拟文件系统后端后端适用环境特性数据持久性MEMFS默认浏览器 / Node.js全部数据保存在内存中性能最佳页面刷新后数据全部丢失IDBFS浏览器基于 IndexedDB 持久化数据可跨页面会话保留NODEFSNode.js直接映射到 Node.js 的真实文件系统持久化且是唯一能直接访问本地磁盘的方案Runtime Environment 文档强调默认的 MEMFS 把所有文件保存在内存中任何修改在页面刷新后都会丢失需要持久化时可在浏览器挂载 IDBFS在 Node.js 下挂载 NODEFS。用 emcc 打包文件preload 与 embedPackaging Files 给出了两种打包方式预加载preloading文件打包成独立的.data数据文件页面加载时单独下载emcc file.cpp -o file.html --preload-file asset_dir该命令生成file.html、file.js和file.data其中.data包含asset_dir/下的全部文件由file.js负责加载。参考测试代码见 test/hello_world_file.cpp。嵌入embedding文件内容直接嵌入file.js没有额外的下载文件效率更高但数据无法单独托管emcc file.cpp -o file.html --embed-file asset_dir默认情况下打包文件应位于编译命令所在目录之下运行时虚拟文件系统的根目录对应编译时的工作目录。也可以用符号显式指定资源在虚拟文件系统中的位置# 将编译目录之外的 asset_dir 映射到虚拟文件系统根目录 / emcc file.cpp -o file.html --preload-file ../../asset_dir/ # 将 ../res/gen123.png 映射为 /main.png emcc file.cpp -o file.html --embed-file ../res/gen123.pngmain.png此外还有两个实用工具手动运行 file packagertools/file_packager.py可以独立于编译过程打包生成.data和.js文件生成的.js必须在主编译代码之前加载。支持--exclude模式过滤Pythonfnmatch语法!开头为反向模式python tools/file_packager.py assets.data \ --preload assets \ --exclude *.psd *.tmp !assets/keep.tmp \ --js-outputassets.jsModule.locateFile当.data文件需要放在 CDN 等不同位置时在加载数据文件之前的script元素中指定该函数返回数据文件的 URL。打包注意事项文件名合法字符集A-Z、a-z、0-9、空格以及!#$%(),-.;[]^_{}~*?|仅在宿主文件系统支持时可用命令行中的需写成转义/、、:不可用。只打包应用实际需要的文件以减小下载体积、提升启动速度。可以通过Module.logReadFiles需加入INCOMING_MODULE_JS_API或检查编译产物中的FS.readFiles对象来监控运行时实际读取了哪些文件详见 packaging_files.rst。配合--use-preload-plugins可根据扩展名自动解码图片.jpg/.png/.bmp、音频.ogg/.wav/.mp3和动态库.so便于 SDL 的IMG_Load、Mix_LoadWAV以及后续的dlopen使用。应用主循环限制协作式多任务与异步主循环为什么无限循环会挂起页面文档 API Limitations 指出浏览器事件模型使用协作式多任务每个事件有一个回合运行之后必须把控制权交还给浏览器事件循环以便处理其他事件。HTML 页面挂起的常见原因就是 JavaScript 没有完成并交还控制权。图形化的 C 应用通常运行在无限循环中每次迭代处理事件、渲染然后等待以保持帧率恒定。这个无限循环在浏览器中是个问题——没有途径把控制权交还给浏览器一段时间后浏览器会提示页面无响应并建议终止页面。同理WebGL 等 JavaScript API 只能在当前回合结束时自动渲染并交换缓冲区而原生 OpenGL 应用需要手动swapBuffers。标准解法单次迭代函数 帧回调Runtime Environment 给出了标准方案定义一个只执行主循环单次迭代不含等待的 C 函数在原生构建中放进无限循环调用在 Emscripten 构建中交给环境按帧率回调。典型的双平台写法如下#include emscripten.h #include emscripten/html5.h #include stdio.h // 主循环单次迭代回调。返回 true 表示继续循环。 bool one_iter(double time, void* userData) { // 可以在这里渲染到屏幕 puts(one iteration); return true; } int main() { #ifdef __EMSCRIPTEN__ // 让浏览器按渲染帧率如 60fps调用 one_iter emscripten_request_animation_frame_loop(one_iter, 0); #else while (1) { one_iter(); // 原生端自行延迟以保持帧率例如 SDL_Delay SDL_Delay(time_to_next_frame()); } #endif }函数emscripten_request_animation_frame_loop的声明位于 system/include/emscripten/html5.h回调参数为bool (*cb)(double time, void *userData)time是浏览器报告的时间戳返回true保持循环继续。更完整的 API 是emscripten_set_main_loop(em_callback_func func, int fps, bool simulate_infinite_loop)声明见 system/include/emscripten/emscripten.h可指定回调频率等参数。文档还提醒两点 SDL 相关细节SDL_QUIT事件只有在使用emscripten_set_main_loop时才能可靠地捕获页面关闭时会强制做一次最终回调不使用主循环的话应用会在你察觉该事件前就关闭。页面关闭onunload阶段能做的事情有限例如浏览器此时禁止弹出 alert。备选方案Asyncify 让出事件循环另一种做法是使用 Asyncify它重写程序使代码只需调用emscripten_sleep()声明见 system/include/emscripten/emscripten.h就能让出控制权、返回浏览器事件循环。代价是重写会带来体积和速度开销而emscripten_request_animation_frame_loop/emscripten_set_main_loop方案没有这类开销。因此能改造为回调式主循环时应优先采用回调方案。执行生命周期与主循环控制当 Emscripten 编译的应用加载时首先进入preloading阶段通过emcc --preload-file标记预加载的文件或用 JavaScript 手动调用FS.createPreloadedFile都在此阶段就绪。额外的操作可通过addRunDependency注册为运行依赖完成后调用removeRunDependency移除所有依赖满足后Emscripten 才会调用程序的main()通常main()中会调用emscripten_set_main_loop完成初始化。主循环运行期间还可以用以下 API 控制执行流程emscripten_push_main_loop_blocker向主循环压入一个阻塞函数直到其完成才恢复主循环。典型场景是加载新游戏关卡——关卡完成后为解包文件、生成数据结构等每个动作压入 blocker全部完成后主循环恢复运行新关卡可配合emscripten_set_main_loop_expected_blockers向用户展示进度。emscripten_pause_main_loop/emscripten_resume_main_loop暂停与恢复主循环属于低层级不推荐的替代方案。emscripten_async_call在指定间隔后调用一个函数默认走requestAnimationFrame指定间隔时使用setTimeout。其他 API 的可移植性文档 API Limitations 最后指出对其他可移植的 C/C 代码Emscripten 的支持相当全面。但这句全面有明确的边界条件Portability Guidelines 对此做了详细展开无法编译、必须重写的代码依赖**大端big-endian**架构的代码——Emscripten 目前要求小端主机因为 JavaScript 类型数组遵循宿主字节序LLVM 也需要确定目标端序使用原生环境底层特性的代码例如配合setjmp/longjmp做原生栈操作向下跳栈的合法setjmp/longjmp是受支持的向上跳转到已展开的栈属于未定义行为扫描寄存器或栈的代码——变量可能保存在 JavaScript 局部变量中而无法被扫描如需保守式 GC 扫描只能在主事件循环迭代等栈上无其他代码的时机进行或使用 Binaryen 的 SpillPointers pass含架构相关内联汇编的代码如包含 x86 指令的asm()需要替换为可移植 C/C。能编译但可能运行较慢的代码asm.js而非 WebAssembly下的 64 位int变量——数学运算因模拟而变慢C 异常——会迫使 JS 引擎关闭部分优化因此在-O1及以上默认关闭异常捕获可用-sDISABLE_EXCEPTION_CATCHING0重新开启setjmp会阻止 relooping导致用较低效的方式模拟控制流。其他问题依赖 x86 对齐行为的代码——x86 允许非对齐读写而 32 位 ARM 会抛SIGILLasm.js 的加载/存储被强制对齐WebAssembly 的非对齐访问可以工作但可能较慢构建时开启SAFE_HEAP1可获得清晰的运行时异常提示。移植工作流总结结合文档与源码一个稳妥的 Emscripten 移植流程可以归纳为四步评估可移植性对照 Portability Guidelines 排查大端依赖、内联汇编、栈扫描等硬伤改造主循环把无限循环重构为单次迭代回调用emscripten_set_main_loop或emscripten_request_animation_frame_loop驱动或用 Asyncify 兜底处理文件用--preload-file/--embed-file打包资源按需挂载 IDBFS浏览器持久化或 NODEFSNode.js 直连本地磁盘重写网络层将阻塞式套接字改为异步回调或poll()/epoll事件驱动或选用 Emscripten 的 WebSockets / Fetch 透传 API。掌握了浏览器沙箱的这三重限制及其官方应对方案绝大多数 C/C 代码都可以在几乎没有改动的情况下被 Emscripten 编译到 WebAssembly平稳运行在浏览器与 Node.js 环境中。【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscripten创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表