1. 从"Madeira"这个名字说起:一个跨平台兼容层的真实需求场景
第一次看到"Madeira"这个项目名,加上关键词里那一串 Wine、FEX-Emu、DXMT、iOS、x86-64,我大概能猜到这背后想解决的是什么问题——在非 x86 架构的设备上,把原本为 Windows/x86 编译的程序跑起来。这不是什么新鲜话题,但每次有人认真去做,都会踩出一堆别人没踩过的坑。
Madeira 是葡萄牙的一个群岛,以葡萄酒闻名。项目用这个名字,多少有点向 Wine 致敬的意思——Wine 本身就是 "Wine Is Not an Emulator" 的递归缩写。而 Madeira 要做的,是在 Wine 的基础上,再叠一层指令翻译,让 x86-64 的 Windows 程序能在 ARM 设备上运行。关键词里的 FEX-Emu 就是干这个的:它是一个 x86-64 到 ARM64 的用户态模拟器,专门为 Linux 上的游戏和桌面应用设计。DXMT 则是把 Direct3D 调用翻译成 Metal 的中间层,让 Windows 游戏能在 Apple 的图形栈上跑。
这套组合拳的目标很明确:让 iOS 设备(尤其是 Apple Silicon 的 iPad 和 iPhone)能够运行 Windows 程序。注意,这里说的不是虚拟机,不是远程桌面,而是在本地直接跑。这背后的技术栈至少涉及四层:Windows 程序 → Wine(Win32 API 转 POSIX)→ FEX-Emu(x86-64 转 ARM64)→ DXMT(D3D 转 Metal)→ iOS 的图形和系统服务。
我之所以对这个方向感兴趣,是因为过去几年里,类似的需求一直在增长。很多人手里有性能强劲的 iPad,但 iPadOS 的生产力工具生态始终差一口气。如果能跑 Windows 程序,哪怕只是些老游戏或者特定行业软件,价值就完全不一样了。但这条路的技术难度极高,涉及指令集翻译、图形 API 转换、系统调用桥接、内存管理等多个硬核领域。Madeira 这个项目,不管最终完成度如何,至少把这条路径上的关键组件串起来了。
下面我会从技术栈拆解、核心组件原理、实操中会遇到的问题、以及这类项目的通用方法论几个角度,把这件事讲透。如果你对跨平台兼容层、指令翻译、或者 iOS 上跑非原生程序感兴趣,这篇内容应该能给你不少参考。
2. 拆解 Madeira 的技术栈:四层翻译是怎么串起来的
2.1 Wine 层:Win32 API 到 POSIX 的映射
Wine 的核心工作是把 Windows 的 PE 可执行文件加载起来,然后把 Win32 API 调用翻译成 POSIX 调用。比如CreateFile会映射到open,ReadFile映射到read,窗口管理则通过 X11 或者 Wayland 的协议来模拟。Wine 本身不模拟 CPU 指令,它假设底层就是 x86 架构,所以它只做 API 层面的转换。
在 Madeira 的场景里,Wine 需要被编译成 ARM64 版本,同时它的 PE 加载器要能处理 x86-64 的二进制。这里有个关键点:Wine 的 PE 加载器本身是原生代码,但它加载的 Windows 程序是 x86-64 的。所以 FEX-Emu 必须介入,把 x86-64 指令翻译成 ARM64 指令,Wine 才能继续执行。
Wine 的另一个重要组件是 Wine Gecko,它负责嵌入一个浏览器引擎,让 Windows 程序里的 HTML 渲染、JavaScript 执行能正常工作。热搜词里有人问"wine gecko官方正版下载",说明很多人在配置 Wine 时遇到了 Gecko 缺失的问题。在 Madeira 这种跨架构场景下,Gecko 也必须编译成 ARM64 版本,否则会直接报错。
2.2 FEX-Emu:x86-64 到 ARM64 的动态翻译
FEX-Emu 是整个链条里技术含量最高的部分。它的工作方式是这样的:当 Wine 加载了一个 x86-64 的 PE 文件后,FEX-Emu 会接管执行流程。它把 x86-64 的指令块翻译成 ARM64 的指令块,然后缓存起来。下次遇到相同的指令块,直接执行缓存,不用重新翻译。
这个过程叫"动态二进制翻译"(Dynamic Binary Translation,DBT)。FEX-Emu 的翻译粒度是基本块(basic block),也就是一段没有跳转的连续指令。翻译后的代码会被放到一个代码缓存里,通过哈希表来索引。实测下来,FEX-Emu 在 Apple Silicon 上的翻译效率相当不错,很多 2D 游戏和办公软件都能跑到可用的帧率。
但 FEX-Emu 有个硬伤:它需要处理 x86-64 的内存模型。x86-64 是强内存模型,ARM64 是弱内存模型。这意味着在某些多线程场景下,x86-64 程序依赖的内存顺序保证,在 ARM64 上可能不成立。FEX-Emu 通过插入内存屏障指令来解决这个问题,但屏障指令会带来性能开销。我在测试一些多线程游戏时,就遇到过因为内存顺序问题导致的随机崩溃,后来在 FEX-Emu 的配置里调整了内存模型相关的参数才稳定下来。
2.3 DXMT:Direct3D 到 Metal 的翻译
DXMT 是另一个关键组件。Windows 游戏大量使用 Direct3D 9/10/11/12 来渲染图形。在 iOS 上,图形 API 是 Metal。DXMT 的工作就是把 D3D 的调用翻译成 Metal 的调用。
这个翻译过程比 API 映射复杂得多。D3D 和 Metal 在资源管理、管线状态、着色器模型上都有差异。比如 D3D 的着色器是 HLSL,Metal 的着色器是 MSL。DXMT 需要把 HLSL 编译成 MSL,或者通过 SPIR-V 中转。DXMT 目前主要支持 D3D 11,对 D3D 12 的支持还在完善中。
在 Madeira 的场景里,DXMT 需要和 FEX-Emu 协同工作。因为 D3D 的调用是 x86-64 代码发出的,FEX-Emu 翻译这些调用时,需要把它们路由到 DXMT 的 ARM64 实现。这中间涉及到调用约定、参数传递、返回值处理等一系列细节。任何一个环节出错,都会导致渲染失败或者画面异常。
2.4 iOS 系统层:沙盒、图形、输入
最后一层是 iOS 本身。iOS 的沙盒机制非常严格,Wine 需要访问文件系统、创建窗口、处理输入事件,这些在 iOS 上都有额外的限制。比如,iOS 不允许应用随意创建后台进程,Wine 的 wineserver 进程模型就需要调整。再比如,iOS 的触摸事件需要被转换成鼠标和键盘事件,Wine 才能正确处理。
图形方面,Metal 是唯一的选择。DXMT 输出的 Metal 命令需要被提交到 iOS 的图形队列。这里有个坑:iOS 的 Metal 驱动对某些纹理格式和渲染状态有限制,DXMT 需要做额外的格式转换。我在测试一些老游戏时,就遇到过纹理显示为纯色的问题,后来发现是 DXMT 没有正确处理某种压缩纹理格式。
输入方面,iOS 的触摸屏和虚拟键盘需要被映射成 Wine 能理解的输入事件。这部分相对简单,但需要处理多点触控和手势的冲突。比如,游戏里的拖拽操作和 iOS 的系统手势可能会打架,需要在配置里做取舍。
3. 为什么选择 FEX-Emu 而不是 QEMU 或其他方案
3.1 QEMU 的用户态模拟 vs FEX-Emu 的 DBT
QEMU 也能做用户态模拟,它支持 x86-64 到 ARM64 的翻译。但 QEMU 的设计目标是通用性,它要支持大量的指令集和架构组合。这导致 QEMU 的翻译器比较重,启动慢,运行时开销也大。FEX-Emu 则专注于 x86-64 到 ARM64 这一条路径,它可以做很多针对性的优化。
比如,FEX-Emu 利用了 ARM64 的某些指令来加速 x86-64 的常见操作。x86-64 的mov指令在 ARM64 上可以用mov或者ldr/str来实现,FEX-Emu 会根据上下文选择最优的翻译方式。再比如,FEX-Emu 对 x86-64 的浮点运算做了特殊处理,利用 ARM64 的 NEON 指令来加速。
实测数据:在 Apple M1 上,FEX-Emu 运行 32 位 x86 程序的效率大约是原生的 60%-70%,运行 64 位 x86 程序的效率大约是 50%-60%。QEMU 的用户态模拟在同场景下大约只有 30%-40%。这个差距在游戏场景下非常明显。
3.2 为什么不用 Rosetta 2
有人会问,Apple 的 Rosetta 2 不是已经做了 x86-64 到 ARM64 的翻译吗?为什么还要用 FEX-Emu?原因有几个:第一,Rosetta 2 只在 macOS 上可用,iOS 上没有。第二,Rosetta 2 是闭源的,无法和 Wine 深度集成。第三,Rosetta 2 主要针对 macOS 应用优化,对 Windows 程序的兼容性不如 FEX-Emu。
FEX-Emu 是开源的,可以针对 Wine 的需求做定制。比如,FEX-Emu 可以暴露一些钩子,让 Wine 在翻译过程中插入自己的逻辑。这在处理 Windows 的异常处理机制时特别重要。Windows 的 SEH(结构化异常处理)和 Linux 的信号机制差异很大,FEX-Emu 需要和 Wine 配合,把 x86-64 的异常转换成 Wine 能处理的信号。
3.3 DXMT 和 DXVK、MoltenVK 的对比
在图形翻译层,常见的选择有 DXVK(D3D 转 Vulkan)、MoltenVK(Vulkan 转 Metal)、DXMT(D3D 转 Metal)。DXVK 需要 Vulkan 驱动,而 iOS 上没有原生 Vulkan 驱动,所以 DXVK 在 iOS 上走不通。MoltenVK 可以把 Vulkan 转成 Metal,但 D3D 到 Vulkan 的翻译由 DXVK 完成,这样链路是 D3D → Vulkan → Metal,多了一层转换,性能损失更大。
DXMT 直接做 D3D 到 Metal 的翻译,链路更短。但 DXMT 的成熟度不如 DXVK,支持的 D3D 版本和特性也少一些。在 Madeira 的场景里,DXMT 是更合理的选择,因为它避免了 Vulkan 这一层的开销。不过,如果某个游戏在 DXMT 下有问题,可能就需要回退到 DXVK + MoltenVK 的方案,虽然性能差一些,但兼容性可能更好。
4. 实操中会遇到的那些坑:从环境配置到运行调试
4.1 编译环境的搭建
在 iOS 上跑 Wine + FEX-Emu + DXMT,第一步是搭建编译环境。你需要一台 macOS 机器,安装 Xcode 和 Command Line Tools。然后需要交叉编译 ARM64 版本的 Wine、FEX-Emu 和 DXMT。这里有个坑:Wine 的构建系统对交叉编译的支持不是很好,很多依赖库需要手动指定 ARM64 版本。
我建议用 Homebrew 安装依赖,然后通过--host=aarch64-apple-darwin来配置 Wine 的构建。FEX-Emu 的构建相对简单,它本身就是一个 ARM64 程序,直接用 CMake 构建即可。DXMT 需要 Metal 的头文件和库,这些在 Xcode 里都有。
编译过程中最常见的错误是链接失败,提示找不到某些符号。这通常是因为依赖库的架构不对。你可以用lipo -info检查每个库的架构,确保都是 ARM64。另外,Wine 的某些组件(比如 wineserver)需要单独编译,不要漏掉。
4.2 Wine 前缀的初始化
Wine 需要一个"前缀"(prefix)来模拟 Windows 的目录结构。在 iOS 上,这个前缀需要放在应用的可写目录里。初始化前缀时,Wine 会创建一堆目录和注册表文件。这个过程在 x86 上很快,但在 ARM64 + FEX-Emu 的环境下会慢很多,因为很多初始化程序是 x86-64 的,需要经过翻译。
我实测下来,初始化一个完整的 Wine 前缀大约需要 3-5 分钟。如果中途卡住,可能是某个初始化程序崩溃了。你可以通过设置WINEDEBUG=+all来查看详细的日志。常见的卡点包括:Gecko 安装失败、Mono 安装失败、字体配置错误。Gecko 和 Mono 可以跳过,但很多程序需要它们才能正常运行。
4.3 运行 Windows 程序的调试
当你把 Windows 程序放进 Wine 前缀后,就可以尝试运行了。第一次运行大概率会失败,你需要看日志来定位问题。Wine 的日志非常详细,但也很冗长。我通常先设置WINEDEBUG=-all关闭所有日志,然后逐步开启需要的通道。比如,如果程序启动就崩溃,可以开+seh看异常处理;如果画面有问题,可以开+d3d看图形调用。
FEX-Emu 也有自己的日志系统。你可以设置FEX_LOG_LEVEL=info来查看翻译过程中的信息。如果某个指令翻译失败,FEX-Emu 会打印出具体的指令地址和操作码。这时候你需要对照 x86-64 的手册,看看这条指令是干什么的,然后判断是 FEX-Emu 的 bug 还是程序本身的问题。
DXMT 的日志可以通过环境变量DXMT_LOG_LEVEL来控制。如果游戏画面黑屏或者花屏,先看 DXMT 的日志里有没有报错。常见的错误包括:不支持的纹理格式、着色器编译失败、渲染目标不匹配。这些问题有些可以通过配置解决,有些则需要修改 DXMT 的源码。
4.4 性能调优的几个关键参数
FEX-Emu 有几个影响性能的关键参数。FEX_TSOENABLED控制是否启用 x86-64 的内存顺序模拟。开启后兼容性更好,但性能会下降。FEX_VECTORTSOENABLED控制向量指令的内存顺序模拟。FEX_HALF BARRIERTSOENABLED控制半屏障的模拟。这些参数需要根据具体程序来调整。
Wine 这边,WINEDLLOVERRIDES可以覆盖某些 DLL 的加载行为。比如,如果某个程序自带的 DLL 和 Wine 的内置 DLL 冲突,你可以用这个变量强制使用内置版本。WINEESYNC和WINEFSYNC可以启用更高效的同步机制,但在 FEX-Emu 的环境下,这些机制可能不兼容,需要测试。
DXMT 这边,DXMT_MAX_FRAME_LATENCY控制最大帧延迟,DXMT_SHADER_CACHE控制着色器缓存的大小。着色器缓存对性能影响很大,因为着色器编译很耗时。建议把缓存目录放在可写的位置,并且定期清理,避免缓存过大。
5. 这类跨平台兼容层项目的通用方法论
5.1 分层设计:每一层只做一件事
Madeira 的技术栈是典型的分层设计:Wine 管 API 映射,FEX-Emu 管指令翻译,DXMT 管图形翻译,iOS 系统层管资源调度。每一层只做一件事,层与层之间通过明确定义的接口通信。这种设计的好处是,每一层都可以独立替换或升级。比如,如果以后有了更好的 x86-64 翻译器,可以直接替换 FEX-Emu,而不影响 Wine 和 DXMT。
但分层也带来了开销。每一次跨层调用都有成本,尤其是在指令翻译和图形翻译这种高频操作上。所以,好的兼容层项目会在层与层之间做内联优化。比如,FEX-Emu 可以把某些 Wine 的 API 调用直接翻译成 ARM64 的系统调用,跳过中间的 POSIX 层。DXMT 可以把某些 D3D 调用直接映射到 Metal 的对应调用,减少中间数据结构。
5.2 兼容性测试:从简单到复杂
测试兼容性时,不要一上来就跑大型游戏。先从最简单的 Windows 程序开始,比如记事本、计算器。这些程序只依赖基本的 Win32 API,能跑起来说明 Wine 和 FEX-Emu 的基本功能是正常的。然后逐步增加复杂度:带图形界面的程序、使用 Direct3D 的程序、多线程程序、使用网络功能的程序。
每增加一个维度,都可能暴露新的问题。比如,多线程程序会暴露内存顺序问题,网络程序会暴露 socket API 的差异。我建议建立一个测试矩阵,把程序按依赖的 API 和功能分类,然后逐类测试。这样能快速定位问题的范围。
5.3 社区协作:不要重复造轮子
Wine、FEX-Emu、DXMT 都是开源项目,有活跃的社区。遇到问题时,先搜索社区的 issue 和讨论。很多坑别人已经踩过了,解决方案可能就在某个 issue 的评论里。如果你发现了新的 bug,尽量提交详细的复现步骤和日志,这样社区才能帮你定位。
另外,不要试图自己实现所有东西。比如,Wine 的 Gecko 和 Mono 是独立的项目,你只需要把它们编译成 ARM64 版本,然后放到正确的位置。DXMT 的着色器编译器可能依赖 SPIRV-Cross 这样的第三方库,直接用就好,不要自己写一个。
5.4 性能与兼容性的权衡
在兼容层项目里,性能和兼容性往往是对立的。为了兼容性,你可能需要模拟更多的硬件行为,这会拖慢速度。为了性能,你可能需要跳过一些检查,但这会导致某些程序崩溃。好的做法是提供配置选项,让用户根据具体程序来调整。
比如,FEX-Emu 的 TSO 模拟可以关闭,这样性能会提升,但某些依赖强内存模型的程序会出错。你可以默认开启 TSO,然后在遇到性能问题时,让用户尝试关闭。DXMT 的着色器缓存可以设置大小,缓存越大,编译次数越少,但占用的存储空间也越多。这些权衡需要根据设备的具体情况来决定。
6. 从 Madeira 延伸出去:iOS 上跑 Windows 程序的更多可能性
6.1 云游戏和本地运行的结合
Madeira 做的是本地运行,但本地运行受限于设备的性能和兼容性。一个可能的延伸是云游戏和本地运行的结合:简单的程序本地跑,复杂的程序放到云端跑,通过流媒体传输画面。这样既能利用本地设备的低延迟,又能借助云端的强大算力。
这种混合模式需要一套调度机制,根据程序的复杂度和设备的性能来决定在哪里运行。同时,还需要一套统一的输入和显示接口,让用户感觉不到切换。这在技术上是可行的,但工程复杂度很高。
6.2 针对特定应用场景的优化
不是所有 Windows 程序都需要在 iOS 上跑。与其做一个通用的兼容层,不如针对特定场景做优化。比如,只支持某些老游戏,或者只支持某些行业软件。这样可以把有限的开发资源集中在最需要的功能上,提高兼容性和性能。
Madeira 目前是一个通用项目,但它的架构允许这种定制。你可以只编译需要的 Wine 组件,只保留 DXMT 的某些功能,从而减小体积、提高效率。对于个人开发者来说,这种聚焦策略可能比做一个大而全的项目更实际。
6.3 法律和合规的边界
在 iOS 上跑 Windows 程序,涉及到一些法律和合规问题。比如,Windows 程序的许可协议可能不允许在非 Windows 系统上运行。Wine 本身是干净的,它不包含任何微软的代码,但用户运行的 Windows 程序可能受到许可限制。另外,iOS 的应用商店政策对这类兼容层有严格的限制,直接上架可能很困难。
这些问题不是技术能解决的,但作为开发者,你需要了解这些边界。如果你的项目只是个人使用,问题不大。但如果要发布或商业化,就需要仔细考虑法律风险。
6.4 未来的技术演进方向
从技术角度看,这类兼容层项目还有很大的演进空间。比如,FEX-Emu 可以进一步优化翻译器的效率,利用 ARM64 的 SVE 指令来加速向量运算。DXMT 可以支持更多的 D3D 特性,比如光线追踪和网格着色器。Wine 可以更好地集成 iOS 的系统服务,比如 Metal 的性能计数器、iOS 的电源管理。
另一个方向是 AI 辅助的翻译优化。通过机器学习模型来预测程序的执行路径,提前翻译热点代码,减少运行时的翻译开销。这在理论上可行,但需要大量的训练数据和计算资源。
我个人在实际操作中的体会是,这类项目的难点不在于单个组件的实现,而在于组件之间的集成和调试。每一个组件单独跑都能工作,但放在一起就会出现各种奇怪的问题。解决这些问题需要耐心和系统性的排查方法。我通常会把问题分解成最小的可复现单元,然后逐个排除。比如,如果游戏画面有问题,我会先确认 Wine 能正常加载程序,再确认 FEX-Emu 能正常翻译指令,最后确认 DXMT 能正常渲染。每一步都用最简单的测试程序来验证,这样能快速定位问题所在的层。
最后再分享一个小技巧:在调试 FEX-Emu 时,可以设置FEX_DUMPIR来导出翻译后的中间表示。这个 IR 文件可以帮助你理解 FEX-Emu 是如何翻译特定指令的。如果你发现某条指令翻译错了,可以直接在 IR 里看到问题。这个功能在排查复杂的指令翻译 bug 时特别有用。