☰
iOS 上跑 Wine 兼容层:FEX-Emu 与 DXMT 实战
2026/10/1 13:33:23 网站建设 项目流程

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 工程里,主要工作是:

  1. 创建一个新的 iOS App 工程,语言选 Objective-C++(因为要同时调 C++ 和 Objective-C 的 API)。
  2. 把libwine.a、libFEXCore.a、libdxmt.a添加到 Link Binary With Libraries。
  3. 把 Wine 的 dll 文件和 FEX-Emu 的配置文件放到 Copy Bundle Resources 里。
  4. 在 Build Settings 里开启Enable JIT(需要越狱环境或者特殊 entitlement)。
  5. 写一个入口函数,初始化 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; } @end

4.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 0x0JIT 内存没分配成功检查 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 三层,任何一层出问题都会黑屏。我的排查顺序是:

  1. 先确认 Metal 层是否正常:写一个最小的 Metal 程序,画一个纯色三角形,确认 iOS 的 Metal 驱动没问题。
  2. 再确认 DXMT 的初始化日志:设置DXMT_LOG_LEVEL=debug,看 DXMT 有没有成功创建 Metal 设备、编译着色器。
  3. 然后确认 Wine 的 D3D 调用:设置WINEDEBUG=+d3d11,看 Wine 有没有把 D3D 调用转发给 DXMT。
  4. 最后确认窗口绑定:检查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能明显改善编译速度。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询