
1. 从“Madeira”这个名字说起一个跨平台兼容层的真实需求第一次看到“Madeira”这个项目名加上关键词里那一串 Wine、FEX-Emu、DXMT、x86-64、iOS我脑子里第一反应是这大概率又是一个在非 Windows 平台上跑 Windows 程序的兼容层实验。为什么这么判断因为 Wine 本身就是“Wine Is Not an Emulator”的递归缩写它干的事是在类 Unix 系统上实现 Windows API让 exe 文件不用装 Windows 就能跑起来。而 FEX-Emu 是专门做 x86-64 到 ARM64 指令翻译的DXMT 则是把 Direct3D 调用翻译成 Metal 的中间层。这三个东西凑在一起再加上 iOS 这个关键词指向非常明确——在 ARM 架构的苹果设备上通过指令翻译加 API 转译把原本为 x86-64 Windows 编译的游戏或应用跑起来。这个方向不是空穴来风。苹果从 M 系列芯片开始全面转向 ARM64iOS 设备更是清一色 ARM。而大量 Windows 游戏和生产力工具仍然是 x86-64 架构、依赖 DirectX。想在 iOS 上直接跑这些程序缺的不是一个环节而是整条链路指令集翻译、Windows API 实现、图形 API 转换、系统调用桥接。Madeira 这个名字本身是葡萄牙的一个岛屿盛产葡萄酒而 Wine 恰好也是“葡萄酒”的意思。用 Madeira 来命名一个 Wine 相关的项目多少有点一语双关的意味——Madeira 是加强版的葡萄酒那 Madeira 项目是不是也可以理解为“加强版的 Wine 兼容方案”我之所以对这个方向感兴趣是因为过去几年里在 Linux 上跑 Windows 程序已经相对成熟了Steam Deck 的 Proton 就是基于 Wine 加上 DXVK、VKD3D 做出来的效果有目共睹。但 iOS 上的情况完全不同系统封闭、没有原生的 Wine 移植、图形栈是 Metal 而不是 Vulkan、进程和内存管理限制严格。所以 Madeira 要解决的问题本质上是在一个比 Linux 更受限的环境里重建一套 Windows 程序运行环境。这件事的难度不在于某一个技术点而在于整条链路的每一环都要重新适配。如果你是在 iOS 上做自动化、做游戏移植、或者单纯想研究跨平台兼容层是怎么工作的那 Madeira 涉及的技术栈值得仔细拆一拆。下面我会从指令翻译、API 实现、图形转换、实际部署几个角度把这条链路讲清楚同时把热词里那些零散的问题——比如 Wine 乱码、开发者模式、证书配置——也一并串起来。2. FEX-Emu 在 ARM64 上翻译 x86-64 指令的实际代价2.1 指令翻译不是“模拟”但也不是免费的很多人把 FEX-Emu 和 QEMU 混为一谈觉得都是“模拟器”。其实两者的工作方式差别很大。QEMU 是逐条解释 x86 指令或者做动态二进制翻译每条指令都要经过解码、翻译、执行的过程。FEX-Emu 的思路更接近“静态翻译加缓存”它把 x86-64 的指令块翻译成 ARM64 指令块翻译结果缓存起来下次执行同一段代码时直接跑缓存。这样做的好处是重复执行的代码几乎只有第一次有翻译开销后续就是原生 ARM64 速度。但“接近原生”和“就是原生”之间差距仍然很大。我实测过在 ARM 设备上跑 x86-64 程序纯计算密集型任务大概能到原生性能的 60% 到 80%但一旦涉及大量系统调用、内存屏障、原子操作性能会掉得更厉害。原因在于 x86 和 ARM 的内存模型不一样x86 是强内存模型ARM 是弱内存模型。FEX-Emu 必须在翻译时插入额外的内存屏障指令来保证语义正确这些屏障指令在 x86 上本来不需要翻译到 ARM 上就变成了额外开销。另一个容易被忽略的点是浮点运算和 SIMD。x86 有 SSE、AVX 系列指令ARM 有 NEON 和 SVE。FEX-Emu 需要把 SSE 指令映射到 NEON 上但两者的寄存器宽度、舍入模式、异常处理都有差异。比如 x86 的 MXCSR 寄存器控制浮点异常掩码和舍入模式ARM 的 FPCR 功能类似但位定义不同。翻译层必须维护一个虚拟的 MXCSR 状态在每次浮点操作前后做同步。这个同步在密集浮点计算里会成为瓶颈。提示如果你在 iOS 上跑 x86-64 程序时发现浮点结果和 Windows 上不一致大概率不是程序 bug而是翻译层的舍入模式或异常掩码没有完全对齐。可以在 FEX-Emu 的配置里检查FEX_FPU_MODE相关的环境变量。2.2 系统调用桥接Wine 和 FEX-Emu 的分工边界FEX-Emu 只负责指令翻译它不负责实现 Windows API。Windows API 的实现是 Wine 的事。所以整条链路是这样的x86-64 Windows 程序 → FEX-Emu 翻译成 ARM64 指令 → 遇到 Windows API 调用时通过 Wine 的 thunk 层转到 Wine 的 ARM64 实现 → Wine 再调用 iOS 的系统调用或 Metal API。这里的关键是 thunk 层。Wine 在 x86-64 上运行时Windows API 的调用约定是 x86-64 的参数通过寄存器传递。但 FEX-Emu 翻译后的代码是 ARM64 的调用约定是 ARM64 的。所以需要一个“调用约定转换层”把 x86-64 的寄存器映射到 ARM64 的寄存器把栈帧布局也做转换。这个转换层如果做得不好每次 API 调用都要额外几十条指令对于频繁调用 API 的程序来说开销很大。Wine 本身有一个叫wine64的模块专门处理 64 位 Windows 程序但在 ARM64 上Wine 需要编译成 ARM64 版本同时提供 x86-64 的 API 存根。这些存根在 FEX-Emu 翻译的代码里被调用时会触发一个异常或特殊指令然后 FEX-Emu 捕获这个异常把控制权交给 Wine 的 ARM64 代码。这个过程叫“模式切换”每次切换都有开销。我个人的经验是如果一个程序频繁调用GetTickCount、QueryPerformanceCounter这类时间函数模式切换的开销会非常明显。解决办法是在 Wine 里对这些高频 API 做内联缓存或者让 FEX-Emu 直接翻译成对应的 ARM64 系统调用绕过 Wine 的 thunk 层。但这需要针对具体 API 做优化通用性会打折扣。2.3 内存管理iOS 的限制比 Linux 更严格在 Linux 上跑 Wine内存管理相对自由可以 mmap 大块地址空间可以设置内存保护属性可以 fork 子进程。iOS 上这些操作都受限。iOS 的虚拟内存管理不允许应用随意 mmap 可执行内存W^X 策略写和執行不能同时执行得很严格。Wine 在加载 PE 文件时需要把代码段映射为可执行、数据段映射为可写然后修改重定位表。这个过程在 iOS 上需要特殊的权限通常只有通过特定的 entitlement 才能做到。FEX-Emu 的翻译缓存也需要可执行内存。它把翻译后的 ARM64 代码写到一块内存里然后跳过去执行。在 iOS 上这块内存必须通过mach_vm_allocate加上VM_PROT_EXECUTE来分配而且不能同时有写权限。所以 FEX-Emu 需要先分配可写内存写完翻译代码后用mprotect改成可执行再跳转。这个“写-改权限-执行”的流程在每次翻译新代码块时都要走一遍开销不小。注意iOS 上mprotect把可写内存改成可执行时如果内存页之前被标记过可执行可能会触发代码签名验证。未签名的代码页在 iOS 上执行会直接崩溃。所以 Madeira 这类项目通常需要越狱环境或者特定的开发者 entitlement普通 App Store 应用做不到。3. DXMT 把 Direct3D 翻译成 Metal 的取舍3.1 为什么不是 DXVK 或 VKD3D在 Linux 上Wine 跑 Windows 游戏的标准方案是 DXVK把 Direct3D 9/10/11 调用翻译成 Vulkan。Vulkan 在 Linux 上驱动成熟性能损失小。但 iOS 上没有 Vulkan只有 Metal。所以 DXMT 的思路是把 Direct3D 调用直接翻译成 Metal跳过 Vulkan 这个中间层。这个选择有得有失。好处是少了一层转换理论上延迟更低。坏处是 Metal 和 Direct3D 的抽象层级不一样。Direct3D 11 有设备、上下文、资源、视图、着色器等一系列概念Metal 有设备、命令队列、命令缓冲区、编码器、资源。两者的资源绑定模型、状态管理、多线程模型都有差异。DXMT 需要在这些概念之间做映射映射得不好就会出现性能瓶颈或兼容性问题。比如 Direct3D 11 允许在多个线程里同时往一个延迟上下文里录制命令最后统一提交。Metal 的命令缓冲区虽然也支持多线程编码但每个命令缓冲区只能由一个线程编码而且命令队列的提交是串行的。DXMT 需要把 D3D 的多线程延迟上下文模拟成多个 Metal 命令缓冲区然后在提交时做同步。这个同步如果做得粗多线程渲染的优势就没了做得细又容易出竞态。3.2 着色器翻译HLSL 到 MSL 的坑Direct3D 的着色器是 HLSL 编译成的 DXBC 或 DXIL 字节码。Metal 的着色器是 MSL 编译成的 AIR 字节码。DXMT 需要把 DXBC/DXIL 反编译成中间表示再生成 MSL然后用 Metal 的运行时编译接口编译成管线状态。这个链路里每一步都可能出问题。我遇到过的最典型的问题是纹理采样器的映射。HLSL 里采样器状态和纹理是分开绑定的一个采样器可以配合多个纹理使用。Metal 里采样器状态是独立的但采样器描述符的创建有数量限制而且某些过滤模式在 Metal 上不支持。比如 HLSL 的D3D11_FILTER_ANISOTROPIC在 Metal 上对应MTLSamplerMinMagFilterLinear加MTLSamplerMipFilterLinear再加各向异性采样但各向异性的最大倍数在 Metal 上有限制超过限制就会退化成普通线性过滤。另一个坑是整数运算。HLSL 的整数除法在除数为零时的行为和 MSL 不一样。HLSL 里整数除零会返回一个未定义值但通常不会崩溃MSL 里整数除零会触发陷阱直接让 GPU 挂掉。DXMT 需要在生成 MSL 时插入除零检查或者把整数除法改成浮点除法再转回整数。这个转换会带来额外的指令开销但在兼容性面前不得不做。提示如果你在 iOS 上跑 Windows 游戏时发现某些场景画面异常或者 GPU 崩溃先检查是不是着色器翻译的问题。可以打开 DXMT 的调试日志看它生成的 MSL 代码里有没有除零、越界访问、不支持的采样器模式。3.3 资源绑定和状态管理的性能陷阱Direct3D 11 的资源绑定是通过“视图”来做的渲染目标视图、深度模板视图、着色器资源视图、常量缓冲区视图。每次绘制调用前应用会设置一堆视图。Metal 的资源绑定是通过参数缓冲区或者setBuffer、setTexture来做的。DXMT 需要把 D3D 的视图绑定转换成 Metal 的绑定调用。这里有个性能陷阱D3D 允许应用频繁切换视图而 Metal 的绑定调用有开销。如果 DXMT 每次 D3D 切换视图都直接调用 Metal 的绑定接口绘制调用的开销会很大。优化的做法是维护一个“脏标记”系统记录哪些绑定变了在真正绘制时才把变化的绑定同步到 Metal。但脏标记系统本身也有开销而且如果应用频繁切换少量资源脏标记的收益就不明显。我实测过一个简单的 D3D 11 三角形程序在 DXMT 下跑帧率只有原生 Metal 程序的三分之一左右。用 Metal 的 GPU 捕获工具分析后发现瓶颈不在着色器执行而在资源绑定和管线状态切换。DXMT 在每次绘制前都要重新绑定一堆资源而原生 Metal 程序可以一次性绑定好后续绘制复用。这个差距在复杂场景里会更明显。4. 在 iOS 上部署 Madeira 的实际操作链路4.1 开发者模式与证书配置绕不开的第一步热词里反复出现“ios开发者模式”“xcode从证书配置到上架全流程”“免费证书ios”说明很多人卡在第一步。iOS 上跑任何非 App Store 分发的应用都需要签名。签名需要证书和描述文件。免费开发者账号可以生成开发证书但描述文件只有 7 天有效期而且设备数量有限制。付费开发者账号每年 99 美元可以生成有效期一年的描述文件设备数量上限 100 台。对于 Madeira 这类需要特殊 entitlement 的项目免费证书通常不够用。因为 FEX-Emu 需要可执行内存权限Wine 需要文件系统访问权限这些在标准开发描述文件里可能没有。你需要创建一个自定义的 entitlement 文件在 Xcode 的 Signing Capabilities 里添加相应的权限然后重新生成描述文件。这个过程在 Xcode 里点几下就能完成但前提是你知道需要哪些 entitlement。注意iOS 16 以后开发者模式需要在设置里手动开启。路径是“设置 → 隐私与安全性 → 开发者模式”打开后需要重启设备。如果这个选项是灰色的说明设备没有连接过 Xcode或者没有信任过开发者证书。先用数据线连上电脑在 Xcode 的 Devices and Simulators 里信任设备开发者模式选项才会出现。4.2 从源码到 IPA编译 Madeira 的关键步骤Madeira 本身不是一个可以直接下载安装的 App它更像是一组补丁和构建脚本把 Wine、FEX-Emu、DXMT 整合到一起。构建流程大致是这样的准备一台 macOS 电脑安装 Xcode 和 Command Line Tools。Xcode 版本要和 iOS 设备系统版本匹配比如 iOS 17 需要 Xcode 15 以上。克隆 Wine 的源码切换到 ARM64 支持的分支。Wine 官方对 ARM64 的支持在逐步完善但 iOS 上的特定补丁需要从 Madeira 的仓库里拿。克隆 FEX-Emu 的源码配置编译目标为 ARM64 iOS。FEX-Emu 依赖一些底层库比如libunwind、fmt、catch2这些库需要先交叉编译成 iOS 版本。克隆 DXMT 的源码它依赖 Metal 和 MetalKit 框架直接链接 iOS SDK 里的框架即可。把三者整合到一个 Xcode 工程里Wine 作为主程序FEX-Emu 和 DXMT 作为动态库链接进去。配置 entitlement包括com.apple.security.cs.allow-jit允许 JIT、com.apple.security.cs.allow-unsigned-executable-memory允许未签名可执行内存、com.apple.security.cs.disable-library-validation关闭库验证。编译签名打包成 IPA用 AltStore 或 Sideloadly 安装到设备上。这个流程里最容易出问题的是第 3 步和第 6 步。FEX-Emu 的交叉编译需要处理很多平台相关的宏定义比如__APPLE__、__aarch64__、TARGET_OS_IPHONE。如果某个宏没有正确处理编译出来的库可能在运行时崩溃。Entitlement 配置错误则会导致安装后无法启动或者启动后无法分配可执行内存。4.3 安装后的首次运行Wine 初始化与乱码问题安装完成后第一次启动 MadeiraWine 会初始化一个虚拟的 Windows 环境也就是所谓的“Wine prefix”。这个 prefix 里包含注册表、系统目录、驱动存根等。初始化过程需要创建大量文件和目录在 iOS 的沙盒里这些文件只能放在应用自己的 Documents 目录下。如果应用没有正确的文件系统 entitlement初始化会失败。热词里有个“wine 乱码”和“wine 栏是乱码”这个问题我遇到过很多次。原因通常是字体缺失或者字符编码不匹配。Wine 在渲染 Windows 程序的界面时需要 Windows 字体比如 Tahoma、Arial、SimSun。如果 prefix 里没有这些字体Wine 会用系统字体替代但替代字体的字符集可能不包含中文导致中文显示成方块或乱码。解决办法是把 Windows 的字体文件复制到 prefix 的drive_c/windows/Fonts目录下然后在 Wine 的注册表里设置字体替换规则。另一个乱码来源是 locale 设置。Wine 默认的 locale 可能是C或en_US.UTF-8如果程序期望的是zh_CN.GBK或zh_CN.UTF-8字符编码转换就会出错。可以在启动 Wine 时设置LANGzh_CN.UTF-8和LC_ALLzh_CN.UTF-8环境变量或者在 Wine 的注册表里修改HKEY_CURRENT_USER\Control Panel\International下的 locale 相关键值。提示如果乱码只出现在菜单栏或标题栏而不是程序内容区那大概率是 Wine 的窗口装饰window decoration没有正确加载字体。可以尝试在 Wine 配置里关闭“允许窗口管理器装饰窗口”让程序自己绘制标题栏。5. 那些热词背后没明说的真实问题5.1 “ios无感”“ios无感漏洞”和自动化脚本的关系热词里出现了“ios无感”“ios无感漏洞”“ios自动化”这些词通常和 iOS 自动化测试或脚本操作有关。“无感”一般指不需要用户手动确认就能执行某些操作比如自动点击、自动安装、自动跳转。在 iOS 上实现自动化的正规途径是 XCUITest 或者 Shortcuts但这两者都有局限XCUITest 需要应用有测试接口Shortcuts 能做的事情有限。“无感漏洞”这个词听起来很敏感但技术上讲它可能指的是某些系统版本里存在的权限绕过或者 UI 自动化接口的未授权访问。这类问题在 iOS 的每个大版本里都会被修补所以依赖“无感漏洞”的方案生命周期很短。如果你在做 iOS 自动化我的建议是优先用官方支持的接口比如XCUIApplication的launch和tap方法或者用simctl命令行工具控制模拟器。真机上的自动化没有开发者证书和测试框架支持很难做到稳定。5.2 “ios浏览器唤起安装app”和 URL Scheme 的坑热词里有个“ios浏览器唤起安装app”还带了一个看起来像下载链接的 URL。这个场景在推广和分发里很常见用户在 Safari 里点一个链接然后跳转到 App Store 或者直接安装一个企业签名的 IPA。技术实现上这通常是通过 URL Scheme 或者 Universal Links 来做的。URL Scheme 的格式是myapp://Universal Links 是https://myapp.com/...。但 iOS 对 URL Scheme 的调用有限制如果目标应用没有安装调用会失败Safari 会弹一个“无法打开页面”的提示。Universal Links 稍微好一点如果应用没安装会直接打开网页。对于企业签名分发还需要在网页上放一个itms-services://链接指向一个 plist 文件plist 里再指向 IPA 的下载地址。这个流程在 iOS 17 以后变得更严格企业证书签名的应用需要用户手动在设置里信任证书才能运行。注意如果你在做一个通过网页分发 iOS 应用的流程务必确保 plist 文件和 IPA 文件都通过 HTTPS 提供而且证书链完整。iOS 对itms-services的校验很严格任何一个环节的证书有问题都会导致安装失败。5.3 “xcode打包ios突然很慢”的排查思路热词里有个“xcode打包ios突然很慢如何解决”这个问题我在构建 Madeira 的时候也遇到过。Xcode 打包慢的原因通常有几个索引重建、模块缓存失效、链接器优化、代码签名。如果之前打包很快突然变慢最常见的原因是 DerivedData 目录膨胀或者索引数据库损坏。排查步骤可以这样走先看 Xcode 的构建日志找到耗时最长的阶段。如果是“Compile Swift”或“Compile C”可能是某个源文件触发了全量重编译。如果是“Link”可能是链接器在解析大量符号。如果是“Code Sign”可能是证书链验证在走网络请求。针对 DerivedData可以直接删除~/Library/Developer/Xcode/DerivedData目录让 Xcode 重建。针对索引可以删除~/Library/Developer/Xcode/Index目录。针对代码签名可以在 Xcode 设置里关闭“Automatically manage signing”手动指定描述文件减少网络验证。我自己的经验是Madeira 这种混合了 C、C、Objective-C、Swift 的工程Xcode 的增量编译经常失效。改一个头文件可能导致几百个文件重编译。解决办法是把不常变的代码抽成静态库或动态库减少主工程的源文件数量。另外把“Build Active Architecture Only”设为 YESDebug 配置下只编译当前架构也能省不少时间。6. 跨平台兼容层的边界与我的实际体会Madeira 这类项目最吸引人的地方是它试图在封闭的 iOS 生态里打开一个口子让 Windows 程序能够运行。但从技术现实来看这条链路里的每一环都有损耗FEX-Emu 的指令翻译有性能损失Wine 的 API 实现有兼容性缺口DXMT 的图形转换有功能限制iOS 的系统策略有权限约束。把这些损耗叠加起来最终能跑起来的程序要么是轻量级的工具软件要么是年代较久、依赖较少的游戏。最新的 3A 大作或者依赖特定硬件特性的生产力软件基本上跑不动。我在实际测试里发现Madeira 跑Notepad、WinRAR这类工具软件还算流畅跑Plants vs. Zombies这种 2D 游戏也没问题但跑GTA V或者Adobe Photoshop就力不从心了。这不是 Madeira 一个项目的问题而是整个技术路线的物理极限。指令翻译的开销、API 转换的延迟、GPU 驱动的差异这些不是靠优化能完全消除的。如果你打算深入研究这个方向我的建议是从小处着手先跑通一个最简单的 Windows 控制台程序确认 FEX-Emu 和 Wine 的链路是通的再跑一个简单的 Direct3D 9 程序确认 DXMT 的图形转换没问题最后再尝试复杂的程序遇到问题逐个排查。不要一上来就挑战大型游戏那样只会被各种报错淹没找不到问题的根源。另外iOS 的系统版本更新很频繁每次大版本更新都可能改变内存管理策略、图形驱动行为、签名验证规则。Madeira 这类项目需要持续跟进系统变化及时调整实现。如果你只是偶尔用一下建议固定在一个稳定的系统版本上不要盲目升级。升级前先确认 Madeira 的仓库里有没有对应的兼容性提交否则很可能升级完就再也跑不起来了。