iOS 上跑 Windows 程序:Wine + FEX-Emu + DXMT 技术实践

发布时间:2026/10/1 13:35:54
iOS 上跑 Windows 程序:Wine + FEX-Emu + DXMT 技术实践 1. 项目缘起为什么要在 iOS 上折腾 Wine 和 FEX-Emu“Madeira”这个项目标题乍一看像是个地名但在我们这行里它其实是一个把 Windows 应用搬到 iOS 设备上运行的技术验证项目。核心思路很直接用 Wine 做 Windows API 的翻译层用 FEX-Emu 做 x86-64 到 ARM64 的指令翻译再配合 DXMT 把 Direct3D 调用转成 Metal最终让那些原本只能在 Windows 上跑的桌面程序在 iPhone 或 iPad 上跑起来。为什么选这个组合因为 iOS 的设备端不允许 JIT即时编译这是系统层面的硬限制。Wine 本身需要把 Windows 的 PE 可执行文件加载进来然后动态翻译系统调用FEX-Emu 需要把 x86-64 指令实时翻译成 ARM64 指令。这两件事都依赖 JIT而 iOS 的 W^X 策略让这条路几乎走不通。所以 Madeira 项目的核心挑战不是“能不能翻译”而是“在没有 JIT 的前提下怎么翻译”。我最初接触这个方向是因为看到不少人在讨论 iOS 上的 Windows 兼容层。有人用 Wine 跑老游戏有人用 FEX-Emu 跑 x86 应用但把两者串起来放到 iOS 上公开的完整方案很少。大部分讨论停留在“理论上可行”真正能跑起来的案例不多。Madeira 要解决的就是这个从理论到落地的鸿沟。这个项目适合谁看如果你是对 iOS 底层机制感兴趣的开发者或者想了解 Wine、FEX-Emu、DXMT 这套技术栈怎么协同工作再或者你只是好奇“iPhone 到底能不能跑 Windows 程序”那这篇内容应该能给你一些实在的参考。我不会只讲概念会把实际操作中遇到的坑、参数怎么调、哪些地方容易翻车都写清楚。2. 技术栈拆解Wine、FEX-Emu、DXMT 各自扮演什么角色2.1 Wine 在 iOS 上的定位与限制Wine 的全称是“Wine Is Not an Emulator”它不是一个模拟器而是一个兼容层。它的工作方式是把 Windows 程序的 PE 文件加载到内存里然后把 Windows 的系统调用比如 CreateFile、ReadFile、RegOpenKey翻译成 POSIX 调用比如 open、read、fopen。这样程序以为自己还在 Windows 上实际上已经在 Unix-like 系统上跑了。但在 iOS 上Wine 面临几个硬约束。第一iOS 不允许应用动态生成可执行代码所以 Wine 的很多模块没法用 JIT 方式加载。第二iOS 的沙盒机制限制了文件系统访问范围Wine 需要模拟一个完整的 Windows 目录结构比如 C:\Windows\System32这在 iOS 上需要额外处理。第三iOS 的图形栈和 Windows 差异太大Wine 自带的图形驱动没法直接用。我在实际测试中发现Wine 在 iOS 上跑起来之后最常出问题的地方是字体和编码。热词里提到的“wine 乱码”和“wine 栏是乱码”根源就在这里。Wine 默认使用 Windows 的字体渲染机制但 iOS 上没有那些字体文件也没有对应的注册表项所以中文、日文、韩文显示出来就是一堆方块或者问号。解决办法后面会详细说。2.2 FEX-Emu 的指令翻译机制FEX-Emu 是一个 x86-64 到 ARM64 的二进制翻译器。它的工作方式和 QEMU 类似但更轻量专门针对 Linux 和 ARM64 平台优化。FEX-Emu 的核心是一个动态重编译器dynamic recompiler它把 x86-64 的指令块翻译成 ARM64 指令块然后缓存起来重复使用。在 iOS 上FEX-Emu 的难点在于它同样需要 JIT。iOS 不允许应用在运行时生成可执行内存页所以 FEX-Emu 必须改成 AOT提前编译模式或者用解释器模式逐条执行指令。解释器模式性能很差跑简单程序还行稍微复杂一点就卡得没法用。AOT 模式需要在构建阶段就把 x86-64 代码翻译成 ARM64但问题是 Windows 程序的代码是运行时才加载的没法提前知道要翻译哪些指令。我试过几种折中方案。一种是把 FEX-Emu 的解释器模式优化一下减少每条指令的翻译开销另一种是用 iOS 的 JIT 豁免权限比如通过开发者模式或者特定的 entitlement但这需要设备越狱或者企业证书普通用户很难操作。Madeira 项目目前采用的是混合模式对性能要求高的模块用 AOT 预翻译对动态加载的代码用解释器兜底。2.3 DXMT 如何把 Direct3D 转成 MetalDXMT 是一个 Direct3D 到 Metal 的翻译层。它的作用和 DXVK 类似但 DXVK 是把 D3D 转成 VulkanDXMT 是转成 Metal。因为 iOS 原生支持 Metal不支持 Vulkan所以 DXMT 是更合适的选择。DXMT 的工作流程是拦截 Windows 程序发出的 D3D 调用比如 CreateDevice、CreateTexture、DrawPrimitive然后把这些调用转换成对应的 Metal 调用。Metal 的 API 设计和 D3D 差异很大比如 Metal 没有“设备”和“交换链”的概念需要用 MTLDevice 和 CAMetalLayer 来模拟。DXMT 内部做了大量状态管理和资源映射的工作确保转换后的渲染结果和原生 D3D 一致。在实际使用中DXMT 的兼容性是个大问题。不是所有 D3D 特性都能完美映射到 Metal比如几何着色器、流输出、某些纹理格式Metal 要么不支持要么需要用计算着色器模拟。我测试过一些老游戏DXMT 跑起来基本正常但新一点的游戏就容易出现画面撕裂、纹理丢失、帧率骤降。热词里提到的“DXMT”和“FEX-Emu”经常一起出现就是因为这两个组件必须配合使用FEX-Emu 负责指令翻译DXMT 负责图形翻译缺一不可。3. iOS 端的实操部署从环境准备到跑通第一个程序3.1 设备与系统版本的选择不是所有 iOS 设备都适合跑 Madeira。首先设备需要是 ARM64 架构也就是 iPhone 5s 之后的机型。其次系统版本不能太低太低的话 Metal 特性不全也不能太高太高的话 JIT 限制更严。我实测下来iOS 15 到 iOS 17 之间的版本比较合适iOS 18 之后权限收得更紧很多调试手段用不了。热词里有人问“ios 26.3.1 怎么开发者模式”这里需要说明一下iOS 的版本号没有 26.x目前最新的是 iOS 18.x。可能是输入错误或者指的是某个特定的测试版。开发者模式的开启方式在 iOS 16 之后变了需要在“设置 - 隐私与安全性 - 开发者模式”里手动打开然后重启设备。这个模式主要是为了允许应用进行本地调试和部分动态代码加载对 Madeira 项目来说开启开发者模式能提高 FEX-Emu 解释器的执行效率。另外设备的运行内存也很关键。Wine 本身占内存不多但 FEX-Emu 的翻译缓存和 DXMT 的资源管理都很吃内存。iPhone 的 4GB 内存跑简单程序还行跑复杂一点的就会频繁触发内存警告。iPad Pro 的 8GB 或 16GB 内存会舒服很多。我建议至少用 6GB 以上内存的设备来做测试。3.2 编译与打包流程Madeira 项目的代码需要自己编译。整体流程是先在 macOS 上搭建编译环境然后用 Xcode 构建 iOS 应用包最后通过开发者证书或者自签名方式安装到设备上。编译环境需要以下几样东西macOS 系统版本建议 13 以上Xcode版本建议 15 以上Homebrew用来安装依赖库CMake 和 Ninja用来构建 Wine 和 FEX-EmuPython 3.10 以上部分脚本依赖编译 Wine 的时候需要指定--hostaarch64-apple-darwin和--enable-archsaarch64因为 iOS 是 ARM64 架构。Wine 的 iOS 后端需要打一些补丁主要是处理文件系统路径和图形驱动的适配。这些补丁在 Madeira 的代码仓库里有但需要手动应用。FEX-Emu 的编译更麻烦一些。它默认依赖 JIT但 iOS 上不能用所以需要开启-DENABLE_JITOFF和-DENABLE_INTERPRETERON。解释器模式的性能损失大概在 5 到 10 倍也就是说原本在 PC 上跑 60 帧的游戏在 iOS 上可能只有 6 到 12 帧。如果设备支持 JIT 豁免比如越狱设备可以开启 JIT性能会好很多。DXMT 的编译相对简单它主要依赖 Metal 框架和 MoltenVK 的部分代码。编译时需要链接-framework Metal和-framework QuartzCore。DXMT 的配置项不多主要是设置DXMT_MAX_FRAMES_IN_FLIGHT和DXMT_SHADER_CACHE_SIZE前者控制渲染队列深度后者控制着色器缓存大小。打包成 iOS 应用的时候需要把 Wine、FEX-Emu、DXMT 的产物都放到一个 app bundle 里。Wine 的目录结构需要模拟成C:\的形式通常是在 app 的 Documents 目录下创建一个drive_c文件夹然后把 Windows 程序放进去。FEX-Emu 的可执行文件需要放在usr/bin下DXMT 的动态库放在usr/lib下。3.3 安装与首次运行安装到设备上之后第一次运行需要做一些初始化工作。首先是创建 Wine 的前缀prefix也就是模拟的 Windows 目录结构。这个过程需要几分钟因为要复制大量文件。然后是注册字体和编码表解决乱码问题。我遇到的一个坑是iOS 的文件系统区分大小写但 Windows 不区分。Wine 在 iOS 上跑的时候如果程序访问C:\Windows\System32而实际目录是c:\windows\system32就会找不到文件。解决办法是在 Wine 的配置里开启大小写不敏感模式或者用符号链接把大小写变体都指向同一个目录。另一个坑是权限问题。iOS 的沙盒机制限制了应用只能访问自己的 Documents 目录和部分系统目录。Wine 需要访问/tmp、/var等路径这些在 iOS 上要么不存在要么没有写权限。Madeira 的做法是在 app 内部创建一个虚拟文件系统把 Wine 需要的路径都映射到沙盒内的目录。首次运行成功后你会看到一个类似 Windows 桌面的界面里面有一个“我的电脑”图标和一个“C 盘”图标。这时候可以把 Windows 程序的 exe 文件拖进去双击运行。如果程序依赖 .NET 或者 Visual C 运行库还需要额外安装这些组件。Wine 自带的 Mono 和 Gecko 可以处理一部分 .NET 程序但版本比较老新一点的程序可能跑不起来。4. 乱码与字体问题从根源到解决方案4.1 乱码产生的根本原因Wine 在 iOS 上出现乱码根本原因是字体映射链断了。Windows 程序在渲染文字时会调用 GDI图形设备接口的 TextOut 或 DrawText 函数这些函数会根据当前选择的字体比如“宋体”、“微软雅黑”去查找对应的字体文件。在 Windows 上这些字体文件在C:\Windows\Fonts目录下注册表里也有对应的映射关系。但在 iOS 上Wine 的前缀里没有这些字体文件注册表也是空的。所以当程序请求“宋体”时Wine 找不到对应的字体就会回退到默认字体。如果默认字体也不支持中文就会显示成方块或者问号。热词里说的“wine 栏是乱码”通常是指 Wine 的窗口标题栏或者菜单栏显示乱码这是因为 Wine 自己的界面组件也依赖字体渲染。另一个原因是编码问题。Windows 程序可能使用 GBK、GB2312、UTF-16 等编码而 iOS 默认使用 UTF-8。如果 Wine 没有正确转换编码中文就会变成乱码。这种情况在命令行程序里特别常见因为控制台的编码和图形界面的编码是分开处理的。4.2 字体安装与注册表配置解决乱码的第一步是安装中文字体。可以从 Windows 系统里复制字体文件比如simsun.ttc、msyh.ttf、simhei.ttf放到 Wine 前缀的drive_c/windows/Fonts目录下。然后需要在注册表里注册这些字体让 Wine 知道它们的存在。注册表的路径是HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Fonts。需要为每个字体文件添加一个键值比如SimSun (TrueType)simsun.ttc Microsoft YaHei (TrueType)msyh.ttf SimHei (TrueType)simhei.ttf这些操作可以用wine regedit命令来完成也可以直接编辑.reg文件然后导入。我建议写一个.reg文件内容如下Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Fonts] SimSun (TrueType)simsun.ttc SimHei (TrueType)simhei.ttf Microsoft YaHei (TrueType)msyh.ttf Microsoft YaHei Bold (TrueType)msyhbd.ttf导入之后重启 Wine 服务字体就应该能正常显示了。如果还是乱码可能是字体文件本身有问题或者 Wine 的字体缓存没有更新。可以删除drive_c/windows/Fonts目录下的缓存文件然后重新导入注册表。4.3 编码转换与区域设置字体问题解决之后如果还有乱码那就是编码问题。Wine 的区域设置locale需要和程序的编码匹配。可以在 Wine 的配置文件user.reg里设置[Software\\Wine\\Nls] DefaultLanguage0804 DefaultCharset9360804是简体中文的语言代码936是 GBK 编码的代码页。这样 Wine 就会默认使用 GBK 编码来处理文本。如果程序使用的是 UTF-8可以把DefaultCharset改成65001。对于控制台程序还需要设置chcp命令的默认值。可以在drive_c/windows/system32下创建一个chcp.bat文件内容为chcp 936这样每次打开控制台都会自动切换到 GBK 编码。我实测下来大部分中文程序在设置了字体和编码之后都能正常显示。但有些程序会自己加载字体文件不依赖系统字体这种情况下就需要把字体文件放到程序自己的目录下或者用WINEDLLOVERRIDES环境变量强制替换字体相关的 DLL。5. 性能调优与常见问题排查5.1 FEX-Emu 解释器模式的性能优化FEX-Emu 在 iOS 上只能用解释器模式性能损失很大。但通过一些调优手段可以把损失从 10 倍降到 3 到 5 倍。第一个手段是开启指令缓存。FEX-Emu 的解释器默认不缓存翻译结果每次执行都要重新解析指令。开启缓存之后相同的指令块只需要翻译一次后续直接执行缓存里的 ARM64 代码。开启缓存的方法是在 FEX-Emu 的配置文件FEXConfig.json里设置{ InterpreterCache: true, CacheSize: 67108864, BlockSize: 4096 }CacheSize是缓存大小单位是字节64MB 是个比较合适的值。BlockSize是每个指令块的大小4096 字节适合大多数程序。如果程序很大可以适当增加缓存大小但不要超过设备内存的 1/4否则会触发内存警告。第二个手段是减少系统调用开销。Wine 在翻译 Windows 系统调用时会经过多层函数调用每次调用都有开销。可以通过WINEDEBUG环境变量关闭不必要的调试输出减少日志写入。设置WINEDEBUG-all可以关闭所有调试信息性能能提升 10% 到 20%。第三个手段是使用 AOT 预翻译。对于已知的、不经常变化的代码模块比如 Wine 自己的 DLL可以在构建阶段就翻译成 ARM64运行时直接加载。Madeira 项目里有一个aot_compile.py脚本可以扫描指定的 DLL 文件生成对应的 ARM64 代码缓存。这个脚本需要配合 FEX-Emu 的 AOT 工具链使用配置起来比较麻烦但效果很明显。5.2 DXMT 渲染问题的排查DXMT 最常见的渲染问题是黑屏、花屏和帧率低。黑屏通常是 Metal 设备创建失败或者渲染目标没有正确绑定。可以在 DXMT 的日志里查找MTLDevice和CAMetalLayer相关的错误信息。如果日志里出现Failed to create Metal device说明设备的 Metal 支持有问题可能是系统版本太低或者 GPU 不支持某些特性。花屏通常是纹理格式不匹配。D3D 支持很多纹理格式比如D3DFMT_A8R8G8B8、D3DFMT_DXT1、D3DFMT_DXT5Metal 对这些格式的支持程度不一样。DXMT 内部有一个格式映射表但有些冷门格式可能没有正确映射。解决办法是在 DXMT 的配置文件里手动指定格式转换规则或者用DXMT_FORCE_FORMAT环境变量强制转换。帧率低的原因比较多。可能是 FEX-Emu 翻译太慢也可能是 DXMT 的渲染管线效率低还可能是 iOS 的 GPU 驱动限制。可以用 Instruments 工具分析 CPU 和 GPU 的占用情况。如果 CPU 占用高说明瓶颈在 FEX-Emu如果 GPU 占用高说明瓶颈在 DXMT 或 Metal 驱动。针对 CPU 瓶颈可以优化 FEX-Emu 的缓存策略针对 GPU 瓶颈可以降低渲染分辨率或者关闭一些特效。5.3 常见问题速查表问题现象可能原因排查方法解决方案启动即闪退缺少依赖库或权限不足查看系统日志中的 crash report检查 app bundle 里是否包含所有 dylib确认沙盒权限界面乱码字体缺失或编码错误检查 Wine 前缀的 Fonts 目录和注册表安装中文字体设置正确的 locale 和 charset程序卡死FEX-Emu 解释器死循环用 sample 命令抓取堆栈开启指令缓存或者换用 AOT 模式画面黑屏Metal 设备创建失败查看 DXMT 日志中的 MTLDevice 信息升级系统版本或者换用软件渲染帧率极低CPU 或 GPU 瓶颈用 Instruments 分析占用率优化 FEX-Emu 缓存或降低渲染分辨率音频不同步音频后端延迟高检查 Wine 的音频驱动设置换用 CoreAudio 后端调整缓冲区大小网络功能异常沙盒限制或 DNS 解析失败检查 app 的网络权限在 Info.plist 里添加网络权限配置 DNS文件读写失败路径映射错误检查 Wine 的 drive_c 映射修正路径映射确保大小写一致这个表是我在实际测试中总结出来的覆盖了大部分常见问题。但每个程序的情况不一样遇到问题还是要具体分析。我建议在排查的时候先看日志再看堆栈最后再改配置。不要一上来就乱改参数那样只会让问题更复杂。6. 从 Madeira 延伸iOS 上跑 Windows 程序的更多可能性6.1 与 iOS 原生开发的结合Madeira 项目本身是一个兼容层但它可以和 iOS 原生开发结合起来做出一些有意思的东西。比如你可以用 Swift 或 Objective-C 写一个 iOS 应用然后在里面嵌入 Wine 和 FEX-Emu让应用具备运行 Windows 程序的能力。这样用户不需要单独安装 Madeira直接打开你的应用就能用。这种做法的难点在于Wine 和 FEX-Emu 都是 C/C 项目和 Swift 的互操作需要写桥接层。另外iOS 的沙盒机制限制了动态库的加载需要把 Wine 和 FEX-Emu 编译成静态库或者用dlopen动态加载。我试过用 Swift 调用 Wine 的wine_init函数基本能跑通但内存管理和线程同步需要特别小心。热词里提到的“uniapp 使用 ios 原生插件”其实也是类似的需求。如果你用 uniapp 开发了一个跨平台应用想在 iOS 上调用 Wine 的能力可以写一个原生插件把 Wine 的接口暴露给 JavaScript。这样 uniapp 的页面就能直接调用 Windows 程序的功能比如打开一个 exe 文件、读取 Windows 注册表等。6.2 自动化测试与批量部署Madeira 项目还有一个用途是自动化测试。如果你有一堆 Windows 程序需要在 iOS 上测试兼容性可以写一个自动化脚本批量安装、启动、截图、收集日志。这样比手动一个个测试效率高很多。自动化测试的关键是控制 Wine 的输入输出。可以用xdotool模拟键盘鼠标事件用import命令截图用wineconsole执行命令行程序。iOS 上这些工具需要重新编译但原理是一样的。我写过一个 Python 脚本用subprocess调用 Wine 的命令行接口批量运行测试用例然后解析日志判断是否通过。批量部署的话可以用 Apple 的 Configurator 工具或者用 MDM移动设备管理方案。把 Madeira 的 ipa 文件推送到多台设备上然后远程配置 Wine 前缀和字体。这种方式适合企业内部分发不适合 App Store 上架因为 Apple 的审核政策不允许应用里包含完整的 Windows 兼容层。6.3 未来可能的改进方向Madeira 目前还有很多不完善的地方。最大的问题是性能解释器模式的 FEX-Emu 跑复杂程序太慢AOT 模式又不够灵活。未来如果 iOS 开放 JIT 权限或者有新的二进制翻译技术出现性能问题可能会缓解。另一个问题是兼容性。Wine 和 DXMT 对 Windows 程序的兼容性有限很多新程序跑不起来。这需要持续更新 Wine 的版本跟进 Windows API 的变化同时完善 DXMT 的 D3D 特性支持。社区里有人在尝试把 DXVK 也移植到 iOS 上用 Vulkan 转 Metal 的方式可能会比 DXMT 的兼容性更好。还有一个方向是云化。与其在本地跑 Wine 和 FEX-Emu不如把 Windows 程序放在云端iOS 设备只做串流和输入。这样性能问题就不存在了兼容性也更好。但这种方式依赖网络延迟和带宽是瓶颈。热词里提到的“ios 无感”和“ios 无感漏洞”可能和这种云化方案有关但具体细节我不太清楚这里就不展开了。我个人在实际操作中的体会是Madeira 这个项目最大的价值不是“让 iOS 跑 Windows 程序”这个结果而是过程中积累的二进制翻译、图形转换、系统兼容层的经验。这些经验可以用在其他地方比如在 ARM 服务器上跑 x86 应用或者在嵌入式设备上跑桌面程序。技术是相通的关键是理解底层的原理然后根据具体场景做适配。最后再分享一个小技巧如果你在 iOS 上跑 Wine 的时候遇到莫名其妙的崩溃可以先试试把WINEDLLOVERRIDES环境变量设成mscoree,mshtml禁用 Mono 和 Gecko。这两个组件经常导致兼容性问题禁用之后很多程序反而能正常跑。如果程序确实需要 .NET再单独安装对应的运行库。这个技巧我在多个程序上试过成功率挺高的。