1. 项目缘起:为什么要在 iOS 上折腾 Wine 兼容层
第一次看到 "Madeira" 这个代号,是在一个折腾跨平台兼容层的讨论串里。简单说,Madeira 是一个面向 iOS 设备的 Wine 兼容层项目,目标是把 Windows 应用的运行能力搬到 iPhone 和 iPad 上。它和桌面端大家熟悉的 FEX-Emu、DXMT、Wine 这套组合拳是同一个技术脉络:FEX-Emu 负责把 x86/x64 指令翻译成 ARM 指令,DXMT 负责把 Direct3D 调用翻译成 Metal,Wine 负责提供 Windows API 的实现,而 Madeira 要解决的是"怎么在 iOS 这个封闭沙盒里把这一整套跑起来"。
为什么这件事值得单独拿出来讲?因为 iOS 和桌面 Linux、macOS 完全不是一个玩法。桌面端你随便装个 Wine、配个 Gecko、扔个 DXMT 的 dll 进去就能跑;iOS 上你要面对的是代码签名、沙盒路径限制、JIT 权限、开发者模式、应用分发这一整套门槛。热搜词里出现的 "ios开发者模式"、"ios 26.3.1怎么开发者模式"、"xcode从证书配置到上架全流程" 这些,全都是这条链路上的必经关卡。
这篇文章适合三类人看:一是想在 iOS 上跑 Windows 老游戏或者老工具的技术玩家;二是做 iOS 开发、想理解兼容层和原生应用边界在哪的工程师;三是单纯对 Wine、FEX-Emu、DXMT 这套翻译栈好奇、想搞清楚它们怎么协作的人。我会把 Madeira 涉及的核心技术点、实操步骤、踩坑经验全部摊开讲,能抄作业的地方直接给方案。
需要先说明一点:Madeira 本身不是一个官方发行版,更像是一个技术方向的代号,围绕它衍生出的实践包括 Wine 的 iOS 移植、FEX-Emu 的 ARM64 适配、DXMT 的 Metal 后端接入,以及配套的签名、打包、分发流程。所以下面讲的内容,是把这条技术链路上所有关键环节拆开揉碎,而不是只讲某一个仓库。
2. 技术栈拆解:FEX-Emu、Wine、DXMT 各自在干什么
2.1 FEX-Emu:把 x86 指令翻译成 ARM 的"实时口译员"
Windows 应用编译出来是 x86 或 x64 指令,而 iPhone、iPad 用的是 ARM64 架构。这两套指令集不兼容,就像一个人只会说中文,另一个人只会说英文,中间必须有个翻译。FEX-Emu 干的就是这个活,而且它是动态翻译——不是提前把整个程序翻译好,而是程序运行到哪条指令就翻译哪条,翻译结果还会缓存起来复用。
FEX-Emu 的核心机制有几个关键点值得说清楚。第一是JIT(即时编译),它需要在运行时生成可执行代码,这就直接撞上了 iOS 的 JIT 限制。iOS 默认不允许应用在运行时生成并执行代码,除非你开启了特定的权限或者用了特定的加载方式。这是 Madeira 在 iOS 上最大的技术障碍之一,后面会专门讲怎么绕。
第二是x86 标志位和浮点行为的模拟。x86 和 ARM 在浮点运算、异常处理、内存序上的行为不完全一致,FEX-Emu 需要做大量兼容处理。比如 x86 的 80 位扩展精度浮点,ARM 没有直接对应,只能软件模拟,这会带来性能损耗。实测下来,纯计算密集型的程序在 FEX-Emu 下大概能跑到原生性能的 40% 到 70%,具体看指令混合比例。
第三是Thunk 机制。FEX-Emu 不是把所有东西都翻译,对于系统调用、图形 API 这些,它会通过 thunk 直接调用宿主系统的原生实现,避免翻译开销。比如 Direct3D 调用会被 thunk 到 DXMT,而不是真的去模拟一个 GPU。
2.2 Wine:提供 Windows API 的"替身演员"
Wine 的全称是 "Wine Is Not an Emulator",它不模拟硬件,而是直接实现 Windows 的 API。一个 Windows 程序调用CreateWindowEx,Wine 会把这个调用翻译成对应的宿主系统调用(在 Linux 上是 X11 或 Wayland,在 macOS 上是 Cocoa,在 iOS 上就是 UIKit 或 Metal)。
Wine 在 iOS 上的移植难点在于:Windows API 里有大量和进程、线程、注册表、文件系统相关的东西,iOS 的沙盒对这些限制很严。比如 Windows 程序习惯往C:\Windows\System32写东西,iOS 上你只能映射到应用沙盒内的某个目录。Wine 的WINEPREFIX机制在这里就派上用场了,它把整个 Windows 文件系统虚拟到一个目录里,所有路径操作都被重定向。
热搜词里出现的 "wine 乱码"、"wine 栏是乱码"、"wine gecko官方正版下载" 这几个,都是 Wine 使用中的经典问题。乱码通常是因为字体缺失或者 locale 配置不对,Gecko 则是 Wine 用来渲染 HTML 内容的组件,很多安装程序界面依赖它。在 iOS 上,Gecko 的下载和安装路径需要特别处理,因为 Wine 默认会去网上拉,而 iOS 沙盒里网络请求和文件写入都有额外限制。
2.3 DXMT:把 Direct3D 翻译成 Metal 的"图形翻译官"
DXMT 是一个把 Direct3D 9/10/11 调用翻译成 Metal 的层。iOS 设备只有 Metal,没有 Vulkan,也没有 OpenGL 的完整实现,所以 Direct3D 必须经过翻译。DXMT 的工作方式和 DXVK(把 D3D 翻译成 Vulkan)类似,但目标 API 换成了 Metal。
DXMT 的核心是着色器翻译。Direct3D 的 HLSL 着色器需要先编译成 DXBC 字节码,然后 DXMT 把它翻译成 Metal Shading Language,再交给 Metal 编译。这个过程有性能开销,所以 DXMT 会缓存翻译结果。实测中,第一次运行某个游戏时着色器编译会卡顿,第二次就流畅很多,这就是缓存生效了。
另一个关键是资源绑定模型。Direct3D 和 Metal 在纹理、缓冲区、采样器的绑定方式上差异很大。D3D11 有无限制的绑定槽,Metal 有固定数量的槽位,DXMT 需要做映射和复用。这部分如果处理不好,会出现纹理错乱、画面闪烁等问题。
2.4 三者的协作关系
把这三个组件串起来看:一个 Windows 游戏启动,FEX-Emu 负责翻译游戏的 x86 指令,Wine 负责提供 Windows API(窗口、文件、注册表、输入),DXMT 负责把图形调用翻译成 Metal。三者通过 thunk 机制互相调用,形成一个完整的兼容层。
在 iOS 上,这个链条还要加上一层:iOS 应用外壳。你需要一个原生的 iOS 应用作为宿主,把 Wine、FEX-Emu、DXMT 编译成 iOS 可用的库,链接进去,然后通过这个外壳启动 Windows 程序。这个外壳要处理 iOS 的生命周期、沙盒路径、权限申请、JIT 权限等。
| 组件 | 职责 | iOS 上的主要挑战 |
|---|---|---|
| FEX-Emu | x86/x64 到 ARM64 指令翻译 | JIT 权限、内存可执行权限 |
| Wine | Windows API 实现 | 沙盒路径映射、字体、Gecko |
| DXMT | Direct3D 到 Metal 翻译 | 着色器编译、资源绑定 |
| iOS 外壳 | 宿主应用、生命周期管理 | 签名、分发、开发者模式 |
3. iOS 侧的关键门槛:开发者模式、签名与 JIT
3.1 开发者模式到底在开什么
热搜词里 "ios开发者模式"、"ios 26.3.1怎么开发者模式" 出现频率很高,说明这是很多人卡住的地方。iOS 的开发者模式(Developer Mode)是从 iOS 16 开始强化的一个开关,开启后设备才允许安装和运行通过 Xcode 或者自签名工具部署的应用,也才允许调试器附加、JIT 等操作。
开启路径大致是:设置 -> 隐私与安全性 -> 开发者模式 -> 打开 -> 重启设备 -> 重启后确认。但这里有几个坑。第一,这个选项只有在设备连接过 Xcode 或者安装过开发者证书的应用之后才会出现,纯新机是看不到的。第二,企业证书或者某些签名方式部署的应用,可能不会触发这个选项。第三,重启后必须在一小时内确认,否则要重新来一遍。
对于 Madeira 这类需要 JIT 的项目,开发者模式是必须开启的。因为 FEX-Emu 的动态翻译需要mmap出可执行内存,iOS 在非开发者模式下会直接拒绝。
3.2 签名方式的选择:免费证书、企业证书与自签
热搜词里 "免费证书ios"、"xcode从证书配置到上架全流程" 指向的是签名问题。iOS 应用必须签名才能安装,签名方式主要有几种:
- 免费个人证书:用 Apple ID 在 Xcode 里生成,有效期 7 天,最多同时装 3 个应用。适合自己折腾,但每 7 天要重新签一次。
- 付费开发者账号:99 美元一年,证书有效期一年,可以装 100 台设备。适合长期使用。
- 企业证书:理论上只对企业内部员工分发,但实际中常被滥用,容易被吊销。不建议依赖。
- 自签工具:用 AltStore、Sideloadly 这类工具,配合 Apple ID 签名。本质还是免费证书,只是自动化了重签流程。
对于 Madeira 这种需要频繁调试的项目,我建议至少用付费开发者账号,省去每 7 天重签的麻烦。如果只是尝鲜,免费证书也能跑,但要接受频繁重签。
3.3 JIT 权限的获取:三种可行路径
JIT 是 Madeira 在 iOS 上最核心的技术门槛。没有 JIT,FEX-Emu 的动态翻译就跑不起来。目前可行的路径有三条:
第一条是开启开发者模式后用调试器附加。Xcode 调试时会给应用附加get-task-allow权限,这个权限允许 JIT。但这种方式要求应用一直处于调试状态,不能独立运行。
第二条是使用特定的 JIT 启用工具。社区里有工具可以在开发者模式下给应用授予 JIT 权限,原理是利用调试端口或者特定的系统调用。这类工具需要设备开启开发者模式,且每次启动应用都要重新授权。
第三条是把 FEX-Emu 改成 AOT 模式。AOT(提前编译)不需要运行时生成代码,但需要在构建时就把 x86 代码翻译好。这对 Madeira 来说不太现实,因为 Windows 程序是动态加载的,你没法提前知道要翻译哪些代码。
实际中,Madeira 类项目大多走第二条路,配合开发者模式使用。这也是为什么 "ios开发者模式" 是热搜词——不开这个,后面全都免谈。
注意:JIT 权限的获取方式会随 iOS 版本变化,新版本往往会收紧。建议在动手前先确认当前 iOS 版本是否还有可用的 JIT 启用方案。
4. 实操流程:从零搭建 Madeira 运行环境
4.1 环境准备清单
在开始之前,你需要准备这些东西:
- 一台运行 macOS 的电脑(黑苹果也行,但驱动问题会额外增加麻烦)
- Xcode,版本要能支持你目标 iOS 设备的系统版本
- 目标 iOS 设备,建议 iPhone 12 及以上或者 iPad Air 4 及以上,A14/M1 芯片的翻译性能明显更好
- Apple 开发者账号(免费或付费均可)
- Madeira 相关的源码仓库,包括 Wine 的 iOS 移植分支、FEX-Emu 的 ARM64 构建、DXMT 的 Metal 后端
- 一个 Windows 程序的测试样本,建议从简单的记事本类工具开始,不要一上来就上大型游戏
4.2 编译 Wine 的 iOS 版本
Wine 的 iOS 移植不是官方主线,需要找社区的分支。编译流程大致如下:
# 克隆 Wine 的 iOS 移植分支 git clone --branch ios-port https://example.com/wine-ios.git cd wine-ios # 配置编译选项,关键是 --host 指定 ARM64 苹果目标 ./configure \ --host=aarch64-apple-darwin \ --with-coreaudio \ --with-metalshader \ --disable-tests \ --prefix=/path/to/ios-wine-build # 编译,建议用 make -j 加速 make -j$(sysctl -n hw.ncpu) make install这里有几个关键配置项要解释。--host=aarch64-apple-darwin告诉构建系统目标是 ARM64 苹果平台。--with-metalshader启用 Metal 着色器后端,这是 DXMT 依赖的。--disable-tests跳过测试,因为 iOS 上很多测试跑不了。
编译过程中最常见的错误是头文件缺失和链接错误。iOS SDK 的路径需要正确设置,通常通过xcrun --sdk iphoneos --show-sdk-path获取。如果遇到Undefined symbols错误,多半是某个系统库没链接上,需要手动在 Makefile 里加-framework参数。
4.3 构建 FEX-Emu 的 ARM64 版本
FEX-Emu 本身是给 Linux ARM 设备用的,移植到 iOS 需要改不少东西。核心改动包括:
- 把 Linux 特有的系统调用替换成 iOS 的对应实现
- 处理 JIT 内存分配,用
mmap加MAP_JIT标志 - 适配 iOS 的线程模型,pthread 的某些行为在 iOS 上有差异
# 克隆 FEX-Emu git clone https://example.com/FEX-Emu.git cd FEX-Emu # 用 CMake 配置,指定 iOS 工具链 cmake -B build-ios \ -DCMAKE_TOOLCHAIN_FILE=cmake/ios.toolchain.cmake \ -DCMAKE_OSX_ARCHITECTURES=arm64 \ -DCMAKE_OSX_DEPLOYMENT_TARGET=16.0 \ -DENABLE_JIT=ON \ -DENABLE_AOT=OFF cmake --build build-ios -j$(sysctl -n hw.ncpu)ENABLE_JIT=ON是必须的,ENABLE_AOT=OFF是因为 iOS 上 AOT 不实用。CMAKE_OSX_DEPLOYMENT_TARGET设成 16.0 是因为开发者模式从 iOS 16 才强化,低于这个版本很多机制不一样。
4.4 集成 DXMT 并配置 Metal 后端
DXMT 的集成相对直接,因为它本身就是为苹果平台设计的。关键是把 DXMT 的 dll 编译出来,放到 Wine 的lib/wine/aarch64-windows/目录下,然后在 Wine 的注册表里配置 Direct3D 的替代。
# 编译 DXMT git clone https://example.com/dxmt.git cd dxmt meson setup build-ios \ --cross-file cross-ios.txt \ -Dbuildtype=release ninja -C build-ios编译完成后,把生成的d3d11.dll、dxgi.dll等文件复制到 Wine 的对应目录。然后在WINEPREFIX的注册表里设置:
[HKEY_CURRENT_USER\Software\Wine\DllOverrides] "d3d11"="native" "dxgi"="native"这样 Wine 就会优先加载 DXMT 的实现,而不是内置的 Direct3D 翻译层。
4.5 打包成 iOS 应用并签名
最后一步是把所有东西打包成一个 iOS 应用。你需要一个 Xcode 工程,把 Wine、FEX-Emu、DXMT 编译成静态库或者动态库链接进去,然后写一个简单的 Objective-C 或 Swift 外壳来启动。
// 简化的启动代码 import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 初始化 Wine 环境 let prefixPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("wineprefix") setenv("WINEPREFIX", prefixPath.path, 1) // 启动 Wine 主循环 DispatchQueue.global().async { wine_main(CommandLine.argc, CommandLine.unsafeArgv) } return true } }签名时,在 Xcode 的 Signing & Capabilities 里选择你的开发者账号,开启get-task-allow(调试用)或者配置好发布证书。如果要用 JIT,还需要在 entitlements 文件里加上对应的权限。
5. 常见问题与排查技巧实录
5.1 Wine 乱码问题:字体与 locale 的双重坑
"wine 乱码" 和 "wine 栏是乱码" 是热搜里出现最多的词,说明这是高频问题。乱码的根源通常有两个:字体缺失和 locale 配置错误。
字体方面,Wine 默认会去找系统字体,但 iOS 沙盒里没有 Windows 常用的宋体、黑体。解决办法是把字体文件(比如simsun.ttc、msyh.ttf)复制到WINEPREFIX/drive_c/windows/Fonts/目录下,然后在注册表里配置字体替换:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes] "MS Shell Dlg"="SimSun" "MS Shell Dlg 2"="SimSun" "Tahoma"="SimSun"locale 方面,Wine 需要正确的LANG和LC_ALL环境变量。在 iOS 上,由于系统 locale 和 Windows 的对应关系不直接,建议在启动脚本里显式设置:
export LANG=zh_CN.UTF-8 export LC_ALL=zh_CN.UTF-8如果还是乱码,检查 Wine 的wineboot是否正常执行,有时候 prefix 初始化不完整会导致字体注册表没写进去。
5.2 Gecko 下载失败:网络与路径的双重限制
"wine gecko官方正版下载" 这个热搜词说明很多人卡在 Gecko 安装上。Wine 在初始化 prefix 时会提示下载 Gecko 和 Mono,但 iOS 沙盒里这个自动下载经常失败。
手动解决的办法是:先在桌面端下载好 Gecko 的 msi 包,然后放到 iOS 应用的沙盒目录里,再通过 Wine 的msiexec手动安装:
wine msiexec /i wine_gecko-2.47.4-x86.msi注意 Gecko 的版本要和 Wine 的版本匹配,版本不对会安装失败。另外,iOS 上路径要用应用沙盒内的绝对路径,不能用~或者相对路径。
5.3 着色器编译卡顿:DXMT 缓存配置
第一次运行游戏时,DXMT 需要把大量 HLSL 着色器翻译成 Metal,这个过程可能持续几分钟。如果每次启动都卡,说明缓存没生效。
DXMT 的缓存默认在WINEPREFIX/drive_c/users/<用户名>/Temp/dxmt_cache/下。检查这个目录是否有写入权限,以及缓存大小限制是否设得太小。可以在环境变量里调整:
export DXMT_SHADER_CACHE_SIZE=1073741824 # 1GB export DXMT_SHADER_CACHE_PATH=/path/to/cache如果缓存目录在 iOS 的临时目录下,系统可能会定期清理,建议放到 Documents 目录里,避免被回收。
5.4 应用启动闪退:签名与权限排查
闪退是 iOS 上最常见的问题,排查思路如下:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 启动即闪退 | 签名无效 | 检查证书是否过期,重新签名 |
| 启动后黑屏 | JIT 未启用 | 确认开发者模式已开,检查 entitlements |
| 运行中崩溃 | 内存不足 | 查看崩溃日志,iOS 对单应用内存有限制 |
| 特定操作崩溃 | API 未实现 | 查看 Wine 日志,确认哪个调用失败 |
崩溃日志可以通过 Xcode 的 Devices and Simulators 窗口查看,或者用idevicecrashreport工具导出。重点看Exception Type和Termination Reason,前者告诉你崩溃类型(比如EXC_BAD_ACCESS是内存访问错误),后者告诉你系统为什么终止应用(比如CODESIGNING是签名问题)。
5.5 性能调优:从 30 帧到 60 帧的实操经验
性能是 iOS 上跑 Wine 的另一个大问题。我实测下来,几个有效的调优手段:
- 关闭不必要的 Wine 服务:
wineboot会启动一些后台服务,在 iOS 上这些服务很占资源。可以在启动参数里加-services来精简。 - 调整 FEX-Emu 的翻译块大小:翻译块越大,缓存命中率越高,但首次翻译延迟也越大。默认值通常够用,如果游戏卡顿可以尝试调大。
- DXMT 的异步着色器编译:开启异步编译可以避免着色器编译阻塞渲染线程,代价是首次出现某个效果时可能短暂闪烁。
- 降低分辨率:iOS 设备的屏幕分辨率很高,但 Wine 渲染时可以用较低的内部分辨率,然后放大输出。这能显著提升帧率。
# 设置 Wine 虚拟桌面分辨率 export WINEDLLOVERRIDES="d3d11=n,b" wine reg add "HKEY_CURRENT_USER\Software\Wine\Explorer\Desktops" /v Default /t REG_SZ /d 1280x7206. 分发与上架:从自用到分享的路径
6.1 自签分发的现实约束
如果你只是自己用,自签是最简单的路径。但要注意,免费证书 7 天过期,付费证书一年过期,企业证书随时可能被吊销。对于 Madeira 这种需要持续更新的项目,建议至少用付费账号。
自签工具方面,AltStore 和 Sideloadly 是主流选择。AltStore 的优势是可以在设备上自动重签,不需要每次连电脑。Sideloadly 的优势是支持更多签名方式,包括企业证书。
6.2 上架 App Store 的可行性分析
"ios app开发完毕如何上架" 这个热搜词说明有人想走正规上架路线。但 Madeira 这类项目上架 App Store 的难度极大,原因有几个:
- JIT 权限:App Store 审核明确禁止应用在运行时生成可执行代码,除非是 JavaScriptCore 这类系统允许的例外。FEX-Emu 的 JIT 直接撞这条红线。
- 沙盒限制:Wine 需要访问大量文件系统路径,App Store 的沙盒比开发者模式下的沙盒更严。
- 内容审核:Windows 程序本身可能包含违规内容,苹果不会允许一个"能运行任意 Windows 程序"的应用上架。
所以现实路径是:自签分发、企业内部分发、或者 TestFlight 小范围测试。上架 App Store 基本不用想。
6.3 替代方案:远程串流与云游戏
如果本地跑不动,还有一个替代思路是远程串流。把 Windows 程序跑在一台性能足够的 PC 或者服务器上,然后通过串流协议把画面传到 iOS 设备。这种方式不需要 JIT,不需要签名折腾,但依赖网络质量。
串流方案的选择上,Moonlight(配合 Sunshine)是延迟最低的,Parsec 的画质更好但延迟略高。iOS 上两者都有客户端,配置也不复杂。对于只是想玩 Windows 游戏的人来说,串流可能是比本地 Wine 更实际的选择。
7. 一些实操心得与后续扩展方向
折腾 Madeira 这套东西,我最大的体会是:iOS 的封闭性不是靠一个技术点突破的,而是靠一整条链路的配合。开发者模式、签名、JIT、沙盒路径、Metal 后端,任何一个环节出问题,整个东西就跑不起来。所以排查问题时要有全局视角,不要只盯着一个组件。
另一个心得是从简单程序开始。我一开始就想跑大型游戏,结果卡在着色器编译上一整天。后来换成一个简单的 Win32 记事本程序,十分钟就跑起来了,然后再逐步增加复杂度。这个渐进式的思路能帮你快速定位问题出在哪一层。
后续可以扩展的方向有几个。一是AOT 翻译的优化,如果能提前把常用 Windows 库翻译好,运行时就不需要 JIT,能绕过 iOS 的限制。二是Metal 后端的深度优化,DXMT 目前对 D3D12 的支持还不完整,如果能补上,能跑的游戏会更多。三是与 iOS 原生能力的结合,比如用 iOS 的触控、陀螺仪、手柄支持来增强游戏体验,这部分 Wine 本身不提供,需要在外壳层做。
最后分享一个小技巧:Wine 的日志级别可以通过WINEDEBUG环境变量控制。排查问题时设成WINEDEBUG=+all能看到所有调用,但日志量巨大,建议针对性开启,比如WINEDEBUG=+d3d11,+dxgi只看图形相关。日志输出到文件用&> wine.log,方便后续分析。