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

资讯详情

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

iOS上运行Windows应用:Wine、FEX-Emu与DXMT跨架构翻译实战

iOS上运行Windows应用:Wine、FEX-Emu与DXMT跨架构翻译实战 1. 项目缘起为什么要在 iOS 上折腾 Wine第一次看到 Madeira 这个项目名很多人会以为是那个葡萄牙的旅游海岛但在我们这行里它指向的是一件事把 Windows 应用搬到 iOS 设备上跑起来。核心思路并不新鲜Wine 在桌面 Linux 和 macOS 上已经跑了二十多年它通过实现 Windows 的 API 调用把 PE 格式的可执行文件翻译成宿主系统能理解的系统调用从而省掉一整套虚拟机的开销。问题在于iOS 是一个封闭得多的环境没有普通的进程 fork没有随意加载动态库的权限沙盒限制严格JIT 编译还受制于系统策略。所以 Madeira 这类项目要解决的核心矛盾就是如何在 iOS 的沙盒和签名体系下塞进一个能加载 Windows 二进制、并且把图形调用转译到 Metal 或 OpenGL ES 的兼容层。这个项目适合谁看三类人。第一类是 iOS 开发者想理解跨架构二进制翻译在移动端的落地方式尤其是 FEX-Emu 和 DXMT 这类组件怎么协同第二类是折腾党手上有越狱设备或者开发者账号想在自己的 iPhone 或 iPad 上跑一些老 Windows 程序第三类是做兼容层、模拟器、云游戏相关工作的工程师需要参考一个完整的 x86-64 到 ARM64 的翻译链路在移动端是怎么搭起来的。关键词里出现的 Wine、FEX-Emu、DXMT、iOS、x86-64基本就是这条技术栈的五个支柱缺一不可。我先把结论放在前面Madeira 不是那种装完就能用的成品软件它更像一套需要你自己拼装的工具链。你要理解 Wine 负责什么、FEX-Emu 负责什么、DXMT 负责什么然后才能明白为什么某些 Windows 程序能跑、某些一启动就崩。下面我按实际搭建和调试的顺序把这条链路拆开讲。2. 整体架构拆解Wine、FEX-Emu、DXMT 各自扮演什么角色2.1 Wine 是翻译官不是模拟器很多人第一次接触 Wine 会误以为它是虚拟机其实完全不是。Wine 的全称是 Wine Is Not an Emulator它做的事情是当 Windows 程序调用CreateWindowEx、ReadFile、RegOpenKey这些 API 时Wine 提供一套同名的实现把这些调用转成宿主系统对应的操作。在 Linux 上它转成 X11 或 Wayland 的调用、POSIX 的文件操作在 iOS 上它要转成 UIKit 或 CoreGraphics 的绘制、沙盒内的文件读写。这里有个关键点Wine 本身不负责 CPU 指令集的翻译。如果你的 Windows 程序是 x86-64 编译的而你的 iOS 设备是 ARM64那 Wine 加载这个 PE 文件之后里面的机器码根本执行不了。这就是为什么必须引入 FEX-Emu。2.2 FEX-Emu 负责 x86-64 到 ARM64 的指令翻译FEX-Emu 是一个用户态的 x86-64 模拟器它的工作方式是把 x86-64 的指令块动态翻译成 ARM64 指令然后执行。和 QEMU 那种全系统模拟不同FEX-Emu 只翻译用户态代码系统调用还是走宿主内核。它的性能在同类里算比较好的因为它做了块级别的缓存和优化翻译过的代码块可以复用。在 Madeira 的链路里FEX-Emu 是夹在 Wine 和 iOS 内核之间的。Wine 加载 PE 文件后把控制权交给 FEX-EmuFEX-Emu 把 x86-64 指令翻译成 ARM64 并执行执行过程中遇到 Windows API 调用再回到 Wine 的实现里。这个来回切换是有开销的所以实际性能取决于程序的调用密度。2.3 DXMT 把 Direct3D 转成 Metal图形是另一个大坑。Windows 程序大量使用 Direct3D 9/10/11 来渲染而 iOS 只认 Metal。DXMT 的作用就是把 D3D 的调用翻译成 Metal 的调用。它和 DXVK 的思路类似但 DXVK 转的是 VulkanDXMT 直接转 Metal少了一层在 iOS 上更合适。为什么不用 MoltenVK 加 DXVK 的组合因为多一层转换就多一层开销和 bugDXMT 直接对接 Metal路径更短。但代价是 DXMT 的成熟度不如 DXVK某些 D3D 特性支持不全遇到复杂渲染的程序可能会花屏或者直接崩。2.4 三者的协作关系把这三个组件串起来看Wine 提供 Windows API 的实现FEX-Emu 提供 CPU 指令的翻译DXMT 提供图形 API 的翻译。三者缺一不可而且版本要匹配。Wine 的版本决定了它实现了哪些 APIFEX-Emu 的版本决定了指令翻译的兼容性DXMT 的版本决定了图形支持的范围。我在实际搭建时踩过最大的坑就是版本不匹配Wine 调用了某个 APIFEX-Emu 那边翻译出错程序直接闪退日志里还看不出明显原因。组件职责输入输出WineWindows API 实现PE 文件的 API 调用宿主系统调用FEX-EmuCPU 指令翻译x86-64 机器码ARM64 机器码DXMT图形 API 翻译Direct3D 调用Metal 调用3. 环境准备iOS 侧的前置条件与工具链3.1 设备与系统版本的选择Madeira 对 iOS 版本有要求太低不行太高也可能出问题。根据我的实测iOS 15 到 iOS 17 之间的版本兼容性最好。iOS 18 之后系统对 JIT 和动态代码生成的限制更严FEX-Emu 的翻译缓存可能无法正常分配可执行内存。如果你手上有旧设备建议保留一台专门用来折腾不要拿主力机冒险。设备方面ARM64 是必须的A12 及以上的芯片性能比较够用。A11 及以下跑起来会很吃力因为 FEX-Emu 的翻译开销本身就不小再加上 Wine 的 API 转换CPU 占用会很高。内存建议 4GB 以上Wine 加 FEX-Emu 加 DXMT 三套东西同时跑内存吃紧的话程序很容易被系统杀掉。3.2 签名与开发者模式iOS 上跑非 App Store 的应用绕不开签名问题。你有两条路一是用免费的开发者证书自签缺点是每 7 天要重签一次而且同时只能签 3 个应用二是用付费开发者账号签名有效期一年设备数量也更多。如果你只是测试免费证书够用如果要长期跑建议上付费账号。开发者模式在 iOS 16 之后需要在设置里手动开启路径是设置、隐私与安全性、开发者模式。开启之后设备会重启重启后要再确认一次。这个模式不开的话自签的应用无法启动会直接闪退。我见过很多人卡在这一步以为是应用本身的问题其实是开发者模式没开。3.3 必要的依赖组件在 iOS 上搭建 Madeira你需要准备这些东西Wine 的 iOS 移植版本通常是编译好的 framework 或者静态库FEX-Emu 的 ARM64 版本需要包含 x86-64 的翻译前端DXMT 的 Metal 后端需要和 Wine 的版本匹配一个能加载这些组件的宿主 App 壳通常是一个简单的 iOS 应用负责初始化环境和启动 Wine这些组件大部分需要你自己编译因为预编译的二进制不一定和你的 iOS 版本、Xcode 版本匹配。编译 FEX-Emu 的时候要注意它的构建系统对 CMake 版本有要求太低会报错。DXMT 需要 Metal 的头文件Xcode 里要装对应的 SDK。提示编译之前先确认 Xcode 的命令行工具已经安装用xcode-select --install检查。另外FEX-Emu 的源码里有一些针对特定 ARM64 扩展的优化如果你的设备不支持这些扩展编译时要关掉对应的选项否则运行时会崩。4. 核心实操从零搭建 Madeira 运行环境4.1 编译 Wine 的 iOS 版本Wine 的源码本身支持交叉编译但 iOS 的交叉编译需要额外的配置。你需要一个 iOS 的 toolchain 文件指定 SDK 路径、目标架构、最低系统版本。配置命令大概长这样./configure --hostaarch64-apple-darwin \ --with-wine-tools../wine-tools \ --disable-tests \ --without-x \ --without-freetype \ CFLAGS-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64 -miphoneos-version-min15.0 \ LDFLAGS-isysroot $(xcrun --sdk iphoneos --show-sdk-path) -arch arm64这里有几个关键点。--without-x是必须的因为 iOS 没有 X11Wine 的图形输出要走 DXMT 或者原生的 CoreGraphics 后端。--disable-tests可以省掉编译测试的时间测试代码在 iOS 上跑不了。--with-wine-tools指向一个已经编译好的桌面版 Wine 工具集因为编译过程中需要用到一些在宿主上运行的工具。编译过程大概需要半小时到一小时取决于你的机器性能。编译完成后你会得到一堆.dylib和.so文件这些就是 Wine 的运行时。把它们打包成一个 framework 或者直接放进宿主 App 的 bundle 里。4.2 集成 FEX-EmuFEX-Emu 的编译相对独立它不依赖 Wine 的源码。你需要从它的仓库拉代码然后用 CMake 配置。关键配置项是目标架构和是否启用 JITcmake -DCMAKE_TOOLCHAIN_FILEios.toolchain.cmake \ -DCMAKE_OSX_ARCHITECTURESarm64 \ -DENABLE_JITON \ -DENABLE_X86_64ON \ ..ENABLE_JITON是必须的因为 FEX-Emu 的核心就是动态翻译没有 JIT 就只能解释执行性能会差一个数量级。但 iOS 对 JIT 有限制你需要确保你的宿主 App 有com.apple.security.cs.allow-jit这个 entitlement否则分配可执行内存会失败。编译完成后FEX-Emu 会生成一个静态库或者动态库。把它链接到宿主 App 里然后在启动 Wine 之前初始化 FEX-Emu 的运行时。初始化的顺序很重要先初始化 FEX-Emu再初始化 Wine最后加载 PE 文件。顺序错了会导致地址空间冲突。4.3 配置 DXMTDXMT 的编译需要 Metal 的 SDKXcode 里自带。它的构建系统也是 CMake配置的时候要指定 Metal 的路径和 Wine 的头文件路径cmake -DCMAKE_TOOLCHAIN_FILEios.toolchain.cmake \ -DCMAKE_OSX_ARCHITECTURESarm64 \ -DWINE_INCLUDE_DIR/path/to/wine/include \ -DMETAL_SDK_PATH$(xcrun --sdk iphoneos --show-sdk-path) \ ..DXMT 编译出来是一个.metallib文件加一个动态库。.metallib是 Metal 的着色器库需要放进 App 的 bundle 里。动态库链接到宿主 App在 Wine 初始化图形驱动的时候加载。这里有个容易忽略的点DXMT 的着色器编译是在运行时做的第一次运行某个程序时会有明显的卡顿因为它在编译 Metal 着色器。编译结果会缓存第二次运行就快了。如果你发现某个程序第一次跑特别慢别以为是性能问题等它编译完就好了。4.4 宿主 App 的初始化流程宿主 App 是整个链路的入口它负责按顺序初始化各个组件。一个典型的初始化流程是这样的检查开发者模式和 JIT 权限初始化 FEX-Emu 运行时分配翻译缓存初始化 Wine 的运行时加载ntdll.dll和kernel32.dll的实现初始化 DXMT创建 Metal 设备和命令队列加载目标 PE 文件把控制权交给 WineWine 调用 FEX-Emu 执行 x86-64 代码遇到 API 调用回到 Wine遇到图形调用转到 DXMT这个流程里第 2 步和第 3 步的顺序不能反。FEX-Emu 需要先占好地址空间Wine 的加载器才能把 PE 文件映射到正确的位置。如果反了PE 文件可能映射到 FEX-Emu 需要的地址范围导致翻译缓存分配失败。注意初始化过程中如果任何一步失败要立即清理已经分配的资源否则下次启动会残留状态导致更奇怪的问题。我遇到过 FEX-Emu 初始化失败后没清理第二次启动时 Wine 直接报内存错误查了半天才发现是残留的共享内存没释放。5. 常见问题与排查技巧实录5.1 Wine 乱码问题Wine 乱码是最常见的问题之一表现是程序界面上的文字变成方块或者问号。根本原因是字体缺失或者字符集不匹配。Wine 默认使用宿主系统的字体但 iOS 的字体和 Windows 的字体集不一样某些中文字符在 iOS 的默认字体里没有对应字形。解决办法是往 Wine 的字体目录里放一套完整的字体比如文泉驿或者思源黑体。然后在 Wine 的注册表里设置字体替换规则把SimSun、Microsoft YaHei这些 Windows 字体映射到你放的字体上。注册表的位置是HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes。如果放了字体还是乱码检查一下 Wine 的 locale 设置。Wine 需要正确的LANG和LC_ALL环境变量才能正确处理多字节字符。在 iOS 上这些环境变量要在启动 Wine 之前设置好不能中途改。5.2 程序启动闪退闪退的原因很多排查要一步步来。先看日志Wine 的日志会输出到 stderr你可以在宿主 App 里把 stderr 重定向到文件。日志里如果有Unhandled exception或者Page fault说明是指令翻译或者内存访问出了问题大概率是 FEX-Emu 的兼容性问题。如果日志里没有明显错误程序就是静默退出那可能是图形初始化失败。DXMT 在创建 Metal 设备的时候如果失败Wine 的图形驱动会返回错误但有些程序不检查返回值直接继续执行然后在某个图形调用上崩掉。这种情况可以在 DXMT 里加日志看 Metal 设备创建是否成功。还有一种闪退是签名问题。自签的应用如果 entitlement 不对系统会在启动时直接杀掉进程日志里会有Code Signature Invalid之类的信息。检查一下你的签名配置确保allow-jit和allow-unsigned-executable-memory这两个 entitlement 都在。5.3 性能调优性能是 iOS 上跑 Wine 的另一个大问题。FEX-Emu 的翻译开销、Wine 的 API 转换开销、DXMT 的图形转换开销三层叠加下来实际性能可能只有原生的三分之一到一半。调优的方向有几个开启 FEX-Emu 的块缓存让翻译过的代码块复用减少重复翻译调整 Wine 的线程模型iOS 的线程调度和桌面不一样某些同步原语在 iOS 上开销更大降低 DXMT 的渲染分辨率Metal 在高分辨率下的填充率开销比 D3D 大关闭不必要的 Wine 调试输出日志写多了也会拖慢速度我实测下来块缓存对性能的影响最大开启后某些程序的帧率能提升一倍。但块缓存会占内存如果你的设备内存小要权衡一下。5.4 常见问题速查表问题现象可能原因排查方向界面文字乱码字体缺失或 locale 错误检查字体目录和 LANG 环境变量启动即闪退签名问题或 JIT 权限不足检查 entitlement 和开发者模式运行中崩溃FEX-Emu 翻译错误查看 stderr 日志中的异常信息图形花屏DXMT 特性支持不全尝试降低 D3D 特性级别性能极低块缓存未开启或内存不足检查 FEX-Emu 配置和设备内存音频无声音频后端未初始化检查 Wine 的音频驱动配置6. 进阶话题从 Madeira 延伸出去的可能性6.1 和其他兼容层方案的对比Madeira 这条链路不是唯一的选择。另一种思路是用 QEMU 做全系统模拟然后在模拟的 Windows 里跑程序。这种方案兼容性更好因为整个 Windows 环境都是模拟的但性能差很多而且资源占用大在 iOS 上不太现实。还有一种思路是用云游戏的方式把 Windows 程序跑在远程服务器上iOS 只做视频流解码。这种方案性能最好但依赖网络而且需要服务器资源。Madeira 的价值在于它是本地的不依赖网络适合那些对延迟敏感或者需要离线使用的场景。6.2 对 iOS 开发者的参考价值即使你不打算跑 Windows 程序Madeira 的技术栈对 iOS 开发者也有参考价值。FEX-Emu 的 JIT 翻译机制、DXMT 的图形 API 转换、Wine 的 API 兼容层设计这些都是跨平台兼容领域的经典问题。理解这些方案对做跨平台框架、模拟器、甚至是一些性能优化工作都有帮助。比如 FEX-Emu 的块缓存机制本质上是一种代码缓存策略和 JIT 编译器里的 inline cache 思路类似。DXMT 的 API 转换和图形引擎里的抽象层设计是一个道理。这些思路可以迁移到其他领域。6.3 后续可以扩展的方向Madeira 目前主要支持 x86-64 的 Windows 程序对 32 位的支持还不完善。如果你有 32 位的程序要跑可能需要额外的翻译层。另外DXMT 对 D3D12 的支持还在开发中目前主要是 D3D9 到 D3D11。如果你要跑的游戏用了 D3D12可能还得等或者找其他方案。还有一个方向是优化启动速度。目前从点击图标到程序界面出来大概要十几秒主要是 Wine 和 FEX-Emu 的初始化开销。如果能做到预初始化或者懒加载启动速度可以提升不少。这个需要改宿主 App 的架构把一些初始化工作提前到后台做。我在实际搭建过程中最大的体会是这条链路里每个组件单独看都不复杂但组合在一起版本匹配和初始化顺序就是最大的坑。我建议你先用最简单的 Windows 程序测试比如记事本或者计算器确认整条链路通了再逐步尝试更复杂的程序。每换一个程序都可能遇到新的兼容性问题要有耐心一步步排查。
返回列表