1. 从“Madeira”这个名字说起:它到底想解决什么问题
第一次看到“Madeira”这个项目标题,加上 Wine、FEX-Emu、DXMT、iOS、x86-64 这一串关键词,我脑子里第一反应是:这又是一个在“跨平台运行 Windows 程序”这条老路上做新文章的东西。Wine 本身是这套体系里最出名的一环,它做的事情说白了就是——在非 Windows 系统上,把 Windows 程序发出来的系统调用“翻译”成当前系统能听懂的调用,让 exe 文件不用装一个完整的 Windows 就能跑起来。而 Madeira 把 iOS 和 x86-64 也拉进来,说明它想碰的是一个更硬核的场景:在移动端、尤其是 iOS 这种封闭生态里,去承载原本为 x86-64 Windows 编译的软件。
这件事为什么难,得先讲清楚。Wine 在桌面 Linux 上跑了这么多年,靠的是 Linux 本身对 ELF、对系统调用、对内存映射的开放支持,加上 x86 和 x86-64 指令集在 PC 上是原生存在的,CPU 直接就能执行 Windows 程序的机器码,Wine 只需要处理“系统调用层”的翻译,不需要处理“指令集层”的翻译。可一旦目标平台换成 iOS,情况就完全变了:iOS 设备用的是 ARM 架构,CPU 根本看不懂 x86-64 的机器码,所以必须再加一层“指令翻译”,把 x86-64 指令动态翻译成 ARM64 指令。FEX-Emu 就是干这个的,它是一个用户态的 x86-64 到 ARM64 的模拟/翻译层,专门用来在 ARM 设备上跑 x86 程序。DXMT 则是另一块拼图,它负责把 Direct3D 的调用翻译成 Metal,因为 iOS 上图形接口是 Metal,不是 DirectX。
所以 Madeira 这个项目的核心价值,可以理解为:把 Wine(系统调用翻译)+ FEX-Emu(指令集翻译)+ DXMT(图形接口翻译)这三层拼在一起,尝试在 iOS 上运行 x86-64 的 Windows 程序。它解决的不是“让 iOS 跑 iOS 应用”这种常规需求,而是“让 iOS 设备去碰那些原本只属于 PC 的 Windows 软件生态”。适合谁来参考?一类是想研究跨平台兼容层架构的开发者,一类是手里有 iOS 设备、又想在上面折腾 Windows 老软件或游戏的玩家,还有一类是做移动端虚拟化、容器化方案的技术人员。哪怕你最后不真的去编译 Madeira,光是把它这套分层思路拆明白,对理解 Wine、FEX-Emu、DXMT 各自边界在哪儿,就已经很值了。
2. 整体架构拆解:三层翻译是怎么叠起来的
2.1 Wine 层:系统调用翻译的老本行
Wine 的定位从来不是“模拟 Windows”,而是“重新实现 Windows 的 API”。它提供了一套自己的 ntdll、kernel32、user32、gdi32 等库,当 Windows 程序调用 CreateFile、ReadFile、MessageBox 这些函数时,实际执行的是 Wine 自己写的实现,这些实现再去调用宿主系统的对应能力。在 Linux 上,宿主是 glibc 和 Linux 系统调用;在 macOS 上,宿主是 Darwin 的 libc 和 Mach 调用。Wine 之所以能跨这么多系统,就是因为它把“Windows API 到宿主 API”的映射做成了可移植的层。
但 Wine 有一个前提:它假设 CPU 能直接执行 Windows 程序的机器码。在 x86-64 Linux 上跑 x86-64 Windows 程序,这个前提成立;在 ARM 设备上跑 x86-64 Windows 程序,这个前提就不成立了。所以 Wine 单独放在 iOS 上是跑不起来的,必须下面垫一层指令翻译。这也是为什么 Madeira 的关键词里同时出现 Wine 和 FEX-Emu——它们不是二选一,而是上下叠放的关系。
2.2 FEX-Emu 层:把 x86-64 指令翻译成 ARM64
FEX-Emu 的工作机制,简单说就是“边跑边翻译”。它加载一个 x86-64 的 ELF 或 PE 文件,读取里面的 x86-64 指令,在运行时把这些指令块翻译成等价的 ARM64 指令块,翻译结果会缓存起来,下次再执行到同一段代码就直接用缓存。这种“块翻译 + 缓存”的策略,比逐条指令解释要快得多,因为大部分程序的执行热点都集中在少数代码块里,缓存命中率一高,整体性能就上来了。
FEX-Emu 还要处理 x86-64 和 ARM64 之间的寄存器映射、标志位语义差异、内存模型差异。x86-64 有 RAX、RBX、RCX 这些通用寄存器,ARM64 有 X0 到 X30,两边的数量和用途都不一样,FEX-Emu 需要维护一套映射表,把 x86 寄存器“安放”到 ARM 寄存器或内存里。标志位更麻烦,x86 的 EFLAGS 里 CF、ZF、SF、OF 这些标志,ARM64 的 NZCV 只覆盖了一部分,剩下的得靠额外指令模拟。这些细节决定了 FEX-Emu 的翻译质量和性能上限。
2.3 DXMT 层:Direct3D 到 Metal 的桥
图形是另一个大坑。Windows 程序画图走的是 Direct3D,iOS 上只有 Metal。DXMT 的作用就是把 D3D 的调用翻译成 Metal 的调用。它和 DXVK 的思路类似,DXVK 是把 D3D 翻译成 Vulkan,DXMT 是把 D3D 翻译成 Metal。为什么 iOS 上不能用 DXVK?因为 iOS 不开放 Vulkan,Metal 才是官方图形接口,所以必须有一个专门面向 Metal 的翻译层。
DXMT 要处理的不只是 API 名字的对应,还有资源管理、着色器编译、同步机制。D3D 的着色器是 HLSL 编译出来的字节码,Metal 用的是 MSL,DXMT 需要在中间做转换。同步方面,D3D 的 fence、query 和 Metal 的 command buffer、event 语义也不一样,翻译层要保证渲染顺序和资源依赖不被破坏。这一层的成熟度,直接决定了 Windows 游戏在 iOS 上能不能跑、跑得顺不顺。
2.4 三层叠起来后的数据流
把三层串起来看,一个 Windows 程序的执行路径大致是这样的:程序入口是 x86-64 机器码,FEX-Emu 把它翻译成 ARM64 并执行;执行过程中遇到 Windows API 调用,Wine 接管,把 API 请求转成宿主系统调用;如果这个 API 涉及图形,Wine 把 D3D 调用转给 DXMT,DXMT 再转成 Metal。整条链路里,FEX-Emu 负责“指令能不能跑”,Wine 负责“API 能不能用”,DXMT 负责“画面能不能出”。任何一层出问题,程序都跑不起来。
3. 核心细节与实操要点:每一层的关键参数和坑
3.1 Wine 在 iOS 上的裁剪与适配
Wine 代码库很大,直接往 iOS 上搬是不现实的。iOS 对可执行文件、动态库加载、系统调用都有严格限制,Wine 里大量依赖 Linux 或 macOS 特性的代码需要裁掉或替换。实操中通常的做法是:只保留 ntdll、kernel32、user32、gdi32 这些核心库,把依赖 X11、Wayland、ALSA 的模块换成 iOS 对应的实现,比如图形走 Metal、音频走 AudioToolbox、输入走 UIKit 的事件系统。
编译 Wine 时,--enable-win64这类选项在 iOS 上要重新评估,因为目标不是原生 x86-64,而是配合 FEX-Emu 的 x86-64 用户态。Wine 的构建系统基于 autotools 和 Makefile,交叉编译到 iOS 需要准备 iOS SDK、设置--host=arm-apple-darwin之类的三元组,还要处理代码签名和 entitlements。这里有个经验:iOS 对 JIT(即时编译)有严格限制,而 FEX-Emu 的块翻译本质上就是 JIT,所以 Madeira 在 iOS 上能不能跑,很大程度上取决于是否拿到了允许 JIT 的权限,或者是否改用了 AOT(提前编译)方案。这是整个项目最敏感、也最容易被卡住的地方。
3.2 FEX-Emu 的配置与性能调优
FEX-Emu 的配置项里,有几个直接影响性能。FEX_TSOENABLED控制是否启用 x86 的总存储顺序(TSO)模拟,x86 的内存模型比 ARM64 强,很多程序依赖这个顺序,关掉能提速但可能出错,开着更稳但更慢。FEX_MULTIBLOCK控制是否启用多块编译,开启后翻译粒度更大,缓存效率更高,但首次编译开销也更大。FEX_ROOTFS指定根文件系统路径,Wine 的 C 盘映射、注册表、DLL 搜索路径都依赖这个。
实测下来,FEX-Emu 在 ARM 设备上的性能损失通常在 30% 到 70% 之间,具体取决于程序的计算密集度和内存访问模式。计算密集、循环紧凑的程序,翻译缓存命中率高,损失小;频繁调用系统 API、大量小函数调用的程序,翻译开销占比大,损失就明显。调优时可以先开FEX_MULTIBLOCK和FEX_TSOENABLED,跑一遍基准,再逐项关掉对比,找到稳定性和速度的平衡点。
3.3 DXMT 的着色器编译与缓存
DXMT 在第一次遇到某个着色器时,需要把 D3D 字节码转成 Metal 着色器,再交给 Metal 编译。这个过程很慢,如果每次启动都重编,游戏加载会非常痛苦。所以 DXMT 支持着色器缓存,把编译结果存到磁盘,下次直接加载。缓存路径、缓存失效策略、缓存版本号这些都要配好,否则会出现“改了着色器但缓存没更新”的诡异问题。
另一个坑是 Metal 的着色器编译在 iOS 上可能受系统限制,某些高级特性在旧设备上不支持,DXMT 需要做能力检测和降级。比如 Metal 的 argument buffer、indirect command buffer 在不同 GPU 家族上支持程度不同,DXMT 要根据设备能力选择不同的翻译路径。这部分没有万能配置,只能针对目标设备逐个测试。
3.4 iOS 侧的限制与绕行思路
iOS 对进程、内存、文件系统的限制比桌面系统严得多。Wine 需要模拟一个 Windows 文件系统结构,通常映射到 iOS 的沙盒目录里,但沙盒路径、权限、持久化策略都要仔细设计。内存方面,iOS 对单个进程的内存上限有硬性约束,Wine 加 FEX-Emu 加 DXMT 三层叠起来,内存占用很容易触顶,需要做内存池、延迟加载、资源回收。
JIT 权限是最大的不确定性。如果目标环境不允许 JIT,FEX-Emu 就只能走解释执行或 AOT 路线,性能会大幅下降。AOT 路线需要在构建阶段就把 x86-64 代码翻译成 ARM64,但 Windows 程序往往是动态加载 DLL 的,AOT 很难覆盖所有代码路径。所以 Madeira 这类项目在 iOS 上的可行性,很大程度上取决于运行环境是否提供了足够的执行权限。
4. 实操过程:从零搭建一套可复现的验证环境
4.1 环境准备与依赖清单
先在 ARM64 Linux 环境上做验证,因为 Linux 对 JIT、对动态库加载的限制比 iOS 少,适合先把三层链路跑通。需要的组件包括:Wine 源码(建议用较新的稳定分支)、FEX-Emu 源码、DXMT 源码、一个 ARM64 的 Linux 发行版、以及一个用于测试的 x86-64 Windows 程序(建议从简单的记事本类程序开始,不要一上来就上游戏)。
依赖方面,Wine 需要 libfreetype、libfontconfig、libgnutls、libxml2 等;FEX-Emu 需要 CMake、Ninja、Clang(因为要生成 ARM64 代码);DXMT 需要 Metal 相关的头文件和库,在 Linux 上验证时可以先用 DXVK 替代,等链路通了再换 DXMT。构建顺序建议是:先编 FEX-Emu,再编 Wine(配置时指向 FEX-Emu 的运行时),最后处理图形层。
4.2 编译 FEX-Emu 的关键步骤
git clone https://github.com/FEX-Emu/FEX.git cd FEX git submodule update --init --recursive cmake -S . -B build -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ -DENABLE_LTO=ON cmake --build build编译时注意ENABLE_LTO开启后链接时间会变长,但运行时性能更好。如果目标设备不支持某些 ARM64 扩展指令,需要在 CMake 里关掉对应的特性开关,否则生成的代码会在目标设备上触发非法指令异常。编译完成后,用FEXRootFSFetcher拉取一个根文件系统,里面包含 Wine 运行所需的基础库。
4.3 配置 Wine 与 FEX-Emu 的对接
Wine 编译时,关键是让它的加载器走 FEX-Emu。通常做法是设置FEX_BIN环境变量指向 FEX-Emu 的可执行文件,然后通过一个包装脚本启动 Wine:
export FEX_ROOTFS=/path/to/rootfs export FEX_TSOENABLED=1 export FEX_MULTIBLOCK=1 export WINEPREFIX=/path/to/prefix $FEX_BIN/wine64 /path/to/app.exe第一次启动时,Wine 会初始化 prefix,创建 C 盘目录结构、注册表、DLL 链接。这个过程在 FEX-Emu 下会比较慢,因为大量系统调用都要经过翻译。初始化完成后,后续启动会快很多。如果卡在初始化阶段,可以开WINEDEBUG=+loaddll看是哪个 DLL 加载失败,常见原因是根文件系统里缺库或者路径映射不对。
4.4 图形层的接入与测试
图形层先用 DXVK 验证,因为 DXVK 在 Linux 上更成熟。把 DXVK 的 d3d11.dll、dxgi.dll 放到 Wine prefix 的 system32 目录,设置WINEDLLOVERRIDES=d3d11,dxgi=n让 Wine 用 DXVK 的实现。跑一个简单的 D3D 测试程序,看能不能出画面。如果 DXVK 能跑通,说明 Wine 和 FEX-Emu 的链路没问题,再换 DXMT 到 iOS 或 macOS 上验证。
DXMT 的接入类似,把编译好的 d3d11.dll、dxgi.dll 替换进去,设置对应的环境变量。DXMT 的日志级别可以调,遇到画面黑屏、花屏、崩溃时,先看日志里有没有着色器编译失败、资源创建失败的错误。Metal 的验证层在调试时很有用,能抓到 API 误用。
4.5 性能基准与瓶颈定位
跑通之后,用time和perf做基准。重点看三个指标:启动时间、稳态帧率(如果是游戏)、CPU 占用分布。启动时间主要花在 FEX-Emu 的翻译缓存构建和 Wine 的 prefix 初始化上;稳态帧率取决于 DXMT 的翻译效率和 Metal 的渲染效率;CPU 占用如果集中在 FEX-Emu 的翻译线程,说明翻译开销大,可以尝试调大缓存、开多块编译。
瓶颈定位的常用手段是分层关闭:先关图形层,跑纯计算程序,看 FEX-Emu 加 Wine 的开销;再关 FEX-Emu,跑原生 ARM64 程序,看 Wine 本身的开销。这样能快速判断性能损失主要来自哪一层。
5. 常见问题与排查技巧实录
5.1 启动即崩溃:先查指令翻译和库加载
最常见的崩溃是 FEX-Emu 翻译到不支持的指令,或者 Wine 加载不到某个 DLL。排查顺序是:先看 FEX-Emu 的日志,有没有 “unhandled instruction” 之类的报错;再看 Wine 的+loaddll日志,有没有 “failed to load” 的库。如果是指令问题,可能是目标程序用了较新的 x86-64 扩展指令(如 AVX-512),FEX-Emu 对某些扩展的支持不完整,需要换程序版本或等 FEX-Emu 更新。如果是库问题,检查根文件系统里有没有对应的 so 文件,路径映射对不对。
5.2 画面黑屏或花屏:图形层的问题定位
黑屏通常是 DXMT 或 DXVK 没有正确接管 D3D 调用,或者着色器编译失败。先确认 DLL 覆盖设置生效,Wine 日志里应该显示用的是 DXMT 的 d3d11 而不是 Wine 自带的。再看 DXMT 日志,着色器编译失败会有明确的错误信息,常见原因是 HLSL 特性不支持、Metal 版本不够、或者着色器缓存损坏。花屏往往是资源格式不匹配或同步问题,可以试着关掉某些优化选项,用更保守的翻译路径。
5.3 性能突然下降:缓存和内存的锅
跑着跑着变卡,常见原因是翻译缓存满了被清空,或者内存触顶触发回收。FEX-Emu 的缓存大小可以配,调大能减少重翻译,但占内存。iOS 上内存更紧张,需要更激进的回收策略。另一个原因是着色器缓存失效,每次遇到新场景都重编着色器,表现就是进新区域卡一下。可以预编译着色器、扩大缓存、或者降低着色器复杂度。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查手段 | 解决方向 |
|---|---|---|---|
| 启动即崩溃 | 不支持指令 / 库缺失 | FEX 日志、Wine +loaddll | 换程序版本、补库、调 FEX 配置 |
| 黑屏 | D3D 未接管 / 着色器失败 | DLL 覆盖、DXMT 日志 | 检查覆盖设置、修着色器 |
| 花屏 | 资源格式 / 同步问题 | Metal 验证层 | 关优化、换翻译路径 |
| 变卡 | 缓存失效 / 内存回收 | 缓存命中率、内存占用 | 调缓存大小、优化回收 |
| 音频异常 | 音频后端不匹配 | Wine 音频日志 | 换 AudioToolbox 实现 |
| 输入无响应 | UIKit 事件未接入 | 输入日志 | 补事件映射 |
5.5 几个踩过的坑
第一个坑是路径大小写。Windows 不区分大小写,Linux 和 iOS 区分,Wine 虽然做了大小写不敏感的文件系统模拟,但在 FEX-Emu 下性能开销很大,能改程序配置就改,别硬扛。第二个坑是时区。Wine 的时区处理依赖宿主,iOS 的时区数据库和 Windows 不完全一致,某些程序会因此出错,可以在 prefix 里固定时区。第三个坑是字体。Wine 默认字体在 iOS 上可能缺失,中文程序容易乱码,需要把字体文件放进 prefix 的 Fonts 目录并注册。
6. 这套方案还能怎么扩展
把 Madeira 这套三层架构拆明白之后,其实可以迁移到很多场景。比如在 ARM64 的 Linux 服务器上跑 x86-64 的 Windows 服务程序,用 FEX-Emu 加 Wine 就能省掉一个 Windows 虚拟机;再比如在 macOS 上跑 Windows 游戏,DXMT 换成 Metal 后端,性能比虚拟机好。甚至可以把 FEX-Emu 单独拿出来,在 ARM 设备上跑 x86-64 的 Linux 程序,不涉及 Wine 和图形层,链路更短,验证更快。
我个人在实际折腾这类兼容层时的体会是:不要一上来就追求“跑 3A 游戏”,先从记事本、计算器这种小程序开始,把链路跑通,再逐步加复杂度。每加一层,先确认这一层单独能工作,再叠下一层。遇到问题先分层隔离,别在三层混在一起的时候瞎猜。这套方法不光适用于 Madeira,任何跨平台兼容项目都能用。