1. 项目缘起:为什么要在 iOS 上折腾 Wine 兼容层
“Madeira”这个项目标题,乍一看像是个地名,但在我们这群折腾跨平台兼容层的人眼里,它代表的是一个非常具体的尝试:在 iOS 设备上跑通 Wine 兼容层,让 x86-64 的 Windows 应用能在 ARM 架构的 iPhone 或 iPad 上运行起来。这个想法听起来有点疯狂,但背后的需求是真实存在的——很多行业软件、老游戏、专业工具只有 Windows 版本,而手头只有 iPad 或者 iPhone,云电脑延迟高、订阅贵,本地跑一个兼容层就成了很有吸引力的方案。
我接触 Wine 差不多有七八年了,从最早在 Linux 桌面发行版上编译 Wine 源码,到后来用 Deepin、统信 UOS 上的 Wine 助手,再到折腾 FEX-Emu 和 DXMT 这类转译层,踩过的坑可以说能写一本小册子。iOS 上的 Wine 方案和桌面端完全不是一个难度级别,因为 iOS 的沙盒机制、代码签名、内存管理策略、图形 API 限制,每一条都在跟你作对。但正因为难,做出来才有意思。
这个项目适合什么人看?如果你是一个对 iOS 底层机制感兴趣、愿意花时间折腾、手头有越狱设备或者开发者账号的玩家,那这篇内容会对你有很大帮助。如果你只是想找个“一键安装 Windows 应用”的方案,那我得提前说清楚:iOS 上的 Wine 目前还远没到开箱即用的程度,你需要有心理准备。我会把整个思路、技术选型、实操步骤、踩坑记录都摊开来讲,尽量让不同基础的人都能找到自己需要的东西。
2. 整体架构设计:Wine + FEX-Emu + DXMT 的三层组合
2.1 为什么不能直接用 Wine 跑 x86-64 程序
Wine 本身不是一个模拟器,它是一个兼容层,负责把 Windows 的 API 调用翻译成 POSIX 调用。这意味着 Wine 要求目标程序的指令集和宿主 CPU 的指令集是一致的。你的 iPhone 是 ARM64 架构,而绝大多数 Windows 应用是 x86 或 x86-64 架构,指令集对不上,Wine 再厉害也没法直接执行这些二进制文件。
所以我们需要在 Wine 下面再垫一层:指令集转译层。这就是 FEX-Emu 出场的地方。FEX-Emu 是一个用户态的 x86-64 到 ARM64 的二进制转译器,它的工作方式是把 x86-64 指令动态翻译成 ARM64 指令,然后交给 CPU 执行。和 QEMU 的全系统模拟不同,FEX-Emu 只做用户态转译,性能损耗小得多,在树莓派、安卓设备上已经有比较成熟的应用案例。
2.2 三层架构的分工
整个方案的分层是这样的:
| 层级 | 组件 | 职责 |
|---|---|---|
| 应用层 | Windows 程序 | 用户实际要运行的目标软件 |
| API 翻译层 | Wine | 把 Windows API 调用翻译为 POSIX 调用 |
| 指令转译层 | FEX-Emu | 把 x86-64 指令转译为 ARM64 指令 |
| 图形翻译层 | DXMT | 把 Direct3D 调用翻译为 Metal 调用 |
| 系统层 | iOS | 提供沙盒、内存、GPU 等底层资源 |
DXMT 是这里面比较新的一个组件,它的全称是 DirectX Metal Translation,专门负责把 D3D11 和部分 D3D12 的调用翻译成苹果的 Metal API。在 iOS 上你没法用 Vulkan,也没法用 OpenGL 的桌面版本,Metal 是唯一的高性能图形接口,所以 DXMT 是绕不开的一环。
2.3 为什么选 FEX-Emu 而不是 QEMU
QEMU 的用户态模式也能做 x86-64 到 ARM64 的转译,但它的翻译策略偏向“块翻译”,每次遇到新的代码块都要重新翻译,缓存命中率不如 FEX-Emu。FEX-Emu 用了更激进的 SMC(自修改代码)检测和块缓存策略,对于游戏这种循环密集型的负载,性能优势很明显。实测在同等硬件上,FEX-Emu 跑同一个 x86-64 程序的帧率大概是 QEMU 用户态模式的 2 到 3 倍。
另一个原因是 FEX-Emu 对 Wine 的适配做得更细。它内置了对 Windows 系统调用约定的特殊处理,比如 syscall 指令的模拟、TEB(线程环境块)的映射,这些细节 QEMU 处理起来会比较粗糙,容易导致程序崩溃或者行为异常。
2.4 iOS 沙盒带来的额外约束
桌面 Linux 上跑 Wine + FEX-Emu,你基本不用操心权限问题。但在 iOS 上,每个应用都活在沙盒里,能做的事情被严格限制:
- 无法 fork 任意进程:Wine 需要创建多个进程来模拟 Windows 的多进程模型,iOS 的 posix_spawn 限制很多,需要特殊处理。
- 无法直接映射可执行内存:FEX-Emu 需要把翻译后的 ARM64 代码写到内存里执行,iOS 的 W^X 策略(可写和可执行不能同时存在)需要绕过,通常靠 JIT 权限或者越狱环境下的特殊 entitlement。
- 文件系统隔离:Wine 的 C 盘映射需要在一个可写的目录里模拟,iOS 的沙盒容器路径和 Windows 的路径风格差异很大,需要做路径重映射。
- 图形上下文限制:Metal 的层需要绑定到 UIView 或者 CAMetalLayer 上,Wine 的窗口系统需要和 iOS 的视图层级做桥接。
这些问题在“Madeira”项目里都需要逐一解决,后面我会详细讲每个环节的处理方式。
3. 核心组件拆解与关键配置
3.1 Wine 的编译与裁剪
在 iOS 上编译 Wine 不是一件轻松的事,因为官方源码树默认面向 Linux/macOS,很多依赖在 iOS 上不存在。我的做法是只保留核心模块,把不需要的部分全部裁掉:
- 保留:ntdll、kernel32、user32、gdi32、d3d11、dxgi、winemac.drv(改造成 iOS 驱动)
- 裁掉:winealsa、winepulse、winex11、winewayland、所有打印相关模块、所有网络服务发现模块
编译工具链用 Xcode 的 clang,target 设为arm64-apple-ios14.0,需要额外传入-isysroot指向 iOS SDK。这里有个坑:Wine 的 configure 脚本会检测很多 Linux 特有的头文件,你需要手动打补丁跳过这些检测,否则 configure 阶段就会失败。
# 交叉编译 Wine 的关键 configure 参数 ./configure \ --host=arm64-apple-ios14.0 \ --with-wine-tools=/path/to/native/wine/tools \ --disable-tests \ --disable-winetest \ --without-alsa \ --without-pulse \ --without-cups \ --without-dbus \ --without-gnutls \ --without-krb5 \ --without-netapi \ --without-opencl \ --without-pcap \ --without-sane \ --without-udev \ --without-v4l2 \ --without-x注意:
--with-wine-tools必须指向一个已经在本机编译好的 Wine 工具集,因为交叉编译过程中需要用到winebuild、wmc、wrc这些工具来生成头文件和资源文件。我一般先在 macOS 上编译一份原生 Wine,专门用来提供这些工具。
3.2 FEX-Emu 的移植要点
FEX-Emu 本身是 C++ 写的,对 POSIX 的依赖比 Wine 少一些,但它在 iOS 上跑需要解决两个核心问题:
第一是 JIT 内存的分配。FEX-Emu 默认用mmap加PROT_EXEC来分配可执行内存,iOS 上这个调用会被拒绝,除非你的应用有com.apple.security.cs.allow-jit这个 entitlement,而且即使有这个 entitlement,也需要用MAP_JIT标志配合pthread_jit_write_protect_np来切换内存的写/执行状态。
第二是信号处理。FEX-Emu 依赖 SIGSEGV 和 SIGBUS 来捕获某些异常指令和内存访问,iOS 的信号处理机制和 Linux 有差异,需要重新实现信号 handler 的注册逻辑,并且要确保 handler 是 async-signal-safe 的。
// iOS 上分配 JIT 内存的典型做法 void* jit_alloc(size_t size) { void* ptr = mmap(NULL, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_JIT, -1, 0); if (ptr == MAP_FAILED) { // 回退方案:先分配可写内存,执行前再改权限 ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); } return ptr; }3.3 DXMT 的 Metal 后端适配
DXMT 在 macOS 上已经有比较成熟的实现,移植到 iOS 的主要工作是:
- 把
NSView替换成UIView,CAMetalLayer的绑定方式从 macOS 的NSView.layer改成 iOS 的UIView.layer。 - 处理 iOS 的
drawable尺寸和contentsScale,确保渲染分辨率跟屏幕的 Retina 缩放匹配。 - 适配 iOS 的
MTLCommandQueue提交频率,iOS 对每帧的 command buffer 数量比 macOS 更敏感,需要做批处理优化。
DXMT 的配置主要通过环境变量控制,下面这几个是我实测下来比较关键的:
# DXMT 关键环境变量 export DXMT_LOG_LEVEL=warn # 日志级别,调试时改成 debug export DXMT_SHADER_CACHE=1 # 开启着色器缓存,大幅减少卡顿 export DXMT_MAX_FRAME_LATENCY=2 # 最大帧延迟,iOS 上建议 2 或 3 export DXMT_METAL_DEVICE_INDEX=0 # 指定 Metal 设备,多 GPU 设备上需要 export DXMT_FORCE_FEATURE_LEVEL=11_0 # 强制 D3D 特性等级3.4 窗口系统桥接
Wine 在 macOS 上用 winemac.drv 来对接 Cocoa 的窗口系统,在 iOS 上需要写一个类似的驱动,我把它叫做 wineios.drv。这个驱动的核心职责是:
- 把 Windows 的 HWND 映射到 iOS 的 UIView 层级上。
- 处理触摸事件到 Windows 鼠标/键盘消息的转换。
- 管理 UIWindow 和 UIViewController 的生命周期,确保 Wine 的窗口能正确响应旋转、分屏、后台切换等系统事件。
这里有个细节值得说:iOS 的触摸事件是多点触控的,而 Windows 程序通常只认单点鼠标。我的处理方式是取第一个触摸点作为鼠标位置,同时提供一个虚拟鼠标模式,让用户可以用手指拖动来移动光标。对于需要右键的场景,用双指点击来模拟。
4. 实操全流程:从零到跑起一个 Windows 程序
4.1 环境准备与依赖安装
先列一下我用的软硬件环境:
- 设备:iPhone 15 Pro(A17 Pro,8GB 内存),系统版本 iOS 17.4,已越狱(用 palera1n 或者 Dopamine,取决于具体版本)。
- 开发机:macOS 14.5,Xcode 15.4,Command Line Tools 已安装。
- 依赖:Homebrew 安装的 cmake、ninja、pkg-config、autoconf、automake、libtool。
第一步是拉取各个组件的源码:
mkdir -p ~/madeira && cd ~/madeira git clone --depth 1 https://github.com/wine-mirror/wine.git git clone --depth 1 https://github.com/FEX-Emu/FEX.git git clone --depth 1 https://github.com/3Shain/dxmt.git第二步是编译 FEX-Emu 的 ARM64 版本。FEX-Emu 的构建系统是 CMake,需要指定 iOS 的 toolchain 文件:
cd FEX mkdir build-ios && cd build-ios cmake .. \ -DCMAKE_TOOLCHAIN_FILE=../Data/CMake/toolchain_ios.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DENABLE_LTO=ON \ -DBUILD_TESTS=OFF \ -DBUILD_THUNKS=ON ninja -j$(sysctl -n hw.ncpu)编译过程中如果遇到undefined symbol: __clear_cache之类的错误,需要在 CMake 里加上-DCMAKE_EXE_LINKER_FLAGS="-lSystem",因为 iOS 的 libSystem 里包含了这个符号。
4.2 Wine 的编译与安装
Wine 的编译分两步:先在 macOS 上编译一份原生工具,再交叉编译 iOS 版本。
# 第一步:编译原生工具 cd wine mkdir build-native && cd build-native ../configure --enable-win64 --disable-tests make -j$(sysctl -n hw.ncpu) tools/winebuild tools/wmc tools/wrc # 第二步:交叉编译 iOS 版本 cd .. && mkdir build-ios && cd build-ios ../configure \ --host=arm64-apple-ios14.0 \ --with-wine-tools=../build-native \ --disable-tests \ --without-x \ --without-alsa \ --without-pulse \ --without-cups \ --without-dbus \ --without-gnutls \ --without-krb5 \ --without-netapi \ --without-opencl \ --without-pcap \ --without-sane \ --without-udev \ --without-v4l2 make -j$(sysctl -n hw.ncpu)编译完成后,你会得到libwine.a和一堆.dll文件。这些文件需要打包进 iOS 应用的 bundle 里,放在Resources/wine/目录下。
实操心得:Wine 的编译非常吃内存,建议开发机至少 16GB 内存,否则
make -j的时候很容易被 OOM killer 干掉。如果内存不够,把并行度降到-j4或者-j2。
4.3 打包成 iOS 应用
把编译好的 Wine、FEX-Emu、DXMT 整合到一个 Xcode 工程里,主要工作是:
- 创建一个新的 iOS App 工程,语言选 Objective-C++(因为要同时调 C++ 和 Objective-C 的 API)。
- 把
libwine.a、libFEXCore.a、libdxmt.a添加到 Link Binary With Libraries。 - 把 Wine 的 dll 文件和 FEX-Emu 的配置文件放到 Copy Bundle Resources 里。
- 在 Build Settings 里开启
Enable JIT(需要越狱环境或者特殊 entitlement)。 - 写一个入口函数,初始化 FEX-Emu 的转译上下文,然后调用 Wine 的
wine_main。
入口函数的骨架大概长这样:
// AppDelegate.mm #import <UIKit/UIKit.h> #include "fexcore/Context.h" #include "wine/debug.h" extern "C" int wine_main(int argc, char** argv); @implementation AppDelegate - (BOOL)application:(UIApplication*)app didFinishLaunchingWithOptions:(NSDictionary*)options { // 初始化 FEX-Emu FEXCore::Context::Initialize(); // 设置 Wine 的路径 setenv("WINEPREFIX", [self winePrefixPath].UTF8String, 1); setenv("WINEDLLPATH", [self wineDllPath].UTF8String, 1); // 启动 Wine dispatch_async(dispatch_get_global_queue(0, 0), ^{ char* argv[] = {"wine", "notepad.exe", NULL}; wine_main(2, argv); }); return YES; } @end4.4 首次运行与初始化 Wineprefix
第一次启动时,Wine 需要初始化一个 wineprefix,这个过程在 iOS 上会比较慢,因为要创建大量的目录结构和注册表文件。我的做法是在应用首次启动时,把预先打包好的 wineprefix 从 bundle 里复制到沙盒的 Documents 目录下,这样能省掉初始化时间。
# 在开发机上预先初始化 wineprefix WINEPREFIX=~/madeira/prefix wineboot -u # 然后把 ~/madeira/prefix 整个目录打包进 iOS 应用的 bundle复制完成后,需要修正 wineprefix 里的路径,因为开发机上的路径和 iOS 沙盒路径不一样。Wine 的注册表里有很多硬编码的路径,需要用wine regedit或者直接改.reg文件来替换。
4.5 跑第一个程序:记事本
记事本(notepad.exe)是最简单的测试目标,它只依赖 user32、gdi32 和 kernel32,不涉及复杂的图形调用。如果记事本能跑起来,说明 Wine 的核心层和 FEX-Emu 的转译层都工作正常。
启动命令:
WINEPREFIX=/var/mobile/Containers/Data/Application/XXX/Documents/prefix \ WINEDLLPATH=/var/mobile/Containers/Data/Application/XXX/Documents/wine/lib \ wine notepad.exe如果一切顺利,你会在屏幕上看到一个 Windows 风格的记事本窗口。如果窗口是黑的或者花屏,那多半是 DXMT 的 Metal 层没配置好,需要检查CAMetalLayer的绑定和contentsScale的设置。
4.6 进阶测试:跑一个 D3D11 程序
记事本只能验证基础功能,真正考验兼容层的是 D3D11 程序。我一般用dxdiag.exe和一个小型的 D3D11 测试程序来验证。
# 先跑 dxdiag 看 D3D 设备信息 wine dxdiag.exe # 再跑一个简单的 D3D11 三角形程序 wine d3d11-triangle.exe如果 dxdiag 能正确识别出 Metal 设备,并且三角形程序能渲染出画面,那说明 DXMT 的翻译链路是通的。这时候可以尝试跑一些老游戏,比如《植物大战僵尸》或者《魔兽争霸3》,这些游戏的 D3D 版本比较老,兼容性相对好。
5. 常见问题与排查技巧实录
5.1 Wine 输出乱码怎么处理
Wine 在终端输出乱码是个老问题,根本原因是 Wine 默认用 UTF-8 输出,但 iOS 的终端环境可能用的是别的编码。解决办法是设置LANG和LC_ALL环境变量:
export LANG=en_US.UTF-8 export LC_ALL=en_US.UTF-8如果还是乱码,那可能是 Wine 的字体配置有问题。Wine 需要一套 Windows 字体来渲染界面,如果字体缺失,中文会显示成方块。解决办法是把simsun.ttc、msyh.ttf这些字体复制到 wineprefix 的drive_c/windows/Fonts/目录下,然后在注册表里配置字体替换。
5.2 FEX-Emu 崩溃的常见原因
FEX-Emu 在 iOS 上崩溃,八成是下面几个原因之一:
| 崩溃现象 | 可能原因 | 解决办法 |
|---|---|---|
| SIGSEGV at 0x0 | JIT 内存没分配成功 | 检查 MAP_JIT 权限和 entitlement |
| SIGILL | 遇到了不支持的 x86 指令 | 更新 FEX-Emu 到最新版本,或者用FEX_DISABLE_OPT=1关闭优化 |
| SIGBUS | 内存对齐问题 | 检查 Wine 的内存分配器配置,尝试WINEDEBUG=+heap |
| 卡死无响应 | 信号处理死锁 | 确保信号 handler 里没有调用非 async-signal-safe 的函数 |
实操心得:FEX-Emu 有一个
FEX_OUTPUTLOG环境变量,设置成stderr可以把转译日志打到终端,排查指令级问题的时候非常有用。但注意日志量很大,只在需要的时候开。
5.3 DXMT 渲染黑屏的排查思路
DXMT 黑屏是最让人头疼的问题,因为涉及 Metal、D3D、Wine 三层,任何一层出问题都会黑屏。我的排查顺序是:
- 先确认 Metal 层是否正常:写一个最小的 Metal 程序,画一个纯色三角形,确认 iOS 的 Metal 驱动没问题。
- 再确认 DXMT 的初始化日志:设置
DXMT_LOG_LEVEL=debug,看 DXMT 有没有成功创建 Metal 设备、编译着色器。 - 然后确认 Wine 的 D3D 调用:设置
WINEDEBUG=+d3d11,看 Wine 有没有把 D3D 调用转发给 DXMT。 - 最后确认窗口绑定:检查
CAMetalLayer的frame和drawableSize是否正确,contentsScale是否和屏幕匹配。
5.4 性能优化的几个关键点
iOS 设备的散热和功耗限制比桌面严格得多,性能优化是必须做的:
- 开着色器缓存:DXMT 的着色器编译很耗时,开启缓存后第二次运行会快很多。
- 限制帧率:iOS 上跑 60fps 会让设备很快发热降频,建议用
DXMT_MAX_FRAME_LATENCY限制到 30fps。 - 关闭不必要的日志:日志输出会占用大量 CPU 时间,生产环境一定要把日志级别调到 warn 以上。
- 用 Release 模式编译:Debug 模式的 FEX-Emu 性能只有 Release 的十分之一,千万别用 Debug 跑游戏。
5.5 常见问题速查表
| 问题 | 排查方向 | 快速修复 |
|---|---|---|
| Wine 启动即崩溃 | 检查 wineprefix 路径和权限 | 重新初始化 wineprefix |
| 程序窗口不显示 | 检查 wineios.drv 的窗口绑定 | 确认 UIView 层级和 UIWindow 的 rootViewController |
| 中文显示为方块 | 字体缺失 | 复制中文字体到 wineprefix 的 Fonts 目录 |
| 游戏帧率极低 | FEX-Emu 没开优化 | 确认编译时开了-DENABLE_LTO=ON |
| 触摸无响应 | 事件桥接没工作 | 检查 wineios.drv 的触摸事件转发逻辑 |
| 音频爆音 | 音频后端不匹配 | 尝试用 CoreAudio 后端,或者关闭音频 |
6. 后续扩展与个人体会
这个项目目前还在持续迭代中,有几个方向是我接下来想做的。一个是把 D3D12 的支持补上,现在 DXMT 对 D3D12 的支持还比较初步,很多新游戏跑不起来。另一个是优化 FEX-Emu 的块缓存策略,针对 iOS 的内存限制做更激进的缓存淘汰,减少内存占用。还有就是做一个图形化的前端,让用户不用敲命令行就能管理 wineprefix 和启动程序。
我在实际折腾的过程中最大的体会是:iOS 上的 Wine 兼容层,难点不在 Wine 本身,而在 iOS 的系统限制。沙盒、JIT、信号、内存管理,每一个都是硬骨头。但反过来想,正是因为这些限制,才让这个项目有挑战性。如果你也在折腾类似的东西,我的建议是先把 FEX-Emu 单独跑通,确认指令转译没问题,再往上叠 Wine 和 DXMT。分层调试,逐层验证,比一上来就整合所有组件要高效得多。
最后分享一个小技巧:iOS 的开发者模式(Developer Mode)在 iOS 16 之后需要在设置里手动开启,而且每次重启设备都会重置。如果你经常重启设备调试,记得把开启开发者模式的步骤写成一个快捷指令,能省不少时间。另外,Xcode 打包 iOS 应用突然变慢,多半是 DerivedData 缓存太大了,定期清理~/Library/Developer/Xcode/DerivedData能明显改善编译速度。