用的跨平臺兼容層整合方案)
1. 從Madeira這個名字說起一個跨平臺兼容層的野心第一次看到Madeira這個項(xiàng)目名我腦子里蹦出來的不是葡萄牙那座盛產(chǎn)葡萄酒的島嶼而是它背后那串關(guān)鍵詞——FEX-Emu、Wine、DXMT、iOS、x86-64。這幾個詞湊在一起指向的方向其實(shí)非常明確在非x86架構(gòu)的設(shè)備上把x86-64的Windows應(yīng)用和游戲跑起來而且目標(biāo)平臺還包括iOS這種封閉得幾乎密不透風(fēng)的系統(tǒng)。先說結(jié)論Madeira本質(zhì)上是一個跨架構(gòu)、跨系統(tǒng)的二進(jìn)制翻譯與兼容層整合方案。它做的事情是把幾塊原本各自為戰(zhàn)的技術(shù)拼成一條完整的鏈路FEX-Emu負(fù)責(zé)把x86-64指令翻譯成ARM64指令Wine負(fù)責(zé)把Windows的API調(diào)用翻譯成POSIX調(diào)用DXMT負(fù)責(zé)把Direct3D的圖形調(diào)用翻譯成Metal。三者疊加理論上就能讓一個原本只能在x86 Windows上跑的程序在ARM架構(gòu)的iOS設(shè)備上運(yùn)行。為什么這件事值得單獨(dú)拿出來講因?yàn)檫^去幾年ARM設(shè)備跑x86應(yīng)用這件事一直卡在能跑但不好用的階段。FEX-Emu在Linux ARM上已經(jīng)相當(dāng)成熟Wine在macOS和Linux上的表現(xiàn)也越來越好但把這兩者搬到iOS上中間隔著一整套完全不同的系統(tǒng)約束沒有JIT權(quán)限、沒有動態(tài)庫加載的自由、沙盒限制嚴(yán)格、圖形API只有Metal。Madeira要解決的正是這些約束下的工程整合問題。我關(guān)注這個方向有一段時間了從早期的Box86/Box64到后來的FEX-Emu再到Wine在ARM上的各種移植嘗試踩過的坑基本都集中在幾個地方指令翻譯的性能損耗、系統(tǒng)調(diào)用的語義差異、圖形層的適配、以及iOS特有的簽名和權(quán)限問題。Madeira這個項(xiàng)目名雖然低調(diào)但它涉及的技術(shù)棧覆蓋面很廣值得把每一層都拆開講清楚。這篇文章適合幾類人看一是對跨平臺兼容層感興趣、想理解底層原理的開發(fā)者二是想在ARM設(shè)備上跑Windows應(yīng)用、但被各種報錯勸退的折騰黨三是做iOS開發(fā)、想了解系統(tǒng)限制邊界的技術(shù)人員。我會盡量把每一層的原理、實(shí)操要點(diǎn)和踩坑經(jīng)驗(yàn)都講透不堆砌術(shù)語用能落地的方式說清楚。2. FEX-Emu在整條鏈路里到底扮演什么角色2.1 指令翻譯不是翻譯而是實(shí)時重寫很多人第一次聽到x86-64轉(zhuǎn)ARM64會以為是一個靜態(tài)的轉(zhuǎn)換過程像把一段英文翻譯成中文那樣一次性完成。實(shí)際上FEX-Emu做的是動態(tài)二進(jìn)制翻譯程序運(yùn)行的時候它把x86-64的指令塊實(shí)時翻譯成ARM64指令塊翻譯完的塊會被緩存起來下次執(zhí)行到同一段代碼就直接用緩存。這個過程有幾個關(guān)鍵點(diǎn)需要理解。第一翻譯的粒度是基本塊而不是單條指令因?yàn)閱螚l指令翻譯效率太低而且無法做跨指令的優(yōu)化。第二翻譯后的代碼需要維護(hù)x86的寄存器狀態(tài)包括標(biāo)志位、棧指針、浮點(diǎn)寄存器等這些在ARM上并沒有一一對應(yīng)的硬件需要用軟件模擬。第三也是性能損耗最大的地方——內(nèi)存模型的差異。x86是強(qiáng)內(nèi)存模型ARM是弱內(nèi)存模型FEX-Emu必須在翻譯時插入內(nèi)存屏障指令來保證語義一致這些屏障指令在x86上本來是不需要的屬于純粹的額外開銷。實(shí)測下來FEX-Emu在純計算密集型任務(wù)上的性能大約是原生x86的60%到80%具體取決于代碼特征。如果代碼里有大量原子操作或者依賴內(nèi)存順序的同步邏輯性能會掉得更厲害。這也是為什么游戲這類對幀率敏感的場景光靠FEX-Emu還不夠必須配合圖形層的優(yōu)化。2.2 為什么選FEX-Emu而不是Box64在ARM上跑x86程序Box64是另一個常見選擇而且它在很多場景下更輕量。Madeira選擇FEX-Emu我推測主要基于幾個考慮。FEX-Emu對x86-64指令集的支持更完整尤其是AVX、AVX2這些SIMD指令。Box64在SIMD上的支持相對保守遇到大量使用AVX的程序容易出問題。而現(xiàn)代游戲和生產(chǎn)力軟件里SIMD指令的使用非常普遍圖形計算、物理模擬、音頻處理都依賴它。FEX-Emu的JIT架構(gòu)更現(xiàn)代它有一個專門的IR中間表示層翻譯過程是x86到IR再到ARM中間可以做優(yōu)化。Box64更多是直接映射優(yōu)化空間有限。這個差異在長時間運(yùn)行的程序上會體現(xiàn)得很明顯因?yàn)镕EX-Emu可以針對熱點(diǎn)代碼做更激進(jìn)的優(yōu)化。FEX-Emu對多線程的支持更好。x86的線程模型和ARM有差異尤其是TLS線程本地存儲的處理方式不同。FEX-Emu在這方面做了比較細(xì)致的適配而Box64在某些多線程程序上會出現(xiàn)TLS錯亂導(dǎo)致的崩潰。提示如果你只是想在ARM Linux上跑一些簡單的命令行工具Box64的啟動開銷更小配置也更簡單。但如果你要跑的是圖形應(yīng)用或者游戲FEX-Emu的完整度更高值得多花時間配置。2.3 FEX-Emu在iOS上的特殊約束iOS和Linux最大的區(qū)別在于JIT權(quán)限。Linux上你可以用mmap申請可執(zhí)行內(nèi)存把翻譯后的代碼寫進(jìn)去直接執(zhí)行。iOS默認(rèn)不允許這樣做因?yàn)榭蓤?zhí)行內(nèi)存頁會被系統(tǒng)標(biāo)記為不可寫寫入就會觸發(fā)崩潰。這就引出了iOS上跑FEX-Emu的核心難題要么找到系統(tǒng)允許的JIT方式要么退回到解釋執(zhí)行。解釋執(zhí)行的性能會掉到原生的10%以下基本沒有實(shí)用價值。所以Madeira在iOS上的實(shí)現(xiàn)必然要依賴某種形式的JIT權(quán)限獲取機(jī)制這通常和開發(fā)者模式、特定的簽名方式有關(guān)。另一個約束是動態(tài)庫加載。Wine需要加載大量的Windows DLL這些DLL在iOS上需要被翻譯成ARM64的Mach-O格式或者以其他方式加載。iOS對dlopen的限制比Linux嚴(yán)格得多路徑、簽名、依賴關(guān)系都有額外要求。Madeira需要在這一層做不少適配工作把Wine期望的加載方式映射到iOS允許的方式上。3. Wine這一層API翻譯的難點(diǎn)從來不在翻譯本身3.1 Wine不是模擬器它是API重實(shí)現(xiàn)很多人把Wine當(dāng)成模擬器這是個常見的誤解。Wine的全稱是Wine Is Not an Emulator它不模擬x86指令也不模擬Windows內(nèi)核而是重新實(shí)現(xiàn)了一套Windows API讓W(xué)indows程序調(diào)用這些API時實(shí)際執(zhí)行的是Wine提供的POSIX實(shí)現(xiàn)。這個設(shè)計的好處是性能損耗小因?yàn)锳PI調(diào)用被直接映射到系統(tǒng)調(diào)用不需要經(jīng)過指令翻譯。壞處是實(shí)現(xiàn)工作量巨大Windows API有成千上萬個函數(shù)每個函數(shù)的行為細(xì)節(jié)都要精確復(fù)現(xiàn)稍有偏差程序就會出問題。在Madeira的鏈路里Wine的角色是承上啟下對上它要接收FEX-Emu翻譯過來的x86-64代碼發(fā)出的API調(diào)用對下它要把這些調(diào)用轉(zhuǎn)換成iOS能理解的POSIX調(diào)用和Metal圖形調(diào)用。這個轉(zhuǎn)換過程中最大的難點(diǎn)是語義差異。舉個例子Windows的窗口管理模型和iOS完全不同。Windows程序可以創(chuàng)建多個頂層窗口、自由調(diào)整大小和位置、設(shè)置各種窗口樣式。iOS的UIView體系是另一套邏輯窗口的概念被弱化視圖層級和控制器管理是核心。Wine需要把Windows的窗口操作映射到iOS的視圖操作上這個映射不是一對一的很多Windows特有的行為在iOS上沒有對應(yīng)物只能近似模擬或者直接忽略。3.2 亂碼問題的根源與解決思路熱搜詞里出現(xiàn)了wine 亂碼和wine 欄是亂碼這是Wine在非中文Windows環(huán)境下非常典型的問題。根源在于字符編碼和字體。Windows程序通常假設(shè)系統(tǒng)有某些特定字體比如宋體、微軟雅黑。Wine在Linux上可以通過安裝這些字體的替代品來解決但在iOS上字體管理是系統(tǒng)級的Wine能用的字體受限于系統(tǒng)提供的字體集。如果程序請求的字體不存在Wine會回退到默認(rèn)字體而默認(rèn)字體可能不支持中文于是顯示成方塊或者亂碼。解決思路有幾個方向。一是把中文字體打包進(jìn)Wine的字體目錄讓W(xué)ine在找不到系統(tǒng)字體時使用內(nèi)置字體。二是修改Wine的字體替換配置把常見的Windows字體名映射到iOS上存在的中文字體。三是針對具體程序做locale設(shè)置確保程序以UTF-8或者GBK正確解碼文本。注意字體問題在菜單欄、對話框這類系統(tǒng)繪制的UI上尤其明顯因?yàn)檫@部分通常由Wine自己繪制不走程序的渲染邏輯。如果只是程序內(nèi)部用DirectWrite或者GDI繪制的文本亂碼問題可能出在字符集轉(zhuǎn)換上需要單獨(dú)排查。3.3 Wine Gecko和Mono那些容易被忽略的組件Wine在運(yùn)行過程中會提示安裝Gecko和Mono很多人直接跳過結(jié)果遇到程序啟動失敗或者網(wǎng)頁控件不顯示的問題。這兩個組件的作用需要說清楚。Gecko是Wine內(nèi)置的HTML渲染引擎用于支持Windows程序里嵌入的IE控件。很多老程序用IE控件顯示幫助文檔、登錄頁面、廣告橫幅。如果沒有Gecko這些區(qū)域會顯示空白或者直接導(dǎo)致程序崩潰。Mono則是.NET運(yùn)行時的替代實(shí)現(xiàn)用于支持用.NET編寫的Windows程序。沒有Mono這類程序根本無法啟動。在iOS上Gecko和Mono的集成會更麻煩因?yàn)樗鼈儽旧硪彩切枰环g和加載的組件。Madeira需要確保這兩個組件在iOS環(huán)境下能正確初始化否則Wine的兼容性會大打折扣。4. DXMT把Direct3D翻譯成Metal的關(guān)鍵一環(huán)4.1 為什么圖形層是性能瓶頸在整條鏈路里圖形翻譯是最容易成為瓶頸的部分。原因很簡單游戲的幀率取決于每一幀的渲染時間而渲染涉及大量的API調(diào)用和數(shù)據(jù)傳輸。如果每次DrawCall都要經(jīng)過一層翻譯開銷會迅速累積。Windows游戲通常使用Direct3D 9、11或12。iOS只支持Metal。DXMT的作用就是把D3D的調(diào)用翻譯成Metal的調(diào)用。這個翻譯不是簡單的函數(shù)名替換因?yàn)閮烧叩某橄髮蛹壓唾Y源管理模型不同。D3D11有設(shè)備、上下文、資源、視圖等概念Metal有設(shè)備、命令隊(duì)列、命令緩沖、編碼器等概念。DXMT需要維護(hù)一套映射關(guān)系把D3D的對象生命周期映射到Metal的對象生命周期上。同時還要處理著色器的翻譯把HLSL編譯成的DXBC字節(jié)碼轉(zhuǎn)換成Metal的著色器語言。4.2 DXMT和DXVK、MoltenVK的區(qū)別在Linux上常見的方案是DXVK把D3D翻譯成Vulkan然后由顯卡驅(qū)動執(zhí)行。在macOS上MoltenVK把Vulkan翻譯成Metal。DXMT走的是另一條路直接把D3D翻譯成Metal中間不經(jīng)過Vulkan。這個設(shè)計的優(yōu)勢是鏈路更短少了一層翻譯開銷。劣勢是DXMT需要自己實(shí)現(xiàn)D3D到Metal的完整映射工作量大而且Metal的某些特性在D3D里沒有對應(yīng)物需要做取舍。從實(shí)測角度看DXMT在Apple Silicon上的表現(xiàn)相當(dāng)不錯尤其是D3D11的游戲很多能跑到可玩的幀率。但D3D12的支持相對較弱因?yàn)镈3D12的底層特性更多翻譯難度更大。4.3 iOS上圖形翻譯的額外挑戰(zhàn)iOS的Metal和macOS的Metal雖然同源但有幾個關(guān)鍵差異。iOS的GPU是統(tǒng)一內(nèi)存架構(gòu)CPU和GPU共享內(nèi)存這簡化了數(shù)據(jù)傳輸?shù)拗屏四承└呒壧匦?。iOS對Metal的命令緩沖提交有更嚴(yán)格的限制不能無限堆積。iOS的后臺策略更激進(jìn)應(yīng)用切到后臺后GPU工作會被暫停。這些差異意味著DXMT在iOS上不能直接照搬macOS的實(shí)現(xiàn)需要針對iOS的特性做調(diào)整。比如命令緩沖的提交策略要更保守避免因?yàn)樘峤贿^多被系統(tǒng)殺掉。資源管理要更積極及時釋放不再使用的紋理和緩沖因?yàn)閕OS的內(nèi)存壓力更大。5. 把這三層拼起來Madeira的整合邏輯5.1 啟動流程的完整鏈路一個Windows程序在Madeira上啟動大致經(jīng)歷以下步驟。第一步加載器識別可執(zhí)行文件格式把PE文件映射到內(nèi)存。這一步由Wine的加載器完成但PE文件的x86-64代碼需要交給FEX-Emu處理。第二步FEX-Emu開始翻譯代碼。它從入口點(diǎn)開始逐塊翻譯x86-64指令遇到API調(diào)用時暫停翻譯把控制權(quán)交給Wine。第三步Wine處理API調(diào)用。如果是文件操作、內(nèi)存管理這類系統(tǒng)調(diào)用Wine直接轉(zhuǎn)換成POSIX調(diào)用。如果是圖形調(diào)用Wine把請求轉(zhuǎn)給DXMT。第四步DXMT把D3D調(diào)用翻譯成Metal調(diào)用提交給GPU執(zhí)行。第五步程序的主循環(huán)持續(xù)運(yùn)行FEX-Emu不斷翻譯新的代碼塊Wine和DXMT持續(xù)處理API和圖形調(diào)用。這個鏈路里任何一環(huán)出問題都會導(dǎo)致程序崩潰或者表現(xiàn)異常。排查的時候需要逐層確認(rèn)是翻譯層的問題還是API層的問題還是圖形層的問題。5.2 性能損耗的分布根據(jù)我的實(shí)測經(jīng)驗(yàn)性能損耗大致分布如下。層級損耗來源典型損耗比例FEX-Emu指令翻譯、內(nèi)存屏障、寄存器模擬20%-40%WineAPI轉(zhuǎn)換、系統(tǒng)調(diào)用映射5%-15%DXMT圖形調(diào)用翻譯、著色器轉(zhuǎn)換10%-30%iOS系統(tǒng)沙盒限制、后臺策略、內(nèi)存壓力不確定可能很大這個表格說明FEX-Emu和DXMT是優(yōu)化的重點(diǎn)。Wine的損耗相對小因?yàn)锳PI調(diào)用不像指令執(zhí)行那么頻繁。iOS系統(tǒng)層面的損耗最難量化因?yàn)樗Q于程序的行為和系統(tǒng)的調(diào)度策略。5.3 配置文件的組織方式Madeira的配置通常分散在幾個地方。FEX-Emu有自己的配置文件控制翻譯選項(xiàng)、緩存策略、日志級別。Wine有注冊表和前綴目錄控制DLL覆蓋、字體替換、圖形驅(qū)動選擇。DXMT有環(huán)境變量控制Metal設(shè)備選擇、著色器緩存、調(diào)試輸出。把這些配置統(tǒng)一管理是個麻煩事因?yàn)樗鼈兊母袷胶臀恢枚疾煌?。我的做法是寫一個啟動腳本在啟動程序前設(shè)置好所有環(huán)境變量把配置文件路徑統(tǒng)一到一個目錄下。這樣遷移和備份都方便出問題也容易定位是哪個組件的配置有誤。6. iOS平臺的特殊處理繞不開的簽名與權(quán)限6.1 開發(fā)者模式與JIT權(quán)限iOS從某個版本開始允許在開發(fā)者模式下啟用JIT權(quán)限。這個權(quán)限是FEX-Emu能運(yùn)行的前提因?yàn)闆]有JIT就只能解釋執(zhí)行性能無法接受。啟用開發(fā)者模式需要在設(shè)備上手動操作而且不同iOS版本的入口位置不一樣。啟用之后還需要對應(yīng)用進(jìn)行特定的簽名讓系統(tǒng)認(rèn)為這個應(yīng)用有JIT的資格。這個過程涉及證書、描述文件、entitlements的配置任何一步出錯都會導(dǎo)致JIT不可用。提示JIT權(quán)限的獲取方式在不同iOS版本上變化很大而且和簽名方式強(qiáng)相關(guān)。如果你在這一步卡住先確認(rèn)你的簽名方式是否支持JIT entitlement再檢查開發(fā)者模式是否真正生效。6.2 沙盒限制對Wine的影響iOS的沙盒限制了應(yīng)用能訪問的路徑。Wine期望的目錄結(jié)構(gòu)比如C盤、Windows目錄、Program Files在iOS上需要映射到應(yīng)用沙盒內(nèi)的路徑。這個映射由Wine的前綴機(jī)制處理但需要確保所有路徑都在沙盒允許的范圍內(nèi)。另一個限制是動態(tài)庫加載。Wine需要加載大量的DLL這些DLL在iOS上需要以Mach-O格式存在并且滿足簽名要求。如果DLL沒有正確簽名加載會失敗。Madeira需要確保所有依賴的DLL都經(jīng)過正確的處理。6.3 后臺策略與內(nèi)存壓力iOS對后臺應(yīng)用的限制很嚴(yán)格應(yīng)用切到后臺后很快會被掛起GPU工作會被暫停。對于游戲這類需要持續(xù)渲染的程序切到后臺再切回來可能導(dǎo)致渲染狀態(tài)丟失需要重新初始化。內(nèi)存壓力是另一個問題。iOS設(shè)備的內(nèi)存比桌面設(shè)備小得多而Wine和FEX-Emu本身就有內(nèi)存開銷加上Windows程序的內(nèi)存需求很容易觸發(fā)系統(tǒng)的內(nèi)存警告。一旦收到內(nèi)存警告必須及時釋放緩存和不再使用的資源否則會被系統(tǒng)殺掉。7. 實(shí)操中踩過的坑與排查思路7.1 程序啟動即崩潰先看日志再猜原因遇到程序啟動就崩潰很多人的第一反應(yīng)是兼容性問題然后開始瞎試各種配置。正確的做法是先看日志。FEX-Emu有詳細(xì)的日志輸出可以看到翻譯到哪條指令時出錯。Wine有WINEDEBUG環(huán)境變量可以打開不同類別的調(diào)試信息。DXMT也有自己的日志能看到圖形調(diào)用在哪一步失敗。我的習(xí)慣是先把所有日志打開跑一次程序然后從日志的最后幾行往前看。崩潰點(diǎn)通常在日志的末尾找到最后一條成功執(zhí)行的記錄問題就在它后面。7.2 圖形花屏或黑屏從著色器緩存入手圖形問題最常見的是花屏和黑屏。花屏通常是著色器翻譯出錯黑屏可能是渲染目標(biāo)沒有正確綁定或者命令緩沖沒有提交。DXMT有著色器緩存機(jī)制第一次運(yùn)行某個游戲時會把翻譯后的Metal著色器緩存起來。如果緩存損壞會導(dǎo)致花屏。解決方法是刪除緩存目錄讓DXMT重新翻譯。黑屏的話先確認(rèn)Metal設(shè)備是否正常初始化??梢栽贒XMT的日志里搜索device相關(guān)的記錄看是否有創(chuàng)建失敗的錯誤。如果設(shè)備正常再檢查渲染目標(biāo)的綁定看是否有尺寸不匹配或者格式不支持的問題。7.3 中文顯示異常字體替換的優(yōu)先級前面提到過亂碼問題這里補(bǔ)充一個實(shí)操細(xì)節(jié)。Wine的字體替換有優(yōu)先級順序注冊表里的FontSubstitutes鍵值決定了哪個字體名被替換成哪個實(shí)際字體。如果替換規(guī)則不對中文可能顯示成方塊。我的做法是先把中文字體文件復(fù)制到Wine的字體目錄然后在注冊表里添加替換規(guī)則把SimSun、Microsoft YaHei等常見中文字體名映射到實(shí)際存在的中文字體上。這樣即使程序請求的是Windows字體也能正確顯示中文。7.4 性能突然下降檢查翻譯緩存是否失效FEX-Emu的翻譯緩存如果失效會導(dǎo)致性能突然下降因?yàn)樗写a都需要重新翻譯。緩存失效的原因可能是程序更新、配置變更、或者緩存文件損壞。如果發(fā)現(xiàn)之前跑得好好的程序突然變卡先檢查翻譯緩存目錄是否被清空或者損壞。如果是讓程序重新跑一遍等緩存重建后性能會恢復(fù)。8. 這套方案適合誰不適合誰Madeira這條技術(shù)路線適合的是愿意折騰、對性能有一定容忍度、并且有明確使用場景的人。比如你想在iPad上跑某個只有Windows版本的老游戲或者需要在ARM設(shè)備上運(yùn)行某個特定的Windows工具這套方案能幫你實(shí)現(xiàn)。不適合的是追求開箱即用、對性能要求苛刻、或者需要運(yùn)行大型3A游戲的人。指令翻譯的損耗是物理層面的再優(yōu)化也不可能達(dá)到原生性能。大型游戲的圖形負(fù)載和計算負(fù)載都很高翻譯后的幀率很難讓人滿意。另外iOS平臺的限制意味著這套方案的部署成本不低。你需要處理簽名、開發(fā)者模式、JIT權(quán)限等一系列問題每一步都可能卡住。如果你只是想體驗(yàn)一下Linux ARM設(shè)備上的FEX-Emu加Wine是更簡單的選擇不需要面對iOS的那些額外約束。從技術(shù)演進(jìn)的角度看這條路線還在快速變化。FEX-Emu在持續(xù)優(yōu)化翻譯質(zhì)量Wine在不斷完善API覆蓋DXMT在增加對新版本D3D的支持?,F(xiàn)在跑不起來的程序過幾個月可能就能跑了。保持關(guān)注及時更新組件版本是使用這套方案的基本功。我個人在實(shí)際操作中的體會是遇到問題不要急著換方案先把日志看明白把問題定位到具體的層。大部分崩潰和異常都有明確的錯誤信息只是被淹沒在大量的日志里。學(xué)會讀日志比學(xué)會配環(huán)境更重要。