ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

iOS 上跑 Windows 程序:Wine+FEX-Emu+DXMT 四层翻译架构实战

iOS 上跑 Windows 程序:Wine+FEX-Emu+DXMT 四层翻译架构实战 1. 项目缘起为什么要在 iOS 上折腾 Wine第一次听到“Madeira”这个代号是在一个折腾跨平台兼容层的群里。有人丢出一张截图iPhone 上跑着一个 Windows 老程序界面糊是糊了点但确实点得动、能输入。底下有人问这是什么方案答曰“Wine 那一套套了个 iOS 的壳”。后来我自己顺着线索摸下去才发现这条链路比想象中长Wine 负责把 Windows 的 PE 可执行文件翻译成 POSIX 调用FEX-Emu 负责把 x86-64 指令翻译成 ARM64 指令DXMT 负责把 Direct3D 翻译成 Metal最后再塞进 iOS 的沙盒里跑起来。四个组件四层翻译任何一层出问题整个链路就断。“Madeira”这个名字本身是葡萄牙的一个群岛盛产一种同名加强葡萄酒。用它来命名一个在 iOS 上跑 Windows 程序的兼容层项目多少有点黑色幽默——就像把一瓶陈年烈酒倒进一个密封的旅行水壶里味道还在但容器完全不是为它设计的。这个项目要解决的核心问题很明确iOS 设备性能足够强但系统封闭、指令集是 ARM64、图形 API 是 Metal而大量存量 Windows 程序是 x86-64 Direct3D 的两者之间存在巨大的鸿沟。Madeira 要做的就是在这道鸿沟上架四座桥。适合谁来参考这篇内容如果你是对跨平台兼容层感兴趣的开发者或者手头有必须在 Windows 上跑的老工具、老游戏又或者你单纯好奇“iOS 上到底能不能跑 exe”那这篇东西应该能给你一条相对完整的路径。我不会把它写成一份官方文档式的说明书而是按我自己踩坑的顺序把每一层的原理、选型理由、实操要点和翻车现场都摊开讲。2. 四层翻译架构的整体设计思路2.1 为什么是 Wine FEX-Emu DXMT 这个组合先说清楚每一层到底在干什么。Wine 的全称是“Wine Is Not an Emulator”它不模拟 Windows 内核而是把 Windows 的 API 调用实时翻译成宿主系统的 POSIX 调用。比如一个程序调用CreateFileWWine 会把它映射成 Linux 或 Darwin 的open。这意味着 Wine 本身不处理 CPU 指令集差异它假设你的 CPU 能直接执行程序的机器码。问题来了Windows 程序绝大多数是 x86-64 的而 iOS 设备是 ARM64 的。这时候就需要 FEX-Emu 出场。FEX-Emu 是一个用户态的 x86-64 到 ARM64 的二进制翻译器它把 x86-64 指令块动态翻译成 ARM64 指令块并做缓存。和 QEMU 那种全系统模拟不同FEX-Emu 只翻译用户态指令系统调用直接透传给宿主内核所以开销小得多。图形这一层更麻烦。Windows 程序用 Direct3D 画图iOS 只认 Metal。DXMT 的作用就是把 D3D 的调用翻译成 Metal 的调用。它和 DXVK 的思路类似但 DXVK 输出的是 VulkanDXMT 输出的是 Metal。在 iOS 上没有 Vulkan 可用所以 DXMT 是唯一合理的选择。那为什么不直接用 QEMU 全系统模拟跑一个完整的 Windows因为性能。全系统模拟要模拟 CPU、内存管理单元、各种外设开销是用户态翻译的好几倍。在移动设备上电池和散热都是硬约束能省一层是一层。Madeira 这个组合的本质是能透传的透传能翻译的翻译只在必要的地方做转换。2.2 各层之间的依赖关系与数据流理解数据流对排查问题至关重要。一个 Windows 程序启动时大致经历这样的路径启动器读取 PE 文件头确认是 x86-64 架构。FEX-Emu 接管执行把 x86-64 代码块翻译成 ARM64 并缓存。程序调用 Windows APIWine 把这些调用翻译成 Darwin 系统调用。程序调用 D3D 创建纹理、着色器DXMT 把这些翻译成 Metal 对象。Metal 驱动最终把命令提交给 GPU。这里有个容易忽略的点Wine 的 PE 加载器和 FEX-Emu 的翻译器之间存在耦合。Wine 需要知道哪些模块是 x86-64 的哪些是 ARM64 的。在纯 Linux 环境下Wine 可以直接加载 x86-64 的 PE 并交给 FEX-Emu。但在 iOS 上Wine 本身也被编译成了 ARM64所以它加载 x86-64 PE 时必须显式地把执行权交给 FEX-Emu。这个交接点的实现质量直接决定了程序能不能启动。另一个依赖是 DXMT 对 Metal 版本的依赖。iOS 的 Metal 版本随系统更新不同设备支持的 Metal 特性集不同。DXMT 在翻译 D3D 特性时需要查询当前 Metal 设备支持哪些能力然后做降级或模拟。比如 D3D11 的某些纹理格式在 Metal 上没有直接对应DXMT 就得用计算着色器做转换。这些转换在桌面端可能无所谓但在移动 GPU 上带宽和算力都紧张一个不当的格式转换就能让帧率腰斩。2.3 方案选型的取舍为什么不用其他路线有人会问为什么不直接用 CrossOver 那套CrossOver 本质上是 Wine 的商业发行版它在 macOS 上跑得很好但 iOS 和 macOS 是两回事。iOS 不允许 JIT即时编译在普通应用中使用而 FEX-Emu 和 Wine 的某些部分依赖 JIT。这是 iOS 沙盒最硬的限制之一。所以 Madeira 在 iOS 上要么走解释执行性能极差要么想办法在允许的范围内做 AOT提前编译加有限的 JIT。还有人问能不能用 Rosetta 2Rosetta 2 是苹果给 macOS 提供的 x86-64 到 ARM64 翻译层但它在 iOS 上不可用苹果没有开放这个接口。而且 Rosetta 2 只翻译指令不处理 Windows API所以即便能用也还得配 Wine。至于 DXMT 和 MoltenVK 的对比MoltenVK 是把 Vulkan 翻译成 Metal而 DXMT 是直接把 D3D 翻译成 Metal。中间少一层延迟和开销都更低。在 iOS 这种资源受限的环境里少一层翻译就是多一分流畅。3. 核心组件拆解与关键配置3.1 Wine 在 iOS 上的编译与裁剪要点把 Wine 编译到 iOS 上第一件事是裁剪。完整的 Wine 包含大量用不到的东西Win16 支持、打印子系统、部分网络组件、控制台驱动。在 iOS 上这些要么没意义要么会引入额外的依赖和权限问题。我自己的裁剪清单是这样的去掉wineconsole和所有控制台相关驱动iOS 上没有终端窗口的概念。去掉wineps打印驱动移动设备不需要打印。保留winex11.drv的替代品实际上在 iOS 上要用一个自定义的显示驱动把窗口内容渲染到 UIView 或 CAMetalLayer 上。网络部分保留 Winsock 的核心实现但去掉一些不常用的协议处理。编译时最大的坑是Darwin 和 Linux 的系统调用差异。Wine 的代码里大量使用syscall直接调用在 iOS 上这些系统调用号完全不同而且很多被沙盒禁止。解决办法是尽量走 libc 封装避免直接 syscall。但有些地方 Wine 为了性能会绕过 libc这些代码在 iOS 上必须改掉。另一个坑是文件系统路径映射。Windows 程序习惯C:\这样的路径Wine 需要把它映射到 iOS 沙盒内的某个目录。通常的做法是在沙盒的 Documents 下建一个drive_c目录然后把C:映射过去。但 iOS 的沙盒路径在每次安装后都会变所以这个映射必须是动态的不能硬编码。提示Wine 的WINEPREFIX环境变量在 iOS 上要指向沙盒内可写目录不要指向 Bundle 内部否则程序无法创建配置文件。3.2 FEX-Emu 的 RootFS 配置与 JIT 限制绕行FEX-Emu 在 Linux 上跑得很欢因为 Linux 允许mmap可执行内存和 JIT。iOS 对mmap的PROT_EXEC限制很严普通应用不能动态生成可执行代码。这是 Madeira 在 iOS 上最棘手的问题。绕行的思路有两条。第一条是AOT 预翻译在程序安装或首次启动时把 PE 文件里的 x86-64 代码全部翻译成 ARM64存成一个缓存文件之后直接执行缓存。这样就不需要运行时 JIT。缺点是首次启动慢而且遇到自修改代码或动态生成的代码就歇菜。第二条是利用 iOS 的 JIT 例外某些类型的应用比如需要 WebKit 的可以获得 JIT 权限但普通应用不行。这条路对 Madeira 来说基本走不通除非你有特殊的分发渠道。我实际测试下来AOT 方案对老程序和老游戏基本够用因为它们的代码段通常是静态的。但遇到加壳或运行时解密的程序AOT 就无能为力了。这时候只能退回解释执行帧率会掉到个位数基本没法玩。FEX-Emu 的 RootFS 配置也很关键。它需要一个根文件系统来存放翻译后的代码、配置文件和临时数据。在 iOS 上这个 RootFS 要放在沙盒的Library/Application Support下并且要确保有足够的空间。一个中等规模的 Windows 程序翻译缓存可能达到几百 MB。3.3 DXMT 的 Metal 后端适配与性能调优DXMT 的配置核心是特性集映射。D3D11 有大量的特性级别feature level从 9_1 到 12_1。Metal 在不同 iOS 设备上的特性集也不同。DXMT 需要把 D3D 的特性请求映射到 Metal 的能力上。比如 D3D11 的D3D_FEATURE_LEVEL_11_0要求支持计算着色器、纹理数组、多采样纹理等。在较老的 iOS 设备上Metal 可能不支持某些纹理格式的写入DXMT 就得用渲染到纹理再拷贝的方式模拟这会增加开销。性能调优方面有几个参数值得关注参数作用建议值DXMT_MAX_FRAME_LATENCY控制最大帧延迟2 或 3太高会增加输入延迟DXMT_SHADER_CACHE着色器缓存路径指向沙盒可写目录DXMT_MSAA多采样抗锯齿移动端建议关闭或 2xDXMT_TEXTURE_COMPRESSION纹理压缩格式优先用 ASTC没有则用 ETC2着色器编译是另一个大头。D3D 的着色器是 HLSL 编译成的字节码DXMT 需要把它翻译成 Metal Shading Language。这个翻译过程如果放在运行时每次启动都要重来很慢。所以 DXMT 支持着色器缓存把翻译结果存下来。在 iOS 上这个缓存要放在沙盒里并且要注意缓存失效策略——驱动更新或系统升级后缓存可能要清掉。注意Metal 的着色器编译在首次运行时可能耗时几百毫秒甚至几秒如果程序在启动时编译大量着色器用户会看到明显的卡顿。建议在程序启动画面期间预编译常用着色器。4. 从零搭建 Madeira 运行环境的实操流程4.1 环境准备与依赖安装假设你已经在 macOS 上准备好了 Xcode 和 iOS 开发环境。第一步是获取各个组件的源码Wine从官方仓库拉取切换到支持 PE 加载的分支。FEX-Emu从官方仓库拉取注意要选支持 ARM64 宿主的分支。DXMT从官方仓库拉取确认 Metal 后端已启用。一个 iOS 应用壳可以用 Xcode 新建一个 Single View App然后把上述组件编译成静态库或动态库链接进去。编译顺序很重要先编译 FEX-Emu因为它提供 x86-64 指令翻译的运行时再编译 Wine链接 FEX-Emu 的库最后编译 DXMT链接 Wine 的库。每个组件的编译都要针对 iOS 的 SDK并且要关闭一些不支持的选项。依赖方面Wine 需要 libxml2、libxslt、freetype、fontconfig 等。这些在 iOS 上要么用系统自带的要么自己编译。我建议尽量用系统框架减少包体积。比如字体渲染可以直接用 CoreText不需要 freetype。4.2 编译参数与关键宏定义编译 Wine 时有几个宏必须定义CFLAGS-DIOS -D__IOS__ -DHAVE_DARWIN -DDARWIN_NO_SETENVIOS和__IOS__用于条件编译 iOS 特有的代码路径。HAVE_DARWIN启用 Darwin 系统调用封装。DARWIN_NO_SETENV是因为 iOS 沙盒不允许修改环境变量需要绕过相关代码。FEX-Emu 的编译要启用 AOT 模式cmake -DCMAKE_TOOLCHAIN_FILEios.toolchain.cmake \ -DENABLE_AOTON \ -DENABLE_JITOFF \ -DARCHarm64 \ ..ENABLE_JITOFF是必须的因为 iOS 不允许 JIT。ENABLE_AOTON启用提前编译支持。DXMT 的编译要指定 Metal 版本cmake -DCMAKE_TOOLCHAIN_FILEios.toolchain.cmake \ -DMETAL_VERSION2.4 \ -DENABLE_DXILON \ ..METAL_VERSION要根据目标设备的最低支持版本来定。如果定得太高老设备跑不了定得太低新特性用不上。4.3 首次运行与基础验证编译完成后把生成的库和资源文件打包进 iOS 应用。首次运行时需要做几件事在沙盒内创建drive_c目录和 Wine prefix。把 FEX-Emu 的 RootFS 解压到指定位置。设置环境变量指向各个组件的路径。加载一个最简单的 Windows 程序做验证比如notepad.exe或一个自己写的 Hello World。验证时重点看几个指标程序能不能启动、窗口能不能显示、输入能不能响应、有没有崩溃日志。如果程序启动就崩先看 Wine 的调试输出通常会有err:或fixme:开头的日志。fixme一般可以忽略err就要具体分析。我自己的验证顺序是先跑一个纯控制台的 Windows 程序确认 FEX-Emu 和 Wine 的加载链路通了再跑一个简单的 GUI 程序确认显示驱动和 DXMT 通了最后跑一个 D3D 程序确认图形链路通了。每一步都单独验证出问题容易定位。5. 常见故障与排查技巧实录5.1 启动即崩溃从日志定位到具体组件启动崩溃是最常见的问题原因可能出在四层中的任何一层。排查的第一步是拿到完整日志。Wine 的日志通过WINEDEBUG环境变量控制可以设置成all输出所有调试信息但这样日志量巨大建议先设成errall,warnall。如果日志里出现FEX: failed to translate block说明 FEX-Emu 翻译失败可能是遇到了不支持的指令或自修改代码。如果出现wine: cannot load PE说明 Wine 的 PE 加载器有问题可能是架构不匹配或依赖缺失。如果出现DXMT: unsupported feature level说明 DXMT 的特性映射失败需要调整 D3D 特性级别。还有一种崩溃是内存不足。iOS 对单个应用的内存限制比较严尤其是后台时。Wine 和 FEX-Emu 都会占用不少内存如果程序本身也吃内存很容易被系统杀掉。解决办法是尽量裁剪组件减少内存占用并且在应用进入后台时主动释放缓存。5.2 界面乱码与字体缺失的处理Wine 乱码是个老问题在 iOS 上尤其突出。原因通常是字体缺失或字符集映射错误。Windows 程序默认使用Tahoma、Arial等字体这些在 iOS 上不一定有。Wine 会尝试用系统字体替代但替代规则可能不对。解决办法是往 Wine 的字体目录里放几个核心字体文件比如simsun.ttc宋体、msyh.ttc微软雅黑。然后在注册表里设置字体替换规则[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes] ArialSimSun TahomaSimSun字符集方面要确保 Wine 的 locale 设置正确。在 iOS 上locale 通常从系统设置读取但 Wine 可能需要显式设置LANG和LC_ALL。如果程序用的是 GBK 编码还要确保 Wine 的代码页转换表完整。提示字体文件要放在沙盒内不要放在 Bundle 里因为 Bundle 是只读的Wine 可能需要修改字体缓存。5.3 图形渲染异常黑屏、花屏、帧率低图形问题最让人头疼因为涉及 D3D、DXMT、Metal 三层。黑屏通常是 DXMT 没有正确创建 Metal 层或者着色器编译失败。花屏往往是纹理格式不匹配或内存对齐问题。帧率低则可能是翻译开销大、着色器编译频繁、或者 GPU 带宽瓶颈。排查时可以先禁用 DXMT让程序走 Wine 的 GDI 渲染路径。如果 GDI 能显示但 D3D 不行问题就在 DXMT。然后检查 DXMT 的日志看有没有unsupported format或shader compile error。帧率低的话可以用 Metal 的帧捕获工具Xcode 自带抓一帧看 GPU 时间花在哪里。常见瓶颈是过多的渲染通道切换和纹理上传。DXMT 有一些优化选项比如批量提交、延迟纹理上传可以在配置里打开。5.4 输入与窗口管理的典型问题输入问题通常表现为键盘没反应、鼠标位置偏移、触摸事件不识别。Wine 在 iOS 上需要一个输入驱动把 UIKit 的触摸事件翻译成 Windows 的鼠标和键盘消息。这个翻译层如果做得不好就会出现各种奇怪的问题。窗口管理方面Windows 程序可能创建多个窗口、对话框、菜单。在 iOS 上这些都要映射到 UIView 层级上。如果映射不对窗口可能被遮挡、无法拖动、或者尺寸不对。我的经验是尽量简化窗口管理把主窗口全屏显示对话框用模态方式呈现避免复杂的窗口嵌套。触摸事件要映射成鼠标事件但要注意双击、右键、滚轮这些在触摸屏上没有直接对应的操作。通常的做法是单指点击是左键双指点击是右键双指拖动是滚轮。这些映射规则要在输入驱动里实现并且要允许用户自定义。6. 性能调优与资源占用的实战经验6.1 指令翻译缓存的预热与复用FEX-Emu 的翻译缓存是性能的关键。首次运行时所有 x86-64 代码都要翻译一遍这个过程很慢。但如果能把翻译结果缓存下来下次启动就快得多。缓存的预热有两种方式。一种是启动时预翻译在程序启动画面期间扫描 PE 文件的代码段把常用函数提前翻译好。另一种是运行时缓存程序运行过程中FEX-Emu 把翻译过的代码块存到缓存文件里下次遇到同样的代码块直接读缓存。缓存文件的管理要注意版本控制。如果 FEX-Emu 更新了翻译算法旧缓存可能不兼容需要清掉重建。可以在缓存文件头里写一个版本号加载时检查版本号是否匹配。6.2 图形管线的精简与 Metal 特性取舍移动 GPU 的带宽和算力都有限图形管线要尽量精简。DXMT 提供了一些选项来控制渲染质量关闭或降低 MSAA。使用 ASTC 纹理压缩减少纹理带宽。合并渲染通道减少 Render Pass 切换。避免频繁的纹理上传和回读。Metal 特性方面要优先使用 iOS 设备普遍支持的特性。比如MTLFeatureSet_iOS_GPUFamily3_v1及以上的设备支持计算着色器和纹理数组这些是 D3D11 的基本要求。如果目标设备只支持到GPUFamily2那 D3D11 就跑不了只能降到 D3D9。6.3 内存与电量的平衡策略iOS 设备的内存和电量都是稀缺资源。Wine FEX-Emu DXMT 这套组合本身就比原生程序吃资源所以更要精打细算。内存方面可以限制 FEX-Emu 的翻译缓存大小超过阈值就淘汰旧缓存。Wine 的堆管理也可以调减少不必要的内存池。DXMT 的纹理缓存也要设上限避免纹理占用过多内存。电量方面主要是控制 CPU 和 GPU 的负载。FEX-Emu 的翻译是 CPU 密集型的可以通过降低翻译精度比如不优化某些指令序列来减少 CPU 占用但会增加翻译后的代码量。GPU 方面降低渲染分辨率和帧率上限是最直接的手段。注意iOS 在低电量模式下会限制 CPU 和 GPU 频率这时候性能会明显下降。如果程序对性能敏感要提示用户关闭低电量模式。7. 兼容性边界与后续扩展方向7.1 哪些程序能跑哪些跑不了根据我的测试以下几类程序在 Madeira 上跑得比较好老版本的 Win32 桌面程序尤其是 2000 年代到 2010 年代初的。使用 D3D9 或 D3D11 的 2D 游戏和轻量级 3D 游戏。不依赖特定硬件或驱动的工具软件。跑不了或跑得很差的需要内核态驱动的程序比如杀毒软件、虚拟光驱。使用 D3D12 或 Vulkan 的新游戏。依赖 .NET 运行时且版本较高的程序因为 .NET 在 iOS 上的支持不完整。使用反作弊或加壳保护的程序因为 AOT 翻译无法处理自修改代码。7.2 从 Madeira 到更通用的 iOS 兼容层Madeira 目前还是一个相对实验性的项目但它验证了一条可行的路径在 iOS 上通过多层翻译运行 Windows 程序是可能的。后续的扩展方向有几个一是完善 AOT 翻译器支持更多的 x86-64 指令和更复杂的代码模式减少对 JIT 的依赖。二是优化 DXMT 的 Metal 后端支持更多的 D3D 特性和更高的性能。三是改进输入和窗口管理让触摸操作更自然。四是探索与 iOS 系统更深度的集成比如利用 App Extension 或 Shortcuts 来启动程序。这条路不好走但每一步都有价值。我在实际折腾的过程中最大的体会是不要试图一次把所有东西都跑通而是分层验证、逐层排查。先让最简单的程序跑起来再逐步增加复杂度。每解决一个问题就对整个链路多一分理解。这种理解比任何一个具体的配置参数都重要。
返回列表