☰
在iOS上运行Windows应用:Wine+FEX-Emu+DXMT兼容层实践
2026/10/1 5:53:08 网站建设 项目流程

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-Emu(x86-64 到 ARM64 的指令转译层)、DXMT(Direct3D 到 Metal 的翻译层)这三块拼图,在 iOS 的约束条件下拼成一个能用的整体。适合谁来参考?如果你是有 iOS 开发基础、对底层兼容层感兴趣、或者想在自己的设备上跑一些 Windows 小工具的人,这篇内容会很有用。如果你只是想让 iPhone 变成 Windows 电脑,那预期需要放低一些,因为性能损耗和兼容性问题会比你想象的多。

我在这块踩过的坑不少,从最早的 Wine 乱码问题,到 FEX-Emu 在 iOS 上找不到可执行内存的报错,再到 DXMT 的 Metal 着色器编译失败,基本每一层都有坑。下面我会把整个项目的设计思路、核心细节、实操流程和排查经验完整拆开讲。

2. 整体架构设计与技术选型逻辑

2.1 为什么是 Wine + FEX-Emu + DXMT 这个组合

先解释这三者各自的位置。Wine 负责 Windows API 到 POSIX 的翻译,比如CreateFile映射到open,MessageBox映射到 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 原生性能损耗部署难度
Madeira(Wine+FEX+DXMT)FEX-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 \ --host=aarch64-apple-darwin \ --enable-archs=i386,x86_64 \ --without-alsa \ --without-pulse \ --without-x \ --without-freetype \ --with-coreaudio \ --disable-tests

这里--enable-archs=i386,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 Ordering,x86 的内存模型比 ARM 强,开启 TSO 模拟会带来大量内存屏障指令,性能下降明显。对于大多数不依赖严格内存序的程序,关掉 TSO 能提升 30% 以上的性能。

内存布局方面,iOS 的虚拟内存地址空间和 Linux 不同,FEX-Emu 默认的地址映射会冲突。需要在FEXCore的配置里把GuestBase设到一个 iOS 允许的区间,比如0x100000000。这个值需要根据实际设备的地址空间布局调整,我试过0x100000000和0x200000000两个值,前者在 iPhone 15 上更稳定。

3.3 DXMT 的 Metal 着色器编译优化

DXMT 把 D3D 的着色器字节码翻译成 Metal 的 AIR(Apple 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_TYPE=Release \ -DENABLE_JIT=0 \ -DENABLE_PREJIT=1 \ -DCMAKE_INSTALL_PREFIX=../output

这里ENABLE_JIT=0关掉 JIT,ENABLE_PREJIT=1开启预翻译支持。ios.toolchain.cmake需要自己写,核心是设置CMAKE_OSX_SYSROOT=iphoneos和CMAKE_OSX_ARCHITECTURES=arm64。

4.2 Wine 的交叉编译与静态链接

Wine 的编译是最耗时的一步,在 M2 MacBook Air 上大概需要 25 分钟。关键是要把winecrt0、ntdll、kernel32这些核心模块编译成静态库。

cd wine ./configure \ --host=aarch64-apple-darwin \ --enable-archs=i386,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 设计的。编译 DXMT:

cd dxmt meson setup build \ --cross-file=ios-cross.txt \ -Dbuildtype=release \ -Denable_tests=false ninja -C build

ios-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 = 0

forceSampleRateShading在 A17 Pro 上建议开启,能提升复杂着色器的渲染效率。enableMetalValidation在调试时开启,发布时关掉,否则性能损失很大。

4.4 启动脚本与运行时环境变量

所有组件编译好后,需要一个启动脚本来设置环境变量并调用 Wine 的入口。在 iOS 上,这个脚本通常用 Objective-C 或者 Swift 写,通过posix_spawn启动 Wine 进程。

关键环境变量:

export WINEPREFIX=$HOME/Documents/wineprefix export WINEDLLOVERRIDES="mscoree,mshtml=" export FEX_APP_CONFIG="TSOEnabled=0;GuestBase=0x100000000" export DXMT_SHADER_CACHE=$HOME/Documents/dxmt_cache export WINEDEBUG=-all

WINEDLLOVERRIDES里的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 模式。

排查步骤:

  1. 检查FEX_APP_CONFIG里EnableJIT是否被误设为1,必须是0。
  2. 检查预翻译缓存文件是否存在,路径是否正确。缓存文件通常在$HOME/Documents/fex_cache下。
  3. 检查 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=-all

5.4 iOS 开发者模式与签名问题

热搜里出现了“ios开发者模式”和“免费证书ios”,这里简单提一下。在 iOS 16 以上,安装自签名应用需要开启开发者模式(设置 → 隐私与安全性 → 开发者模式)。免费证书签名的应用有效期只有 7 天,过期后需要重新签名。对于 Madeira 这种项目,建议用付费开发者账号,签名有效期一年,省去频繁重签的麻烦。

另外,Xcode 打包时如果报Provisioning profile doesn't match,检查 Bundle ID 是否和证书里的 App ID 一致。我遇到过因为 Bundle ID 里带了-导致签名失败,改成纯字母就好了。

6. 性能调优与后续扩展方向

6.1 实测性能数据与瓶颈分析

在 iPhone 15 Pro(A17 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 系统更新都可能破坏兼容性,需要重新编译和调试。但如果你对底层技术感兴趣,这个项目能让你深入理解指令转译、图形翻译和沙盒绕过的一整套工程实践,收获还是很大的。

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

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

立即咨询