1. 项目缘起:为什么要在 iOS 上折腾 Wine
第一次看到 "Madeira" 这个代号,是在一个折腾跨平台兼容层的讨论串里。简单说,Madeira 是一个把 Wine 的 Windows 兼容能力搬到 iOS 设备上的实验性项目,它让 iPhone、iPad 这类原本封闭的 ARM 设备,有机会跑起 x86-64 架构的 Windows 程序。听起来有点天方夜谭,但背后的技术链条其实很清晰:Wine 负责把 Windows 的 API 调用翻译成 POSIX 调用,FEX-Emu 负责把 x86-64 指令动态翻译成 ARM64 指令,DXMT 负责把 Direct3D 调用翻译成 Metal,三者叠在一起,才凑齐了在 iOS 上运行 Windows 程序的完整拼图。
我之所以对这个项目感兴趣,是因为它踩中了几个很实际的需求点。第一,iOS 生态里长期缺少原生的 Windows 程序运行方案,很多老旧的行业软件、单机游戏、工具类 exe 根本没有 iOS 版本,用户要么换设备,要么放弃。第二,苹果从 M 系列芯片开始全面转向 ARM64,硬件性能早就不是瓶颈,缺的只是软件层的翻译和兼容。第三,Wine 在 Linux 和 macOS 上已经相当成熟,把它移植到 iOS 上,理论上只是解决沙盒限制、JIT 权限、图形接口映射这几个硬骨头。
这个项目适合谁来参考?如果你是对跨平台兼容层感兴趣的技术爱好者,想搞清楚 Wine、FEX-Emu、DXMT 这三者是怎么协作的,那这篇内容会对你有帮助。如果你是 iOS 开发者,想了解在沙盒环境下怎么申请 JIT 权限、怎么处理动态代码生成,也能从中找到一些思路。如果你只是普通用户,想在自己的 iPhone 上跑某个 Windows 小工具,那我会在实操部分把能踩的坑都标出来,让你少走弯路。
需要提前说明的是,Madeira 目前还处于比较早期的阶段,不是那种装完就能用的成熟产品。它更像是一个技术验证平台,很多环节需要手动配置,稳定性也取决于具体运行的程序。我下面写的内容,一部分来自公开的技术资料,一部分来自我自己在类似项目上的实操经验,还有一部分是基于 Wine 和 FEX-Emu 在其它平台上的表现做的合理推断。凡是推断的部分,我都会明确标注出来,避免误导。
2. 核心技术栈拆解:Wine、FEX-Emu、DXMT 各自扮演什么角色
2.1 Wine:把 Windows API 翻译成 POSIX 调用
Wine 的全称是 "Wine Is Not an Emulator",这句话本身就是它的核心定位。它不做 CPU 指令级的模拟,而是实现了一套 Windows API 的兼容层,把 Windows 程序调用的 kernel32.dll、user32.dll、gdi32.dll 这些系统库,翻译成宿主系统的 POSIX 调用。比如 Windows 程序调用 CreateFile,Wine 会把它翻译成宿主系统的 open 调用;调用 MessageBox,Wine 会把它翻译成宿主系统的窗口绘制逻辑。
在 iOS 上跑 Wine,最大的障碍不是 API 翻译本身,而是 iOS 的沙盒限制。Wine 需要访问文件系统、需要创建进程、需要加载动态库,这些在 iOS 的沙盒里都受到严格限制。Madeira 的做法,我推测是通过越狱环境或者特定的开发者权限,绕过部分沙盒限制,让 Wine 能够正常初始化。如果没有越狱,那就只能依赖苹果提供的 JIT 权限和特定的 entitlement,这也是为什么很多类似项目都要求设备处于开发者模式。
Wine 在 iOS 上的另一个难点是图形接口。Windows 程序通常通过 GDI 或者 Direct3D 来绘制界面,Wine 需要把这些调用映射到 iOS 的图形栈上。GDI 部分相对简单,可以用 Core Graphics 来模拟;Direct3D 部分就复杂得多,需要 DXMT 这样的中间层来做转换。
2.2 FEX-Emu:x86-64 到 ARM64 的动态翻译
FEX-Emu 是一个 x86-64 到 ARM64 的动态二进制翻译器,它的作用是把 Windows 程序的 x86-64 指令,实时翻译成 iOS 设备能执行的 ARM64 指令。和传统的模拟器不同,FEX-Emu 不做完整的 CPU 模拟,而是采用 JIT 编译的方式,把热点代码块翻译成 ARM64 指令后缓存起来,后续执行直接走缓存,性能损耗相对可控。
FEX-Emu 在 iOS 上运行,最大的挑战是 JIT 权限。iOS 默认不允许应用在运行时生成可执行代码,这是安全策略的一部分。要绕过这个限制,通常有两种途径:一是设备越狱,直接关闭代码签名校验;二是利用苹果给开发者提供的 JIT entitlement,在特定条件下申请可写可执行内存。Madeira 具体走的是哪条路,公开资料里没有明确说明,但从它要求 iOS 开发者模式这一点来看,应该是走了第二条路。
FEX-Emu 的翻译质量直接影响程序的运行效率。我实测过类似方案在其它 ARM 设备上的表现,对于计算密集型的程序,翻译后的性能大概能到原生 x86 的 60% 到 80%,具体取决于程序的指令特征。如果程序大量使用 SSE、AVX 这类向量指令,翻译开销会更大;如果只是普通的整数运算和分支跳转,性能损失相对较小。
2.3 DXMT:Direct3D 到 Metal 的桥梁
DXMT 是一个把 Direct3D 调用翻译成 Metal 调用的中间层,它的定位类似于 DXVK 在 Linux 上的角色。Windows 程序通过 Direct3D 9、10、11 来渲染画面,DXMT 把这些 API 调用转换成 Metal 的 API 调用,让 iOS 设备的 GPU 能够执行渲染任务。
DXMT 的翻译质量决定了游戏的画面表现和帧率。Direct3D 和 Metal 在渲染管线、资源管理、着色器模型上都有差异,DXMT 需要做大量的适配工作。比如 Direct3D 的着色器是 HLSL 编写的,Metal 的着色器是 MSL 编写的,DXMT 需要在运行时把 HLSL 编译成 MSL,这个编译过程会带来额外的开销。再比如 Direct3D 的资源绑定模型和 Metal 不同,DXMT 需要做资源状态的跟踪和转换,这部分如果做得不够高效,就会成为性能瓶颈。
在 iOS 上,DXMT 还面临一个特殊问题:Metal 的驱动层对某些渲染特性支持有限,比如几何着色器、细分曲面这些在桌面 GPU 上很常见的功能,在 iOS 的 GPU 上可能没有直接对应。DXMT 需要用计算着色器或者其它方式来模拟这些功能,这会进一步增加翻译的复杂度和性能开销。
2.4 三者的协作关系
把这三个组件串起来看,整个流程是这样的:Windows 程序启动后,它的 x86-64 指令由 FEX-Emu 翻译成 ARM64 指令执行;程序调用的 Windows API 由 Wine 翻译成 POSIX 调用;程序发起的 Direct3D 渲染请求由 DXMT 翻译成 Metal 调用。三者各司其职,缺一不可。
这个架构的复杂度在于,三个组件之间需要紧密配合。比如 Wine 在创建窗口时,需要把窗口句柄传递给 DXMT,让 DXMT 知道渲染目标是什么;FEX-Emu 在执行翻译后的代码时,需要保证内存模型和 Wine 的假设一致,否则会出现难以排查的 bug。Madeira 作为整合方,需要处理这些组件之间的接口适配和状态同步,这也是它比单独跑 Wine 或单独跑 FEX-Emu 要复杂得多的原因。
3. iOS 环境下的实操部署:从零到跑通第一个程序
3.1 设备与系统版本的选择
Madeira 对设备的要求比较苛刻。首先,设备必须是 ARM64 架构,也就是 iPhone 5s 之后的机型,这一点大部分现代 iOS 设备都满足。其次,系统版本不能太低也不能太高,太低缺少必要的 JIT 支持,太高可能封堵了某些关键接口。根据社区反馈,iOS 15 到 iOS 17 之间的版本兼容性相对较好,iOS 18 之后的部分版本对 JIT 权限的限制更严格,需要额外的配置才能跑通。
我建议优先选择 A12 及以后的芯片设备,比如 iPhone XS、iPhone 11 系列、iPad Pro 2018 及以后的型号。这些设备的 CPU 性能足够支撑 FEX-Emu 的翻译开销,GPU 也支持较新的 Metal 特性,DXMT 的兼容性会更好。内存方面,建议至少 4GB,跑一些稍大的 Windows 程序时,内存不足会导致频繁的交换和卡顿。
系统版本的选择上,如果你手头有多个设备,可以优先尝试 iOS 16 或 iOS 17 的版本。这两个大版本对开发者模式的限制相对宽松,申请 JIT 权限的成功率更高。iOS 18 之后,苹果收紧了部分接口,需要更复杂的配置才能达到同样的效果。
3.2 开发者模式与 JIT 权限的申请
iOS 从 16 版本开始,引入了开发者模式的概念。开启开发者模式后,设备会允许安装未经 App Store 审核的应用,也会放宽部分运行时限制。Madeira 需要这个模式来加载自定义的二进制文件和申请 JIT 权限。
开启开发者模式的步骤大致如下:首先在设备上安装 Xcode 或者 Apple Configurator,通过数据线连接设备;然后在设备的设置里找到隐私与安全性,进入开发者模式选项,按照提示重启设备;重启后再次进入设置,确认开启开发者模式。这个过程需要设备连接电脑,并且电脑上安装了最新版本的 Xcode。
JIT 权限的申请比开发者模式更复杂。iOS 默认不允许应用在运行时生成可执行代码,要绕过这个限制,通常需要在应用的 entitlement 里声明 com.apple.security.cs.allow-jit 权限,并且通过特定的 API 向系统申请可写可执行内存。Madeira 的具体实现方式,我推测是通过一个辅助进程或者特定的签名配置来完成的。如果你是自己编译 Madeira,需要在 Xcode 项目里配置对应的 entitlement,并且使用开发者证书签名。
注意:JIT 权限的申请在不同 iOS 版本上的表现差异很大。iOS 16 上相对宽松,iOS 17 开始收紧,iOS 18 之后需要额外的配置。如果你在某个版本上反复失败,不妨换一个系统版本试试,不要在一个版本上死磕。
3.3 Madeira 的获取与安装
Madeira 目前没有上架 App Store,获取途径主要是社区分享的 IPA 包或者自行编译。如果你选择自行编译,需要准备一台 macOS 设备,安装 Xcode 和相关的命令行工具,然后从代码仓库拉取源码,配置好依赖后编译打包。
编译过程中会遇到几个常见的坑。第一是依赖库的版本问题,Wine、FEX-Emu、DXMT 都是活跃开发的项目,不同版本的接口可能有变化,需要按照 Madeira 的文档锁定对应的版本。第二是签名问题,iOS 应用必须签名才能安装,如果你没有开发者账号,可以用免费的个人签名,但免费签名只有 7 天有效期,过期后需要重新签名。第三是架构问题,编译时需要确保目标架构是 ARM64,并且开启了必要的优化选项,否则性能会打折扣。
如果你不想自己编译,可以找社区分享的 IPA 包。安装 IPA 包需要用到 sideload 工具,比如 AltStore、Sideloadly 这些。安装过程和普通 IPA 一样,连接设备后选择 IPA 文件,输入 Apple ID 进行签名,然后等待安装完成。安装完成后,需要在设备的设置里信任对应的开发者证书,才能正常打开应用。
3.4 首次运行与基础配置
Madeira 首次启动时,会进行一系列初始化操作,包括创建 Wine 的前缀目录、解压必要的运行库、初始化 FEX-Emu 的翻译缓存等。这个过程可能需要几分钟,取决于设备的性能和存储速度。初始化完成后,你会看到一个类似文件管理器的界面,可以在这里导入 Windows 程序、配置运行参数、查看日志输出。
导入 Windows 程序的方式有几种。最简单的是通过 iTunes 文件共享或者 iOS 的文件应用,把 exe 文件拷贝到 Madeira 的文档目录下。然后在 Madeira 的界面里选择这个 exe,点击运行。Madeira 会自动调用 Wine 来加载这个程序,FEX-Emu 会在后台翻译指令,DXMT 会处理渲染请求。
首次运行某个程序时,可能会遇到缺少 DLL 的情况。Wine 自带了一部分 Windows 系统 DLL 的实现,但有些程序依赖的 DLL 不在其中,需要手动补充。你可以把缺失的 DLL 放到 Wine 前缀的 system32 目录下,或者通过 winetricks 这样的工具来安装。Madeira 是否集成了 winetricks,公开资料里没有明确说明,但按照 Wine 的惯例,手动补充 DLL 是常见的做法。
3.5 性能调优的几个关键参数
Madeira 跑起来之后,性能调优是绕不开的话题。我整理了几个影响最大的参数,你可以根据自己的设备情况调整。
| 参数 | 作用 | 建议值 | 说明 |
|---|---|---|---|
| FEX 翻译缓存大小 | 决定翻译后的代码能缓存多少 | 256MB - 512MB | 缓存越大,重复执行的代码命中率越高,但占用内存也越多 |
| Wine 虚拟内存上限 | 限制 Wine 进程能使用的虚拟内存 | 2GB - 4GB | 设置太小会导致大程序无法启动,设置太大会挤占系统内存 |
| DXMT 着色器缓存 | 缓存编译后的 Metal 着色器 | 开启 | 首次编译着色器较慢,缓存后后续运行会快很多 |
| 线程数 | FEX-Emu 使用的翻译线程数 | 2 - 4 | 线程太多会增加调度开销,太少会影响翻译速度 |
| 分辨率缩放 | 渲染分辨率与显示分辨率的比例 | 0.5 - 1.0 | 降低渲染分辨率可以显著提升帧率,但画面会变模糊 |
这些参数的具体调整方式,取决于 Madeira 的界面设计。有些参数可以在设置界面里直接修改,有些可能需要编辑配置文件。我建议先从默认值开始,跑一个具体的程序,观察帧率和 CPU 占用,然后有针对性地调整。比如帧率低但 CPU 占用不高,可能是 GPU 瓶颈,可以尝试降低分辨率缩放;如果 CPU 占用很高但帧率上不去,可能是翻译开销太大,可以尝试增大翻译缓存。
4. 常见问题与排查技巧实录
4.1 Wine 乱码问题的根源与解决
Wine 乱码是跨平台兼容层里的经典问题,在 Madeira 上同样会出现。乱码的表现形式有好几种:有的是菜单文字变成方块,有的是对话框里的中文显示为问号,有的是整个界面都是乱码。不同的表现形式,根源不同,解决方法也不同。
菜单文字变成方块,通常是字体缺失导致的。Wine 默认使用的字体不一定包含中文字形,当程序请求显示中文时,Wine 找不到对应的字形,就会用方块代替。解决方法是把中文字体安装到 Wine 的字体目录下,比如把 simsun.ttc、msyh.ttf 这些字体拷贝到 Wine 前缀的 drive_c/windows/Fonts 目录下,然后在 Wine 的注册表里配置字体替换规则。
对话框里的中文显示为问号,通常是字符编码问题。Windows 程序可能使用 GBK 或者 GB2312 编码来存储中文字符串,而 Wine 默认使用 UTF-8 编码来解析,两者不匹配就会出现问号。解决方法是在 Wine 的配置里设置正确的 locale,比如设置 LANG=zh_CN.GBK,或者在程序的启动参数里指定编码。
整个界面都是乱码,可能是字体配置完全错误,也可能是程序的资源文件没有正确加载。这种情况下,建议先检查 Wine 的字体配置,确认中文字体已经正确安装和注册。如果字体没问题,再检查程序的资源文件是否完整,有些程序依赖外部的语言包或者资源 DLL,缺失这些文件也会导致乱码。
实操心得:Wine 乱码问题,十有八九是字体问题。我习惯在初始化 Wine 前缀后,第一件事就是把常用的中文字体拷贝进去,然后在注册表里配置好字体替换。这样后续跑大部分中文程序都不会出现乱码。如果遇到个别程序仍然乱码,再针对性地排查编码和资源文件。
4.2 程序启动失败或闪退的排查思路
程序启动失败或闪退,是 Madeira 上另一个高频问题。排查这类问题,我通常按照从外到内的顺序,逐步缩小范围。
第一步,检查程序本身是否完整。有些 exe 文件依赖同目录下的 DLL 或者资源文件,如果只拷贝了 exe 而遗漏了依赖,程序启动时就会失败。解决方法是把整个程序目录都拷贝过去,保持目录结构不变。
第二步,检查 Wine 的日志输出。Madeira 应该提供了日志查看功能,你可以在这里看到 Wine 加载程序时的详细输出。常见的错误包括:缺少 DLL、无法创建窗口、无法初始化图形设备等。根据日志里的错误信息,可以快速定位问题所在。
第三步,检查 FEX-Emu 的翻译日志。如果程序在启动阶段就崩溃,可能是 FEX-Emu 在翻译某些指令时出了问题。FEX-Emu 的日志会显示它翻译了哪些代码块,哪些指令导致了异常。如果发现某个指令反复出错,可能是 FEX-Emu 对该指令的支持不完善,需要等待更新或者寻找替代方案。
第四步,检查 DXMT 的渲染日志。如果程序能启动但画面异常或者崩溃,可能是 DXMT 在翻译 Direct3D 调用时出了问题。DXMT 的日志会显示它处理了哪些渲染调用,哪些调用失败了。常见的错误包括:不支持的着色器模型、不支持的纹理格式、不支持的渲染状态等。
4.3 性能卡顿的优化方向
性能卡顿是 Madeira 上最让人头疼的问题,因为它涉及的因素很多,优化起来需要耐心。我整理了一个排查表,你可以按照这个顺序逐一检查。
| 卡顿表现 | 可能原因 | 排查方法 | 优化方向 |
|---|---|---|---|
| 启动阶段卡顿 | 翻译缓存未建立 | 观察首次启动和二次启动的差异 | 增大翻译缓存,让二次启动走缓存 |
| 界面操作卡顿 | CPU 翻译开销大 | 查看 CPU 占用率 | 减少翻译线程数,降低调度开销 |
| 游戏帧率低 | GPU 渲染瓶颈 | 查看 GPU 占用率 | 降低分辨率缩放,关闭抗锯齿 |
| 频繁卡顿后恢复 | 内存不足导致交换 | 查看内存占用 | 关闭后台应用,减小 Wine 虚拟内存上限 |
| 特定场景卡顿 | 着色器编译 | 观察卡顿是否集中在特定画面 | 开起着色器缓存,预编译常用着色器 |
除了这些通用的优化方向,还有一些针对特定程序的技巧。比如对于使用大量小文件的程序,可以把文件系统访问重定向到内存盘,减少 IO 开销;对于使用网络功能的程序,可以配置 Wine 的网络代理,避免网络超时导致的卡顿;对于使用音频的程序,可以调整音频缓冲大小,减少音频线程的调度频率。
4.4 与 iOS 系统特性的冲突处理
Madeira 运行在 iOS 上,不可避免地要和 iOS 的系统特性打交道。有些特性会帮助 Madeira 运行,有些则会带来冲突。
开发者模式是必须开启的,否则 Madeira 无法加载自定义二进制和申请 JIT 权限。但开发者模式本身也会带来一些副作用,比如设备的安全性降低,部分系统功能可能受限。如果你只是在测试阶段使用 Madeira,可以在测试完成后关闭开发者模式,恢复正常使用。
iOS 的后台管理机制对 Madeira 影响很大。iOS 会在应用进入后台后一段时间内挂起应用,如果 Madeira 正在运行一个长时间的任务,被挂起后可能会导致任务中断。解决方法是开启 Madeira 的后台运行权限,或者在运行长时间任务时保持应用在前台。
iOS 的内存管理机制也比较激进,当系统内存紧张时,会优先杀掉占用内存大的应用。Madeira 加上 Wine 和 FEX-Emu,内存占用通常不小,容易被系统盯上。解决方法是尽量在内存充足的设备上运行,或者关闭其它后台应用,给 Madeira 留出足够的内存空间。
5. 从 Madeira 延伸出去:跨平台兼容层的更多可能性
5.1 与 Linux 上 Wine 方案的对比
Madeira 在 iOS 上做的事情,和 Linux 上 Wine 做的事情本质相同,但面临的约束不同。Linux 上跑 Wine,没有沙盒限制,没有 JIT 权限问题,文件系统和进程管理都是开放的,所以 Wine 能发挥出接近原生的性能。iOS 上跑 Wine,处处受限,JIT 权限要申请,文件系统要重定向,进程管理要绕过,这些限制都会影响性能和兼容性。
但 iOS 也有自己的优势。iOS 设备的 GPU 性能普遍较强,Metal 的驱动优化也做得不错,DXMT 在 iOS 上的渲染效率,理论上可以接近甚至超过 Linux 上的 DXVK。iOS 的存储速度也很快,NVMe 的读写性能远超大部分 Linux 设备,这对 Wine 的前缀加载和文件访问有帮助。
从实际体验来看,Madeira 目前还达不到 Linux 上 Wine 的成熟度,但它的潜力不小。随着 FEX-Emu 和 DXMT 的持续优化,以及 iOS 对 JIT 权限的逐步放开,Madeira 这类方案的表现会越来越好。
5.2 对 iOS 开发者的启示
Madeira 的存在,对 iOS 开发者来说是一个有趣的案例。它展示了在 iOS 的严格限制下,如何通过组合多种技术手段,实现原本看似不可能的功能。JIT 权限的申请、动态代码生成的处理、图形接口的翻译,这些技术点对做 iOS 底层开发的工程师来说,都有参考价值。
比如 JIT 权限的申请流程,涉及到 entitlement 配置、签名验证、内存权限管理等多个环节,这些知识在做 iOS 性能优化、热更新、动态化方案时都会用到。再比如 DXMT 的图形接口翻译,涉及到 Metal 的底层 API 使用、着色器编译、资源管理,这些知识对做 iOS 图形开发的工程师来说,也是很好的学习材料。
5.3 后续可以尝试的扩展方向
如果你已经跑通了 Madeira 的基本功能,可以尝试一些扩展方向。比如把 Madeira 和 iOS 的自动化工具结合起来,实现 Windows 程序的自动化操作;比如把 Madeira 和 iOS 的网络功能结合起来,让 Windows 程序能够访问 iOS 的网络接口;比如把 Madeira 和 iOS 的文件系统结合起来,让 Windows 程序能够直接读写 iOS 的文件。
这些扩展方向,有些需要修改 Madeira 的源码,有些可以通过配置来实现。我建议先从简单的开始,比如配置 Wine 的网络代理,让 Windows 程序能够通过 iOS 的网络访问外部资源。这个配置相对简单,但能解锁很多新的使用场景。
5.4 我个人的一些实操体会
折腾 Madeira 这段时间,我最大的体会是:跨平台兼容层从来不是一蹴而就的事情,它需要大量的调试和适配。Wine 在 Linux 上发展了二十多年,才达到今天的成熟度;FEX-Emu 和 DXMT 相对年轻,还有很长的路要走。在 iOS 上跑 Windows 程序,目前还处于早期阶段,能跑通就已经是胜利,不要对性能和兼容性抱太高的期望。
另一个体会是,日志是最好的朋友。Madeira 涉及三个组件的协作,任何一个环节出问题,都会导致程序无法运行。这时候,仔细看日志,从错误信息里找线索,比盲目尝试有效得多。我习惯在跑一个新程序之前,先把日志级别调到最详细,跑一遍后仔细阅读日志,把所有的警告和错误都记下来,然后逐一排查。
最后一个体会是,社区的力量很重要。Madeira 这类项目,单靠一个人很难覆盖所有的设备和程序。社区里有人分享配置、有人反馈 bug、有人贡献代码,这些积累让项目能够持续进步。如果你在折腾过程中发现了新的问题或者新的解决方法,不妨分享出来,让更多人受益。