
1. 从“Madeira”这个名字说起它到底想解决什么问题第一次看到“Madeira”这个项目名很多人会以为是葡萄酒相关的项目毕竟热搜词里赫然挂着“Wine”。但稍微往深里看一层就会发现这里的“Wine”指的是那个在 Linux 和 macOS 上跑 Windows 程序的兼容层而“Madeira”则是一个围绕 Wine 生态、面向 iOS 与 x86-64 架构的兼容性实验项目。它的核心目标很直接让原本为 Windows 编译的 x86-64 程序能够在 iOS 设备上跑起来同时借助 FEX-Emu 和 DXMT 这两块拼图把指令翻译和图形翻译这两件最难的事分别解决掉。我自己接触这类项目有一段时间了从最早的纯 Wine 折腾到后来在 ARM 设备上跑 x86 程序踩过的坑基本能写一本小册子。Madeira 这个组合之所以值得单独拿出来讲是因为它把几个原本各自为战的技术点串成了一条链路Wine 负责 Windows API 的转译FEX-Emu 负责 x86-64 到 ARM64 的指令翻译DXMT 负责把 Direct3D 调用翻译成 Metal。三者叠加才有可能在 iOS 这种封闭且架构完全不同的环境里让一个 Windows 程序真正跑出画面。这篇文章适合谁看如果你是对跨平台兼容层感兴趣的开发者或者手上有 iOS 设备想折腾 Windows 程序又或者你只是好奇“为什么有人非要在手机上跑 Windows 软件”那接下来的内容应该能给你一些实在的参考。我会把整体设计思路、核心组件的选型逻辑、实际搭建流程、以及我踩过的那些坑尽量讲清楚。需要提前说明的是iOS 环境的封闭性决定了这条路不会是“一键安装”那么简单很多环节需要你自己动手编译和签名。2. 整体架构拆解Wine、FEX-Emu、DXMT 各自扮演什么角色2.1 为什么不能只用 Wine 解决问题很多人对 Wine 的理解停留在“Linux 上跑 exe”的层面觉得只要把 Wine 移植到 iOS 就行了。这个想法在 x86 架构的设备上或许还有一丝可能但 iOS 设备从 A7 芯片开始就是 ARM 架构而绝大多数 Windows 程序是 x86 或 x86-64 指令集编译出来的。指令集不兼容这是第一道硬门槛。Wine 本身并不做指令翻译它做的是 API 层面的转译。也就是说它把 Windows 程序调用的CreateWindowEx、ReadFile这类 API映射到宿主系统的对应实现上。但如果程序本身的机器码是 x86-64 的ARM 处理器根本读不懂这些指令Wine 再厉害也无从下手。所以必须有一个东西先把 x86-64 指令翻译成 ARM64 指令Wine 才有机会介入。这就是 FEX-Emu 的位置。它是一个用户态的 x86-64 到 ARM64 的二进制翻译器工作在用户空间不需要修改内核。相比 QEMU 这种全系统模拟方案FEX-Emu 的开销要小得多因为它只翻译用户态指令而且带 JIT 缓存重复执行的代码块不需要反复翻译。实测下来在 ARM 设备上跑一些轻量级 Windows 程序FEX-Emu 的性能损耗大概在 30% 到 50% 之间具体取决于程序的指令特征。2.2 DXMT 为什么是图形链路的关键一环指令翻译解决了“程序能跑”的问题但“能看见画面”是另一回事。Windows 程序渲染图形绝大多数走的是 Direct3D而 iOS 上唯一可用的底层图形 API 是 Metal。这两者之间的鸿沟需要一个翻译层来填。传统方案是用 DXVK 把 D3D 翻译成 Vulkan再在 iOS 上找一个 Vulkan 到 Metal 的转换层。但 iOS 对 Vulkan 的支持本身就非常有限这条路走起来很别扭。DXMT 的思路更直接它直接把 D3D 调用翻译成 Metal省掉了中间的 Vulkan 环节。少一层转换意味着少一层性能损耗和兼容性问题。Madeira 选择 DXMT 而不是 DXVK 加 MoltenVK 的组合我认为核心考量就是链路长度。在 iOS 这种资源受限的环境里每多一层抽象出问题的概率就成倍增加。DXMT 虽然相对年轻但它针对 Metal 做了不少优化特别是在渲染管线的状态管理上比通用转换层更贴合 iOS GPU 的工作方式。2.3 三者的协作关系与数据流向把这三个组件串起来看一个 Windows 程序的执行流程大致是这样的程序启动后FEX-Emu 接管 x86-64 指令的执行遇到系统调用时转交给 WineWine 把 Windows API 调用映射到 POSIX 接口当程序发起 D3D 渲染调用时Wine 的 d3d 模块把请求转给 DXMTDXMT 再翻译成 Metal 命令提交给 GPU。这个链路里FEX-Emu 和 Wine 的边界在系统调用层Wine 和 DXMT 的边界在图形 API 层。每一层的接口定义是否清晰直接决定了整个系统的稳定性。我在调试时遇到过一个问题某个程序在创建窗口时卡死最后定位到是 FEX-Emu 对某个 x86-64 系统指令的翻译有偏差导致 Wine 收到的参数不对。这种跨层的问题排查起来非常费劲所以理解数据流向对定位故障很有帮助。3. 核心组件选型背后的考量与替代方案对比3.1 FEX-Emu 对比 QEMU 用户态模拟在 x86-64 到 ARM64 的翻译方案里QEMU 的用户态模式是另一个常见选择。我两个都用过说说实际感受。QEMU 的优势是成熟度高支持的指令集特性更全遇到冷门指令不容易翻车。但它的 JIT 翻译策略相对保守翻译块粒度小重复翻译的开销大。FEX-Emu 在这方面做了更多优化它的翻译块更大而且对常见的 x86-64 指令序列有专门的快速路径。在 iOS 上FEX-Emu 还有一个隐性优势它的代码库对 ARM64 的适配更现代编译时对 iOS 的工具链兼容性更好。QEMU 的代码库历史包袱重在 iOS 上编译经常遇到各种头文件和系统调用的适配问题。当然FEX-Emu 也不是没有短板它对某些 x86-64 扩展指令集的支持不如 QEMU 完整如果你的目标程序用到了 AVX-512 这类指令可能会遇到翻译失败。3.2 DXMT 对比 DXVK 加 MoltenVK 的链路前面提到过 DXMT 直接翻译到 Metal 的思路这里展开说说为什么我不推荐在 iOS 上走 DXVK 加 MoltenVK 的路线。DXVK 本身是为 Vulkan 设计的它的很多优化都依赖 Vulkan 的特性比如描述符集、管线缓存等。MoltenVK 把这些 Vulkan 特性映射到 Metal 时不可避免地会有语义损失。两层翻译叠加调试难度是指数级上升的。DXMT 虽然年轻但它的设计目标很明确只做 D3D 到 Metal 的翻译不做多余的事。它的代码结构比 DXVK 简洁很多遇到问题时更容易定位。而且 DXMT 对 Metal 的某些特性利用得更充分比如 Metal 的 argument buffer 机制DXMT 用得很到位减少了 CPU 和 GPU 之间的数据同步开销。3.3 Wine 版本的选择与编译配置Wine 的版本选择是个容易被忽视但很关键的点。Madeira 项目里用的是较新的 Wine 版本具体版本号会随项目更新变化。我的建议是不要盲目追新也不要死守旧版本。新版本通常对 D3D 的支持更好但可能引入新的兼容性问题旧版本稳定但可能缺少某些必要的 API 实现。编译 Wine 时有几个配置项需要特别注意。--enable-archs参数要确保包含 i386 和 x86_64因为很多 Windows 程序是 32 位的。--with-metal要开启否则 DXMT 无法正常工作。还有--disable-tests建议加上在 iOS 上编译测试套件既耗时又容易失败没必要在搭建阶段浪费时间。4. 在 iOS 上搭建 Madeira 环境的完整实操流程4.1 准备工作设备、工具链与依赖项先说设备要求。你需要一台运行较新 iOS 版本的设备具体版本号取决于你使用的开发者工具链支持范围。设备需要开启开发者模式这个在设置里的隐私与安全性菜单里能找到。注意开启开发者模式需要设备重启而且部分系统版本对开启方式有额外要求如果找不到入口可以先连接一次 Xcode 或者使用开发者相关的配置工具。工具链方面你需要 macOS 环境来编译 iOS 目标。Xcode 是必须的版本要和你 iOS 设备的系统版本匹配。另外需要安装 Homebrew 来管理依赖CMake 和 Ninja 用于构建还有 Python 3 用于跑一些构建脚本。依赖库方面Wine 需要 libxml2、libpng、freetype 这些基础库FEX-Emu 需要 LLVM 和它的 JIT 相关组件DXMT 需要 Metal 框架的头文件。提示整个编译过程对磁盘空间要求不小建议预留至少 50GB 的可用空间。中间产物和缓存文件会占很多地方空间不够会在链接阶段报出莫名其妙的错误。4.2 编译 FEX-Emu 的注意事项FEX-Emu 的编译是整个流程里最耗时的一步在 M 系列芯片的 Mac 上大概需要 40 分钟到 1 小时。克隆代码仓库后先跑一遍依赖检查脚本确保 LLVM 的版本符合要求。FEX-Emu 对 LLVM 版本比较敏感太新或太旧都可能导致编译失败。配置 CMake 时-DCMAKE_BUILD_TYPERelease是必须的Debug 版本在 iOS 上跑起来慢得没法用。-DENABLE_JITON要确保开启否则 FEX-Emu 会退化成解释执行模式性能差好几个数量级。还有一个容易忽略的点-DBUILD_TESTSOFFFEX-Emu 的测试套件在 iOS 上编译会报错直接关掉省事。编译完成后你会得到一个动态库文件。这个库需要和 Wine 的可执行文件一起打包进 iOS 应用的 bundle 里。注意检查库的架构确保是 arm64 的可以用lipo -info命令确认。4.3 Wine 的交叉编译与配置Wine 的交叉编译需要指定目标为 iOS。这里有个坑Wine 的构建系统对交叉编译的支持不是特别友好很多配置检测会默认按宿主系统来。我的做法是先在一个干净的目录里配置明确传入--hostaarch64-apple-darwin和 iOS 的 SDK 路径。配置参数里--enable-archsi386,x86_64要加上--with-metal要开启--without-x要加上因为 iOS 没有 X11。还有--disable-winegcc在某些版本上需要加否则 winegcc 的编译会出问题。这些参数不是一成不变的不同 Wine 版本可能有差异遇到报错时先看 config.log 里的具体错误信息。编译 Wine 的时间也不短大概 30 到 45 分钟。编译完成后你需要把 Wine 的运行时文件、FEX-Emu 的库、DXMT 的库以及一个 Windows 程序的测试样本一起放进一个 iOS 应用的目录结构里。这个目录结构需要符合 iOS 的 bundle 规范可执行文件放在根目录动态库放在 Frameworks 子目录。4.4 DXMT 的编译与 Metal 着色器缓存DXMT 的编译相对快一些大概 15 分钟。它依赖 Metal 框架所以必须在 macOS 上编译不能在其他系统上交叉编译。编译时注意-DCMAKE_OSX_DEPLOYMENT_TARGET要设置成你 iOS 设备支持的最低版本设太高会导致库在旧设备上无法加载。DXMT 有一个 Metal 着色器缓存机制第一次运行程序时会把 D3D 着色器编译成 Metal 着色器并缓存起来。这个编译过程在 iOS 设备上可能比较慢特别是复杂的着色器第一次加载可能要等十几秒。我的建议是第一次运行时耐心等待不要以为卡死了就强杀进程。缓存生成后后续启动会快很多。4.5 签名、打包与安装到设备iOS 应用的签名是绕不过去的环节。你需要一个开发者证书免费的个人开发者账号也能用但签名有效期只有 7 天过期后需要重新签名。签名时要注意bundle 里的所有可执行文件和动态库都需要签名漏掉任何一个都会导致应用启动时被系统杀掉。打包成 ipa 文件后可以通过 Xcode 的 Devices and Simulators 窗口安装也可以用一些第三方工具。安装后第一次启动时iOS 会提示“不受信任的开发者”需要去设置里的设备管理里手动信任。这个步骤每次重新签名后都要做一遍。注意iOS 对应用的内存使用有严格限制不同设备型号的限制不同。如果你的 Windows 程序内存占用较大可能会被系统强制终止。建议先从记事本这类轻量级程序开始测试确认链路通了再尝试更复杂的程序。5. 实际运行中的性能表现与调优经验5.1 指令翻译的性能损耗实测我在一台搭载 A15 芯片的 iOS 设备上跑了几个测试程序记录了一些数据。记事本这类几乎不涉及图形渲染的程序启动时间大约 3 到 5 秒操作响应基本跟手文字输入有轻微延迟但可以接受。计算密集型的程序比如一个简单的质数计算器性能大约是原生 x86-64 设备的 40% 到 50%。这个损耗主要来自 FEX-Emu 的指令翻译开销以及 Wine 的 API 转译开销。图形程序的性能波动更大。一个用 D3D9 渲染的简单 3D 场景帧率大概在 20 到 30 帧之间复杂场景会掉到 15 帧以下。DXMT 的翻译效率在这里是瓶颈特别是遇到复杂的着色器时Metal 着色器的编译和切换开销比较明显。5.2 内存管理与后台策略iOS 的内存管理策略和桌面系统完全不同。桌面系统内存不够时会用交换分区iOS 则是直接杀掉占用内存过多的进程。Wine 和 FEX-Emu 本身就有一定的内存开销加上 Windows 程序自身的内存占用很容易触碰到系统的限制。我的经验是在 Wine 的配置里把WINEDEBUG设成-all关掉所有调试输出能省下不少内存。FEX-Emu 的 JIT 缓存大小也可以调默认值偏大适当调小能降低内存占用代价是翻译缓存命中率下降性能会受一点影响。DXMT 那边Metal 的资源分配策略可以调把一些不常用的资源改成延迟分配也能省内存。5.3 图形渲染的兼容性调优DXMT 虽然兼容性不错但也不是所有 D3D 特性都支持。我遇到过几个典型问题某个程序用了 D3D11 的几何着色器DXMT 对这个特性的支持不完整导致画面缺失。还有程序用了多采样抗锯齿DXMT 在 Metal 上实现 MSAA 的方式和原生 D3D 有差异画面边缘会有轻微锯齿。遇到这类问题可以先查 DXMT 的文档看有没有对应的配置项可以绕过。有些特性可以通过环境变量禁用让程序回退到兼容路径。如果实在不支持那就只能换程序或者等 DXMT 更新。我在调试图形问题时会先用一个简单的 D3D 测试程序确认基础渲染链路是通的然后再逐步增加复杂度这样容易定位问题出在哪一层。6. 常见问题排查与避坑指南6.1 启动即崩溃的排查思路应用启动后立刻闪退是最常见也最让人头疼的问题。排查时按这个顺序来先看 iOS 的系统日志用 Xcode 的 Devices 窗口能看到崩溃时的调用栈。如果调用栈里出现dyld相关的错误多半是签名问题或者动态库路径不对。如果调用栈停在 FEX-Emu 或 Wine 的某个函数里那可能是编译配置有问题。我遇到过一次启动崩溃日志显示是 FEX-Emu 在初始化 JIT 时访问了非法内存。最后发现是编译 FEX-Emu 时 LLVM 版本不匹配换了一个 LLVM 版本重新编译就好了。所以遇到崩溃不要慌先看日志日志里通常有足够的线索。6.2 画面黑屏或花屏的处理程序能启动但画面黑屏说明图形链路有问题。先确认 DXMT 的库是否正确加载可以在 Wine 的日志里看 d3d 模块的初始化信息。如果 DXMT 加载了但画面还是黑的可能是 Metal 着色器编译失败。DXMT 会把编译失败的着色器信息输出到日志里根据日志里的着色器代码可以判断是哪个特性不支持。花屏通常是纹理格式转换的问题。D3D 和 Metal 支持的纹理格式不完全一样DXMT 在转换时如果遇到不支持的格式可能会用近似格式替代导致颜色偏差或花屏。这种情况比较难解只能看 DXMT 后续版本有没有改进。6.3 输入法与文字乱码问题热搜词里出现了“wine 乱码”和“wine 栏是乱码”说明这是很多人遇到的问题。Wine 在非中文 locale 下运行时菜单栏和对话框里的中文可能显示成方块或乱码。解决办法是设置正确的 locale 环境变量在 Wine 的启动脚本里加上LANGzh_CN.UTF-8和LC_ALLzh_CN.UTF-8。还需要确保系统里有中文字体可以把一个中文字体文件放进 Wine 的字体目录里。输入法的问题更复杂一些。iOS 的输入法和 Wine 的输入法处理机制之间需要一层桥接。目前 Madeira 项目对输入法的支持还在完善中简单的英文输入没问题中文输入可能需要额外的配置。我的建议是先用英文测试程序确认链路通了再折腾中文输入。6.4 常见问题速查表问题现象可能原因排查方法解决方向启动闪退签名问题或库路径错误查看 dyld 调用栈重新签名检查 Frameworks 目录启动闪退FEX-Emu 初始化失败查看 FEX-Emu 日志检查 LLVM 版本和编译配置画面黑屏DXMT 未加载查看 Wine d3d 日志确认 DXMT 库路径和依赖画面花屏纹理格式不兼容查看 DXMT 着色器日志等待 DXMT 更新或换程序中文乱码locale 设置错误检查环境变量设置 LANG 和 LC_ALL性能极差JIT 未开启检查 FEX-Emu 配置确保 ENABLE_JITON内存被杀内存占用过高查看系统日志关闭调试输出调小缓存7. 这套方案还能怎么扩展Madeira 目前的形态还是一个实验性项目但它打开了一个方向在 iOS 上跑 Windows 程序不一定需要完整的虚拟机通过指令翻译加 API 转译加图形翻译的组合也能达到可用的程度。后续可以扩展的地方不少比如对更多 D3D 特性的支持对输入法更完善的处理以及对 iOS 新版本系统的适配。我个人的体会是这类项目的价值不在于它能完美运行所有 Windows 程序而在于它验证了一条技术路径的可行性。每次 iOS 系统更新都可能引入新的限制或新的可能性保持关注项目的更新及时调整自己的配置是长期折腾这类项目的必备心态。如果你也在做类似的事情建议把每次编译和调试的过程记录下来这些记录在遇到新问题时往往能派上大用场。