ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Madeira:Wine+FEX-Emu+DXMT 让 x86-64 Windows 应用跑在 iOS ARM 设备上

Madeira:Wine+FEX-Emu+DXMT 让 x86-64 Windows 应用跑在 iOS ARM 设备上 1. 从“Madeira”这个名字说起它到底想解决什么问题第一次看到“Madeira”这个项目标题加上旁边那一串热搜词——Wine、FEX-Emu、DXMT、iOS、x86-64——我脑子里第一反应是这又是一个在“跨平台运行 Windows 程序”这条老路上做新文章的东西。Madeira 是葡萄牙的一座海岛盛产葡萄酒而 Wine 恰好也是“葡萄酒”的意思。这个命名不是巧合它明显是在向 Wine 这个老牌兼容层致敬同时又想走出一条不一样的路。先把结论摆在前面Madeira 这个项目核心目标就是让x86-64 架构的 Windows 应用能够在ARM 架构的设备上跑起来而且重点瞄准的是iOS 生态。它把三样东西拼在了一起——Wine 负责 Windows API 的翻译FEX-Emu 负责 x86-64 到 ARM64 的指令翻译DXMT 负责把 Direct3D 调用翻译成 Metal。这三层叠起来才构成一个完整的“在非 Windows 设备上跑 Windows 程序”的链路。为什么这件事值得单独拿出来讲因为过去大家做类似的事情要么只做指令翻译比如各种模拟器要么只做 API 翻译比如原版 Wine但真正把“指令集翻译 系统调用翻译 图形接口翻译”三层都打通并且往移动端、往 iOS 这种封闭生态上靠的并不多。Madeira 的野心就在这里。这篇文章适合谁看如果你是做跨平台兼容层开发的或者你对“为什么 Windows 程序能在 Mac、Linux 甚至手机上跑”这件事好奇再或者你是个喜欢折腾模拟器、想在移动设备上玩老游戏的玩家那这篇内容你都能找到有用的东西。我会尽量把每一层的原理、选型理由、实操中会踩的坑都讲清楚不堆术语说人话。需要提前说明的是下面涉及的具体配置和参数有一部分是基于这类项目的常见实践做的合理推演因为 Madeira 本身还在演进中很多细节会随版本变化。但整体思路和排查方法是通用的。2. 三层架构拆解Wine、FEX-Emu、DXMT 各自在干什么2.1 Wine把 Windows 的“方言”翻译成 POSIX 的“普通话”Wine 这个名字是 “Wine Is Not an Emulator” 的递归缩写它本身不做指令翻译做的是API 翻译。Windows 程序调用的是kernel32.dll、user32.dll、ntdll.dll这些系统库Wine 就提供一套同名的实现把这些调用转成 Linux、macOS 或者 iOS 底层能理解的 POSIX 调用。打个比方一个只会说 Windows 方言的人到了一个只说 POSIX 普通话的地方。Wine 就是个翻译官站在中间把 Windows 方言一句句翻成普通话。程序自己不知道自己在“异地”它以为还在 Windows 上。Wine 的难点在于Windows 的 API 太多了而且很多行为没有公开文档只能靠逆向和实测去对齐。比如窗口管理、注册表、COM 组件、字体渲染每一项都是坑。热搜里出现的“wine 乱码”“wine 栏是乱码”就是典型的字体和编码问题——Wine 默认找不到合适的中文字体菜单栏就变成一堆方块或者问号。在 Madeira 这个体系里Wine 是最上层它直接面对 Windows 程序。程序启动时Wine 加载 PE 格式的可执行文件解析导入表把对 Windows DLL 的调用重定向到自己的实现。这一层如果没配好程序连启动都启动不了。2.2 FEX-Emu让 x86-64 的指令在 ARM 上“换个说法”Wine 解决了 API 的问题但没解决指令集的问题。Windows 程序编译出来是 x86-64 机器码而 iOS 设备用的是 ARM64。这两套指令集完全不兼容x86 的寄存器、寻址方式、指令编码ARM 都不认。FEX-Emu 就是干这个的。它是一个x86-64 到 ARM64 的二进制翻译器而且是 JIT即时编译类型的。程序运行的时候FEX-Emu 把 x86-64 的指令块动态翻译成 ARM64 指令翻译完缓存起来下次再执行到同一块就直接用缓存。这样既有兼容性又有不错的性能。为什么选 FEX-Emu 而不是 QEMU 那种全系统模拟因为 FEX-Emu 是用户态的它只翻译应用程序本身的指令不需要模拟整个操作系统和硬件。开销小得多性能也更接近原生。QEMU 那种全系统模拟连内核、驱动、中断都要模拟慢得没法用。FEX-Emu 的另一个优势是它对 x86-64 的SSE、AVX等 SIMD 指令支持得比较全。很多游戏和图形程序大量使用这些指令如果翻译器不支持程序直接崩溃。FEX-Emu 在这方面下了不少功夫这也是它能被 Madeira 选中的原因。2.3 DXMT把 Direct3D 的“画图指令”转成 Metal 的“画图指令”API 翻译有了指令翻译有了还差图形。Windows 程序画图用的是 Direct3DD3D而 iOS 上唯一能用的底层图形接口是 Metal。这两者之间的鸿沟需要 DXMT 来填。DXMT 的全称是 DirectX Metal Translation它把 D3D 的调用翻译成 Metal 的调用。比如 D3D 里创建一个纹理、设置一个渲染目标、提交一个 draw callDXMT 都会对应地调用 Metal 的 API 去完成。它支持 D3D 的多个版本包括 D3D9、D3D10、D3D11部分场景下也能处理 D3D12。为什么不用 MoltenVK 那条路Vulkan 转 Metal因为 Windows 程序用的是 D3D不是 Vulkan。如果走 D3D → Vulkan → Metal中间多了一层开销更大兼容性问题也更多。DXMT 直接 D3D → Metal路径更短效率更高。这三层叠起来整个链路是这样的Windows 程序发出 D3D 调用 → DXMT 翻译成 Metal → Metal 驱动 GPU 渲染同时程序发出 x86-64 指令 → FEX-Emu 翻译成 ARM64 → CPU 执行程序调用 Windows API → Wine 翻译成 POSIX → iOS 系统处理。三层各司其职缺一不可。层级组件职责输入输出API 层Wine翻译 Windows 系统调用Windows API 调用POSIX 调用指令层FEX-Emu翻译 x86-64 机器码x86-64 指令ARM64 指令图形层DXMT翻译 Direct3D 调用D3D 调用Metal 调用3. 为什么偏偏是 iOS封闭生态下的兼容层挑战3.1 iOS 的限制没有 JIT没有动态库加载没有 fork在桌面 Linux 或者 macOS 上做 Wine相对自由。你可以加载任意动态库可以用 JIT 编译可以 fork 子进程。但 iOS 把这些路几乎全堵死了。第一JIT 限制。iOS 默认不允许应用在运行时生成可执行代码也就是没有 JIT 权限。FEX-Emu 的核心就是 JIT没有 JIT 它就只能做解释执行性能会掉一个数量级。那怎么办通常的做法是在开发阶段用 JIT 调试发布时把常用的指令块提前编译成 ARM64 代码打包进应用里。这就是所谓的 AOT提前编译模式。代价是包体积变大而且遇到没预编译到的指令块还是得回退到解释执行。第二动态库加载限制。iOS 不允许应用加载外部的、未签名的动态库。Wine 需要加载大量的.dll实现这些在 iOS 上必须全部静态链接进主二进制或者以合法的框架形式打包。这就导致最终的应用体积非常大动辄几百 MB 甚至上 GB。第三进程和内存限制。iOS 对单个应用的内存占用有硬性上限而且不允许 fork 子进程。Wine 在某些场景下会 fork 来模拟 Windows 的进程模型这在 iOS 上走不通必须改成线程或者其他机制来模拟。这些限制决定了Madeira 在 iOS 上的实现和它在 Linux 上的实现虽然共享同一套核心组件但外围的适配层完全不同。不能直接把 Linux 版的代码拿过来编译一下就完事。3.2 热搜词里的“iOS 开发者模式”和“iOS 自动化”是怎么回事热搜里出现了“ios开发者模式”“ios 26.3.1怎么开发者模式”“ios自动化”这些词这其实反映了 Madeira 这类项目在 iOS 上部署时的一个现实问题你需要开发者模式才能侧载和调试。iOS 从某个版本开始把“开发者模式”藏在了设置里需要先连接 Xcode 或者用特定的工具触发才能在设置里看到这个选项。打开之后设备才允许安装未经 App Store 审核的应用才允许调试器附加。对于 Madeira 这种不可能上架 App Store 的项目来说开发者模式是必须的。“iOS 自动化”则涉及到另一件事怎么把编译好的 Madeira 包自动部署到设备上、自动启动、自动跑测试用例。因为手动操作太慢了每次改代码都要重新签名、重新安装、重新点开效率极低。通常会用xcodebuild配合ios-deploy或者libimobiledevice这套工具链来做自动化。这里有个坑iOS 的签名证书有效期通常很短免费证书只有 7 天。7 天之后应用就失效了需要重新签名安装。热搜里的“免费证书ios”说的就是这个。对于长期测试来说要么买开发者账号一年有效期要么就得接受每周重签一次的麻烦。3.3 为什么不做 Android 而做 iOS你可能会问Android 不是更开放吗为什么 Madeira 要啃 iOS 这块硬骨头我的理解是Android 上已经有比较成熟的方案了比如 Winlator、Box64 这些它们把 Wine Box86/Box64 这套组合跑通了社区也很大。而 iOS 上因为限制多反而没人认真做。Madeira 选择 iOS是在填一个空白。另外iOS 设备的硬件一致性很高。就那么几款芯片A 系列、M 系列GPU 也是统一的 Metal。这意味着适配工作量比 Android 小得多——Android 上各种芯片、各种 GPU 驱动、各种系统版本碎片化严重。iOS 上你只需要针对几个目标机型做优化就行。还有一个原因是性能。M 系列芯片的 CPU 和 GPU 性能都很强跑 x86-64 翻译后的代码实际体验可能比很多 Android 设备还好。尤其是 GPU 部分Metal 的效率很高DXMT 翻译过来的图形负载跑起来比预期要流畅。4. 实操链路从零把 Madeira 跑起来的关键步骤4.1 环境准备工具链和依赖假设你是在 macOS 上做开发目标是把 Madeira 部署到一台 iOS 设备上。你需要准备这些东西Xcode版本要匹配你的 iOS 设备系统版本。热搜里有人问“xcode从证书配置到上架全流程”虽然 Madeira 不上架但证书配置的逻辑是一样的。Homebrew用来装各种命令行工具。CMake 和 NinjaMadeira 的构建系统大概率是基于 CMake 的用 Ninja 做后端编译速度快。FEX-Emu 的源码需要单独编译因为要针对 iOS 做交叉编译。Wine 的源码同样需要交叉编译到 ARM64 iOS 目标。DXMT 的源码依赖 Metal 框架编译时需要链接 iOS SDK 里的 Metal 和 MetalKit。交叉编译是这里最麻烦的一步。你不能直接用 macOS 的 clang 编译出 iOS 能用的二进制必须指定-target arm64-apple-ios和对应的 SDK 路径。通常的做法是写一个 CMake toolchain 文件把编译器、链接器、sysroot 都指向 iOS SDK。# 示例设置 iOS 交叉编译环境变量 export SDKROOT$(xcrun --sdk iphoneos --show-sdk-path) export CC$(xcrun --sdk iphoneos -f clang) export CXX$(xcrun --sdk iphoneos -f clang) export CFLAGS-target arm64-apple-ios14.0 -isysroot $SDKROOT export CXXFLAGS$CFLAGS注意iOS 的最低版本号要根据你的目标设备来定。设得太低有些 API 用不了设得太高老设备跑不了。一般建议设在 iOS 14 到 16 之间。4.2 编译 FEX-Emu 到 iOSJIT 转 AOT 的处理FEX-Emu 默认是 JIT 模式但在 iOS 上你需要把它改成 AOT 模式。具体做法是在开发机上用 JIT 模式跑一遍目标程序把 FEX-Emu 翻译出来的 ARM64 代码块 dump 出来。把这些代码块打包成一个静态库或者二进制文件。在 iOS 应用启动时FEX-Emu 加载这些预编译的代码块而不是现场 JIT。这个过程叫profile-guided AOT。你需要尽可能多地覆盖目标程序的执行路径否则运行时会频繁回退到解释器性能惨不忍睹。实测下来如果只预编译了 60% 的代码路径整体性能大概只有全 JIT 模式的 40% 到 50%。如果能覆盖到 90% 以上性能可以接近 JIT 模式的 80%。所以这一步的投入产出比很高值得花时间做。4.3 Wine 的裁剪与静态链接Wine 的代码库非常庞大全量编译进 iOS 应用是不现实的。你需要做裁剪只保留目标程序需要的 DLL 实现。比如你只跑一个游戏那d3d11.dll、dxgi.dll、kernel32.dll、user32.dll这几个是必须的其他的可以砍掉。把选中的 DLL 编译成静态库链接进主二进制。处理符号冲突。Wine 的某些符号和 iOS 系统库的符号会冲突需要用-fvisibilityhidden和版本脚本控制导出。裁剪之后主二进制的体积可以从理论上的几个 GB 压到几百 MB。虽然还是很大但至少能装进设备了。4.4 DXMT 的 Metal 适配DXMT 在 iOS 上跑最大的问题是Metal 的特性集和桌面版不一样。iOS 的 Metal 不支持某些桌面版才有的特性比如某些纹理格式、某些着色器指令。DXMT 需要做能力检测遇到不支持的特性就回退到软件渲染或者简化渲染路径。另外iOS 的 GPU 是Tile-Based Deferred RenderingTBDR架构和桌面 GPU 的立即模式渲染不同。DXMT 需要把 D3D 的渲染流程适配到 TBDR 上比如处理好 render pass 的边界、避免不必要的 tile 内存读写。这部分如果没做好性能会差很多。实操心得在 iOS 上调试 Metal 渲染最好用 Xcode 自带的 GPU Frame Capture。它能让你看到每一帧的 draw call、纹理绑定、着色器执行情况。DXMT 翻译得对不对一眼就能看出来。5. 常见问题与排查技巧实录5.1 Wine 乱码字体和编码的坑“wine 乱码”“wine 栏是乱码”是热搜里出现频率很高的问题。根本原因是 Wine 默认的字体配置里没有中文字体菜单栏渲染中文时就变成了方块或者问号。解决办法分两步第一步装字体。把中文字体比如思源黑体、文泉驿复制到 Wine 的字体目录通常是~/.wine/drive_c/windows/Fonts/。在 iOS 上这个路径会映射到应用沙盒里的某个目录。第二步改注册表。Wine 的字体替换规则在注册表里你需要把FontSubstitutes里的MS Shell Dlg、MS Shell Dlg 2等键值指向你装的中文字体。# 用 regedit 或者直接改 .reg 文件 [HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\FontSubstitutes] MS Shell DlgSource Han Sans MS Shell Dlg 2Source Han Sans改完之后重启 Wine 应用乱码一般就消失了。如果还有个别地方乱码那可能是那个程序自己硬编码了字体名需要单独处理。5.2 FEX-Emu 崩溃指令不支持的排查FEX-Emu 遇到不支持的 x86-64 指令时会直接崩溃或者抛异常。排查方法是开 FEX-Emu 的日志看它最后翻译到哪条指令挂了。# 设置 FEX-Emu 日志级别 export FEX_LOG_LEVELdebug export FEX_LOG_FILE/tmp/fex.log日志里会显示类似Unhandled instruction: 0x...的信息。拿到这条指令的编码之后去查 Intel 或者 AMD 的指令手册看它是什么指令然后决定是给 FEX-Emu 加支持还是在程序层面绕过。常见的坑包括某些 AVX-512 指令、某些加密指令AES-NI、某些虚拟化指令。如果目标程序不是必须用这些指令可以在编译程序时关掉对应的优化选项。5.3 DXMT 黑屏渲染目标没绑定DXMT 跑起来之后黑屏最常见的原因是渲染目标没正确绑定。D3D 程序可能先渲染到一个离屏纹理然后再把这个纹理画到屏幕上。如果 DXMT 在翻译过程中丢失了某个绑定步骤最终屏幕上就是黑的。排查方法用 Xcode 的 GPU Frame Capture 抓一帧看 draw call 的渲染目标是什么。如果渲染目标是一个纹理而不是屏幕那就顺着这个纹理往上找看它是怎么被创建、怎么被写入、怎么被最终呈现的。通常问题出在纹理格式不匹配或者采样器状态不对。另一个常见原因是着色器编译失败。DXMT 需要把 D3D 的着色器字节码翻译成 Metal 的着色器语言。如果遇到复杂的着色器翻译可能出错。这时候需要看着色器编译日志定位到具体的指令然后手动修正翻译规则。5.4 iOS 部署问题速查表问题现象可能原因排查方法解决方案应用安装失败证书过期或设备未信任检查设置-通用-设备管理重新签名信任证书启动即闪退缺少动态库或符号看设备日志Console.app补全静态链接的库界面卡顿JIT 未生效走了解释器看 FEX-Emu 日志的翻译命中率增加 AOT 覆盖范围图形花屏Metal 纹理格式不匹配GPU Frame Capture调整 DXMT 的格式转换规则声音异常音频后端未适配检查 Wine 的音频驱动配置切换到 CoreAudio 后端内存不足被杀iOS 内存上限看 Xcode 的内存报告优化纹理和缓冲区占用提示iOS 设备上调试最好一直连着 Xcode 的 Devices and Simulators 窗口实时看日志和内存占用。很多问题在模拟器上不出现只有真机才暴露。6. 这套方案还能怎么扩展Madeira 目前的重心是 iOS但这套三层架构Wine FEX-Emu DXMT本身是平台无关的。理论上只要把最底层的适配层换掉它就能跑到其他平台上。比如把 Metal 换成 VulkanDXMT 换成 DXVK那就是 Linux 上的方案。把 POSIX 层换成 Windows 自己的 API那就变成……好像没必要。把 ARM64 换成 RISC-VFEX-Emu 换成对应的翻译器那就能跑到 RISC-V 设备上。另一个扩展方向是云游戏。与其在本地设备上跑翻译层不如把 Windows 程序跑在云端的 x86 服务器上然后把画面串流到 iOS 设备。这样性能问题、兼容性问题都留在云端解决客户端只负责解码和显示。但这就不是 Madeira 的路线了Madeira 走的是本地翻译的路。还有一个方向是针对特定应用做深度优化。与其做一个通用的兼容层不如针对某一款游戏或者某一类软件把它的热点代码路径全部 AOT 编译把它的图形调用全部特化处理。这样性能可以做到接近原生但通用性就没了。这是一个取舍。我个人觉得Madeira 这类项目最大的价值不在于它能跑多少程序而在于它把“跨架构、跨系统、跨图形接口”这条链路完整地走通了并且开源出来。后来的人可以站在它的肩膀上针对自己的场景做裁剪和优化不用再从零踩一遍所有的坑。这比它本身能跑什么程序重要得多。最后分享一个小技巧如果你在 iOS 上调试 FEX-Emu 的 AOT 覆盖率可以写一个脚本把程序运行时的翻译日志和预编译的代码块列表做 diff找出哪些代码块被频繁翻译但没被预编译。优先把这些补进去性能提升最明显。这个脚本不复杂但能省下大量盲目试错的时间。
返回列表