恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
iOS 上跑 Wine 兼容层:FEX-Emu 与 DXMT 实战
首页
资讯中心
/
iOS 上跑 Wine 兼容层:FEX-Emu 与 DXMT 实战
iOS 上跑 Wine 兼容层:FEX-Emu 与 DXMT 实战
发布时间:2026/10/1 13:33:25
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 而不是 QEMUQEMU 的用户态模式也能做 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 的 clangtarget 设为arm64-apple-ios14.0需要额外传入-isysroot指向 iOS SDK。这里有个坑Wine 的 configure 脚本会检测很多 Linux 特有的头文件你需要手动打补丁跳过这些检测否则 configure 阶段就会失败。# 交叉编译 Wine 的关键 configure 参数 ./configure \ --hostarm64-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替换成UIViewCAMetalLayer的绑定方式从 macOS 的NSView.layer改成 iOS 的UIView.layer。处理 iOS 的drawable尺寸和contentsScale确保渲染分辨率跟屏幕的 Retina 缩放匹配。适配 iOS 的MTLCommandQueue提交频率iOS 对每帧的 command buffer 数量比 macOS 更敏感需要做批处理优化。DXMT 的配置主要通过环境变量控制下面这几个是我实测下来比较关键的# DXMT 关键环境变量 export DXMT_LOG_LEVELwarn # 日志级别调试时改成 debug export DXMT_SHADER_CACHE1 # 开启着色器缓存大幅减少卡顿 export DXMT_MAX_FRAME_LATENCY2 # 最大帧延迟iOS 上建议 2 或 3 export DXMT_METAL_DEVICE_INDEX0 # 指定 Metal 设备多 GPU 设备上需要 export DXMT_FORCE_FEATURE_LEVEL11_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 ProA17 Pro8GB 内存系统版本 iOS 17.4已越狱用 palera1n 或者 Dopamine取决于具体版本。开发机macOS 14.5Xcode 15.4Command 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_TYPERelease \ -DENABLE_LTOON \ -DBUILD_TESTSOFF \ -DBUILD_THUNKSON 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 \ --hostarm64-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 LANGen_US.UTF-8 export LC_ALLen_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 权限和 entitlementSIGILL遇到了不支持的 x86 指令更新 FEX-Emu 到最新版本或者用FEX_DISABLE_OPT1关闭优化SIGBUS内存对齐问题检查 Wine 的内存分配器配置尝试WINEDEBUGheap卡死无响应信号处理死锁确保信号 handler 里没有调用非 async-signal-safe 的函数实操心得FEX-Emu 有一个FEX_OUTPUTLOG环境变量设置成stderr可以把转译日志打到终端排查指令级问题的时候非常有用。但注意日志量很大只在需要的时候开。5.3 DXMT 渲染黑屏的排查思路DXMT 黑屏是最让人头疼的问题因为涉及 Metal、D3D、Wine 三层任何一层出问题都会黑屏。我的排查顺序是先确认 Metal 层是否正常写一个最小的 Metal 程序画一个纯色三角形确认 iOS 的 Metal 驱动没问题。再确认 DXMT 的初始化日志设置DXMT_LOG_LEVELdebug看 DXMT 有没有成功创建 Metal 设备、编译着色器。然后确认 Wine 的 D3D 调用设置WINEDEBUGd3d11看 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_LTOON触摸无响应事件桥接没工作检查 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能明显改善编译速度。