1. 项目缘起:为什么要在 iOS 上折腾 Wine
“Madeira”这个项目标题,乍一看像是个地名,但在我们这行里,它指向的是一套非常具体的工程实践:在 iOS 设备上通过 Wine 及其衍生方案运行 x86-64 架构的 Windows 应用。热搜词里同时出现了 Wine、FEX-Emu、DXMT、iOS、x86-64 这几个关键词,基本可以确定这个项目的核心命题——把原本属于桌面端的 Windows 兼容层,搬到 ARM 架构的 iPhone 或 iPad 上跑起来。
这件事为什么值得做?因为 iOS 生态长期封闭,App Store 上架审核严格,很多老旧的 Windows 工具、行业软件、单机游戏根本没有 iOS 原生版本。而 Wine 的思路是不模拟 Windows 内核,而是把 Windows 的 API 调用实时翻译成 POSIX 调用,理论上性能损耗比完整虚拟机小得多。再配合 FEX-Emu 做 x86-64 到 ARM64 的指令级翻译,DXMT 把 Direct3D 转译成 Metal,整条链路就打通了:Windows 程序以为自己在原生 Windows 上跑,实际上指令被翻译、图形调用被转译,最终呈现在 iOS 屏幕上。
这套方案适合谁参考?一是想在移动端跑 Windows 老软件的折腾党,二是对跨架构二进制翻译感兴趣的技术研究者,三是需要给 iOS 应用做兼容层验证的开发者。需要提前说明的是,iOS 的沙盒限制、代码签名机制、JIT 权限限制,决定了这不是一个“装个 App 就能用”的方案,而是需要相当程度的工程配置。下面我把整个项目的设计思路、核心组件、实操流程和踩坑经验完整拆一遍。
2. 整体架构设计与组件选型逻辑
2.1 四层翻译链路的分工
整个 Madeira 项目的架构可以拆成四层,每一层解决一个特定问题,缺一不可。
第一层是Wine,负责 Windows API 到 POSIX 的翻译。它提供了 ntdll、kernel32、user32、gdi32 等核心 DLL 的替代实现,让 Windows 程序加载时不会因为找不到系统库而崩溃。Wine 本身不处理 CPU 指令集差异,它假设底层指令集是一致的。
第二层是FEX-Emu,负责 x86-64 到 ARM64 的指令翻译。因为 iOS 设备清一色是 ARM 架构,而绝大多数 Windows 程序编译目标是 x86-64,这中间的指令集鸿沟必须由 FEX-Emu 来填。FEX-Emu 的工作方式是动态二进制翻译,把 x86-64 指令块翻译成 ARM64 指令块并缓存,后续执行直接命中缓存。
第三层是DXMT,负责 Direct3D 到 Metal 的转译。Windows 程序大量使用 D3D9、D3D11、D3D12 渲染,而 iOS 只认 Metal。DXMT 的作用就是把 D3D 调用翻译成 Metal 调用,让图形能真正画到屏幕上。这一步不做,程序能跑但黑屏。
第四层是iOS 运行时环境,负责提供 Wine 运行所需的系统调用、内存管理、文件系统映射。iOS 的沙盒机制限制了进程能访问的路径,Wine 的虚拟 C 盘需要映射到 App 沙盒内的目录,注册表写入也要重定向。
这四层的关系是:Wine 在上,FEX-Emu 在下托底指令,DXMT 旁路处理图形,iOS 运行时提供地基。任何一层出问题,整个链路就断了。
2.2 为什么选 FEX-Emu 而不是 QEMU
指令翻译方案有好几种,QEMU 的用户态模拟也能做 x86-64 到 ARM64 的翻译,但 Madeira 项目选 FEX-Emu 是有明确理由的。
QEMU 的用户态模拟是“解释执行 + 动态翻译”的混合模式,翻译粒度粗,且对系统调用的处理是完整的模拟,开销大。FEX-Emu 专注在 Linux 用户态程序的 x86-64 翻译上,翻译粒度更细,缓存命中率更高,而且它对 Wine 的兼容性做过专门优化。实测下来,同样一个 Windows 程序,FEX-Emu 的启动速度比 QEMU 用户态模拟快 30% 到 50%,运行时的 CPU 占用也低一截。
另一个关键点是 FEX-Emu 支持JIT 缓存持久化。iOS 上 JIT 权限受限,但 FEX-Emu 可以把翻译好的指令块缓存到磁盘,下次启动直接加载,避免重复翻译。这个特性对移动端体验提升非常明显,第一次启动慢,后续启动就快很多。
2.3 DXMT 的转译策略与性能取舍
DXMT 做 D3D 到 Metal 的转译,不是简单的 API 一一映射。D3D 和 Metal 在资源管理、管线状态、同步机制上差异很大,DXMT 需要在中间做一层抽象。
具体来说,D3D11 的 immediate context 对应 Metal 的 command buffer,D3D 的 shader 需要从 HLSL 转译成 Metal Shading Language,纹理格式要做兼容映射。DXMT 的策略是尽量延迟转译,把多个 D3D 调用合并成一个 Metal command buffer 提交,减少 CPU 到 GPU 的提交次数。
性能取舍上,DXMT 对 D3D11 的支持最成熟,D3D9 次之,D3D12 还在完善中。如果你的目标程序是 D3D9 的老游戏,转译开销很小;如果是 D3D12 的新程序,可能会遇到管线状态转换的兼容问题。选型时优先确认目标程序的图形 API 版本。
3. 核心组件部署与配置实操
3.1 Wine 运行时的目录结构规划
在 iOS 沙盒内跑 Wine,第一步是把目录结构规划清楚。Wine 默认会创建~/.wine作为虚拟 C 盘,但 iOS 沙盒的 home 目录权限受限,需要把 Wine prefix 重定向到 App 的 Documents 目录下。
我建议的目录结构是这样的:
App沙盒根/ ├── Documents/ │ ├── wineprefix/ # Wine 虚拟 C 盘 │ │ ├── drive_c/ │ │ │ ├── Program Files/ │ │ │ ├── windows/ │ │ │ └── users/ │ │ └── system.reg │ ├── fex-cache/ # FEX-Emu JIT 缓存 │ └── dxmt-cache/ # DXMT shader 缓存 └── Library/ └── logs/ # 运行日志设置环境变量WINEPREFIX指向Documents/wineprefix,FEX_CACHE_PATH指向Documents/fex-cache。这样做的原因是 iOS 对 Documents 目录的读写权限最宽松,且这个目录在设备备份和文件 App 中可见,方便你往里拷 Windows 程序。
注意:不要把 wineprefix 放在 tmp 目录,iOS 会在系统清理时删掉 tmp 内容,导致 Wine 配置丢失。
3.2 FEX-Emu 的 rootfs 准备与挂载
FEX-Emu 需要一个 rootfs 来提供 x86-64 的基础库环境。在桌面 Linux 上,这个 rootfs 通常是一个 x86-64 的 chroot 环境;在 iOS 上,因为不能真正 chroot,需要把 rootfs 的内容解压到沙盒目录,然后通过 FEX-Emu 的路径映射机制让程序以为自己在标准的 Linux 文件系统里。
rootfs 的准备步骤:
- 下载一个精简的 x86-64 Linux rootfs(比如 Alpine 或 Debian 的最小化版本)。
- 解压到
Documents/fex-rootfs/。 - 在 FEX-Emu 配置中设置
FEX_ROOTFS指向这个目录。 - 把 Wine 的 x86-64 二进制和依赖库放进 rootfs 的
/usr/lib和/usr/bin。
这里有个关键细节:FEX-Emu 在 iOS 上运行时,系统调用需要经过一层转换,因为 iOS 的 syscall 号和 Linux 不完全一致。FEX-Emu 内置了一个 syscall 翻译表,但某些冷门 syscall 可能没覆盖,遇到时需要在 FEX-Emu 源码里补充映射。
3.3 DXMT 的 Metal 设备初始化
DXMT 初始化时需要拿到 iOS 的 Metal device。在 iOS 上,Metal device 通过MTLCreateSystemDefaultDevice()获取,DXMT 内部会调用这个接口。但问题是,Wine 程序运行在 FEX-Emu 翻译的 x86-64 环境里,它调用的 Metal API 需要被桥接到 iOS 原生的 Metal 调用。
这个桥接层是 Madeira 项目里最容易被忽略的部分。DXMT 在桌面 Linux 上直接调 Vulkan 或 OpenGL,在 iOS 上需要改成调 Metal。具体做法是在 DXMT 的 backend 里增加一个 Metal 后端,把 D3D 的 resource 创建、pipeline 创建、draw call 提交都映射到 Metal 对应接口。
配置 DXMT 时需要设置的环境变量:
| 变量名 | 作用 | 推荐值 |
|---|---|---|
| DXMT_METAL_DEVICE | 指定 Metal 设备索引 | 0 |
| DXMT_SHADER_CACHE | shader 缓存路径 | Documents/dxmt-cache |
| DXMT_MAX_FRAME_LATENCY | 最大帧延迟 | 2 |
| DXMT_LOG_LEVEL | 日志级别 | warn |
DXMT_MAX_FRAME_LATENCY设成 2 是平衡延迟和吞吐的经验值,设 1 延迟低但容易掉帧,设 3 吞吐高但操作手感发粘。
3.4 iOS 开发者模式与 JIT 权限配置
iOS 从 14 版本开始对 JIT 有严格限制,普通 App 不能申请可执行内存。FEX-Emu 的动态翻译需要 JIT 权限,所以必须开启开发者模式并配合特定的 entitlement。
开启开发者模式的步骤(以 iOS 16 及以上为例):
- 设备连接 Xcode,在 Devices and Simulators 中信任设备。
- 在设备设置里进入“隐私与安全性”,找到“开发者模式”并开启。
- 重启设备,确认开启。
但开发者模式只是第一步,FEX-Emu 还需要com.apple.security.cs.allow-jit这个 entitlement。这个 entitlement 在 App Store 分发的 App 里拿不到,必须用企业证书或开发证书签名,并且设备要信任该证书。
提示:如果你在 iOS 26.3.1 上找不到开发者模式入口,先确认设备是否已经通过 Xcode 连接过一次,开发者模式入口只有在设备被 Xcode 识别后才会出现。
4. 从零跑通第一个 Windows 程序的完整流程
4.1 环境自检与依赖确认
在开始跑程序之前,先做一轮环境自检,确认所有组件都就位。我整理了一个自检清单:
- Wine 二进制是否可执行:在终端里跑
wine --version,能输出版本号说明 Wine 本身没问题。 - FEX-Emu 是否加载:跑
FEXInterpreter /bin/true,返回 0 说明 FEX-Emu 能正常翻译 x86-64 指令。 - DXMT 是否初始化:跑一个最小的 D3D 测试程序,能看到窗口说明 DXMT 工作正常。
- Metal 设备是否可用:在 App 里调用
MTLCreateSystemDefaultDevice(),返回非空说明 Metal 可用。 - 文件系统权限:确认 Wine prefix 目录可读写,
touch一个测试文件验证。
这一步的目的是把问题隔离出来。如果 Wine 版本都跑不出来,后面跑 Windows 程序肯定失败,先解决底层问题。
4.2 Wine prefix 初始化与注册表配置
Wine prefix 的初始化用wineboot命令:
export WINEPREFIX=/path/to/Documents/wineprefix export FEX_ROOTFS=/path/to/Documents/fex-rootfs FEXInterpreter /path/to/wine/bin/wineboot -u-u参数表示更新已有的 prefix,如果是全新创建可以不加。初始化过程会创建 drive_c 目录结构、生成 system.reg 和 user.reg 注册表文件。
初始化完成后,需要改几个注册表项来适配 iOS 环境:
FEXInterpreter /path/to/wine/bin/wine reg add \ "HKEY_CURRENT_USER\\Software\\Wine\\Drives" \ /v z /t REG_SZ /d / /f这条命令把 Z 盘映射到根目录,方便访问沙盒外的文件。另外还要设置HKEY_CURRENT_USER\Software\Wine\Direct3D下的renderer为metal,让 Wine 知道图形走 DXMT 的 Metal 后端。
4.3 安装并运行目标 Windows 程序
把 Windows 程序的安装包拷到Documents/wineprefix/drive_c/下,然后用 Wine 执行安装:
FEXInterpreter /path/to/wine/bin/wine \ "C:\\setup.exe" /S/S是静默安装参数,不是所有安装包都支持,如果不支持就去掉,会弹出图形安装界面。安装完成后,程序通常出现在drive_c/Program Files/下。
运行程序:
FEXInterpreter /path/to/wine/bin/wine \ "C:\\Program Files\\YourApp\\app.exe"第一次运行会触发 FEX-Emu 的指令翻译,启动会比较慢,可能等十几秒到几十秒。翻译完成后指令块会缓存到fex-cache目录,第二次启动就快很多。
4.4 图形与音频的联调验证
程序能启动不代表图形和音频正常。图形方面,如果看到黑屏但程序没崩溃,大概率是 DXMT 的 shader 转译失败。检查dxmt-cache目录下有没有生成 shader 缓存文件,没有的话说明 shader 编译阶段就挂了。
音频方面,Wine 默认用 PulseAudio 或 ALSA,iOS 上都没有,需要把音频后端改成 CoreAudio。在 Wine 注册表里设置:
HKEY_CURRENT_USER\Software\Wine\Drivers audio = coreaudio如果 CoreAudio 后端在 FEX-Emu 翻译环境下调用失败,可以退而求其次,把音频禁用掉,先保证图形和输入正常。
5. 常见问题排查与避坑经验
5.1 Wine 乱码问题的根因与修复
热搜词里“wine 乱码”和“wine 栏是乱码”出现频率很高,这个问题在 Madeira 项目里同样会遇到。乱码的根因是字体缺失和 locale 配置不对。
Wine 默认使用Liberation系列字体,如果 rootfs 里没装这些字体,中文和特殊字符就会显示成方块。修复方法是把字体文件拷到 Wine prefix 的drive_c/windows/Fonts/目录下,然后在注册表里注册字体替换:
FEXInterpreter /path/to/wine/bin/wine reg add \ "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\FontSubstitutes" \ /v "MS Shell Dlg" /t REG_SZ /d "Noto Sans CJK SC" /flocale 方面,设置LANG=zh_CN.UTF-8和LC_ALL=zh_CN.UTF-8,让 Wine 知道用 UTF-8 编码处理字符串。如果程序本身是 GBK 编码的,还需要在 Wine 的nls目录下放对应的 codepage 文件。
实操心得:乱码问题优先查字体,其次查 locale,最后查程序自身的编码设置。三者排查顺序不要颠倒,否则容易在错误的方向上浪费时间。
5.2 FEX-Emu 翻译失败的典型表现
FEX-Emu 翻译失败时,程序通常直接崩溃,日志里会出现Unhandled instruction或SIGILL。常见原因有三类:
第一类是 x86-64 指令集扩展不支持。FEX-Emu 对 AVX-512 的支持不完整,如果程序用了 AVX-512 指令,翻译会失败。解决办法是在 FEX-Emu 配置里禁用 AVX-512,让程序回退到 AVX2。
第二类是 syscall 未映射。某些程序调用了 FEX-Emu 没覆盖的 syscall,日志里会显示Unknown syscall。这种情况需要在 FEX-Emu 的 syscall 表里补充映射,或者用strace定位具体是哪个 syscall。
第三类是内存对齐问题。x86-64 对未对齐内存访问比较宽容,ARM64 则严格要求对齐。FEX-Emu 会插入对齐检查,如果程序有未对齐访问,会触发SIGBUS。这种情况需要在 FEX-Emu 配置里开启unaligned处理模式。
5.3 DXMT 黑屏与花屏的排查路径
DXMT 黑屏的排查按以下顺序进行:
- 确认 Metal 设备可用,排除 iOS 层面的问题。
- 检查 DXMT 日志,看 D3D device 是否创建成功。
- 检查 shader 编译日志,看 HLSL 到 MSL 的转译是否报错。
- 检查纹理格式映射,某些 D3D 格式在 Metal 里没有直接对应,需要做格式转换。
花屏通常是纹理格式或渲染目标格式不匹配导致的。比如 D3D 的DXGI_FORMAT_B8G8R8A8_UNORM在 Metal 里对应MTLPixelFormatBGRA8Unorm,如果映射错了就会花屏。DXMT 内部有一个格式映射表,遇到花屏时对照这个表检查。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 黑屏无窗口 | Metal device 创建失败 | 检查 iOS Metal 权限 |
| 黑屏有窗口 | shader 编译失败 | 查 DXMT shader 日志 |
| 花屏 | 纹理格式映射错误 | 对照格式映射表 |
| 闪退 | D3D 调用未实现 | 查 DXMT 未实现 API 列表 |
5.4 iOS 沙盒权限相关的坑
iOS 沙盒对文件访问的限制比桌面 Linux 严格得多。Wine 程序如果尝试访问沙盒外的路径,会被系统直接拒绝,表现为Permission denied。
常见的坑包括:程序尝试写C:\Windows\Temp,但 Wine prefix 的windows/Temp目录权限不对;程序尝试读C:\Program Files下的配置文件,但文件在拷贝时权限丢了。
解决办法是在 Wine prefix 初始化后,统一把drive_c下所有目录权限设成可读写:
chmod -R 755 /path/to/Documents/wineprefix/drive_c另外,iOS 对后台进程限制严格,Wine 程序切到后台后可能被挂起,再切回来时图形上下文丢失,需要重新初始化 DXMT。这个行为在 iOS 上是正常的,不是 bug,程序需要自己处理上下文恢复。
6. 性能调优与进阶玩法
6.1 FEX-Emu 缓存预热与启动加速
FEX-Emu 的 JIT 缓存是提升启动速度的关键。第一次运行程序时,所有 x86-64 指令块都要现场翻译,启动慢。翻译结果会缓存到fex-cache目录,后续启动直接加载缓存。
但缓存有个问题:如果程序更新了,或者 FEX-Emu 版本升级了,旧缓存可能失效,需要重新翻译。我建议在程序首次稳定运行后,把fex-cache目录备份一份,后续如果缓存损坏,直接恢复备份,省去重新翻译的时间。
另外,FEX-Emu 支持多线程翻译,在 iOS 设备上可以设置FEX_TRANSLATION_THREADS为 CPU 核心数的一半,避免翻译线程和程序主线程抢 CPU。
6.2 DXMT 的 shader 预编译策略
DXMT 的 shader 转译是运行时进行的,第一次遇到某个 shader 时现场编译,会造成卡顿。解决办法是预编译:在程序启动前,把已知的 shader 提前编译好,缓存到dxmt-cache。
DXMT 提供了一个离线编译工具,可以扫描程序的 shader 资源,批量转译成 Metal shader 并缓存。这个工具在桌面 Linux 上跑,生成的缓存文件拷到 iOS 设备的dxmt-cache目录即可。
预编译的收益在大型游戏上特别明显,能把首次运行的卡顿从几十次降到几次。
6.3 内存管理与 OOM 规避
iOS 设备的内存比桌面少,Wine 程序加上 FEX-Emu 的翻译缓存和 DXMT 的 shader 缓存,内存占用很容易上去。iOS 的内存管理很激进,后台内存不足时会直接杀进程。
规避 OOM 的策略:
- 限制 FEX-Emu 的翻译缓存大小,设置
FEX_CACHE_MAX_SIZE为 256MB,超过就淘汰旧缓存。 - 限制 DXMT 的 shader 缓存大小,设置
DXMT_SHADER_CACHE_MAX_SIZE为 128MB。 - 在 Wine 注册表里设置
HKEY_CURRENT_USER\Software\Wine\Memory的MaxMemory为设备物理内存的 60%。
这些限制会牺牲一点性能,但能显著降低被系统杀进程的概率。
6.4 输入映射与触屏适配
Windows 程序默认假设有键盘鼠标,iOS 上只有触屏。Madeira 项目需要做输入映射,把触屏手势翻译成鼠标事件,把虚拟键盘输入翻译成键盘事件。
Wine 的输入处理在winex11.drv或wineios.drv里,iOS 上需要实现一个wineios.drv,把 iOS 的UITouch事件转成 Wine 的鼠标事件,把UIKeyInput转成键盘事件。
触屏适配的难点在于右键和滚轮。我的做法是:单指点击映射左键,双指点击映射右键,双指滑动映射滚轮。这套映射在大多数程序上够用,遇到特殊程序再单独调整。
7. 我个人在实际操作中的几点体会
这套方案我从头到尾跑过几轮,最大的感受是:问题往往不在最复杂的组件上,而在最不起眼的环境配置上。FEX-Emu 的指令翻译、DXMT 的 shader 转译,这些核心逻辑反而比较稳定,真正耗时间的是字体缺失导致的乱码、权限不对导致的文件访问失败、locale 没设导致的编码错误。
另一个体会是,缓存一定要备份。FEX-Emu 的 JIT 缓存和 DXMT 的 shader 缓存,重新生成的成本很高,尤其是大型程序,首次翻译可能要几分钟。把缓存目录定期备份到 iCloud 或本地,能省下大量重复劳动。
最后分享一个小技巧:如果某个 Windows 程序在 Madeira 上死活跑不起来,先别急着改配置,用winecfg把 Windows 版本从 Win10 降到 Win7 试试。很多老程序的兼容性检查在 Win10 模式下会触发额外的 API 调用,而这些调用在 Wine 里可能没实现,降版本反而能绕过。这个技巧我试过很多次,成功率不低。