十八個 DLL 在 Linux 上全數轉綠
跨平台建置不只是擴大你的觸及範圍——它會迫使你的程式碼庫不再依賴某一個編譯器悄悄給的方便。你每新增一個平台,執行期在原有平台上就變得更誠實一分。
Linux 建置有好幾週一直是個已知的「部分通過」狀態。十八個原生執行期 DLL(引擎的架構是一組職責明確的共享函式庫艦隊)大多數建置得很乾淨,但有幾個不行。失敗的樣貌每次都一樣:連結階段對明明存在於程式碼庫裡的函式報出 undefined-symbol 錯誤。任何在 Linux 上用 C++ 做過共享函式庫的人都知道這是怎麼回事。
這個星期六我坐下來,打算把這件事收尾。到了星期六結束時,十八個 DLL 全部建置乾淨,Linux CI 管線回報全綠。
這篇文章要談的是:實際的問題是什麼、修法長什麼樣,以及為什麼我認為在三個平台(Linux、macOS、Windows MSVC)上建置是一種值得付出代價的紀律。
失敗的樣貌
模式總是類似這樣。一個連結到(例如)libraku_runtime.so 的測試二進位檔,會在連結時失敗,錯誤像是:
undefined reference to `raku_runtime_create_session(...)'
undefined reference to `raku_runtime_initialize(...)'
undefined reference to `raku_runtime_shutdown(...)'
這些函式確實存在。它們寫好了,就在原始碼檔案裡,也編譯過了,目的檔裡包含它們。但用 nm -D libraku_runtime.so 檢視共享函式庫時,卻沒有把它們匯出。
這就是經典的 Linux 共享函式庫符號可見性問題。執行期的原始碼檔案是用 CMake 旗標 -fvisibility=hidden 建置的,這本身是個非常好的預設值。-fvisibility=hidden 的用意是讓內部符號不出現在共享函式庫的公開符號表裡。任何需要公開的東西,都必須明確地標記 __attribute__((visibility("default"))),或用一個會展開成它的巨集。
執行期的建置系統有一個叫 RAKU_API 的巨集,理應在每個平台上展開成正確的可見性屬性。在 Windows 上,RAKU_API 在建置 DLL 時展開成 __declspec(dllexport),在使用 DLL 時展開成 __declspec(dllimport)。在 Linux 上,它理應在建置時展開成 __attribute__((visibility("default"))),在使用時展開成空。
問題在於,Linux 這一側的巨集並沒有套用到每一個公開函式上。有些函式標了,很多沒標。沒標的那些被 -fvisibility=hidden 的預設值隱藏,從公開符號表裡消失了。
修法的樣貌
三個步驟,全都是機械性的,全都值得記下來。
第一:稽核每個公開 API 標頭檔,找出缺少 RAKU_API 標記的地方。寫了一支腳本,解析公開 API 裡的每個 .h 檔,找出每一個應該公開卻沒有標記的函式宣告。腳本會回報函式、檔案和行號。跑了腳本,得到一份清單:十八個 DLL 裡共有三百四十七個函式需要加上標記。
第二:全面掃過補上標記。這正是 AI 代理擅長的那種機械式重構。issue 的框定是這樣寫的:「對下列每個函式,在宣告行加上 RAKU_API。不要修改函式本體。不要修改任何其他行。每完成一個子系統的批次就跑一次建置,確認在 Linux 上仍然可以建置。」代理一個子系統接一個子系統地乾淨完成。每個子系統的 PR 都可以獨立審查,而 Linux 建置隨著每次合併愈來愈綠。
第三:一道防止回歸的 CI 檢查。加了一項在 PR 時執行的檢查。這項檢查在 Linux 上建置執行期,對每個產出的 .so 跑 nm -D,把匯出的符號清單和公開 API 標頭檔裡宣告的預期集合比對,只要有任何預期符號缺失就讓 PR 失敗。這種護欄能在「我忘了幫新函式加標記」落地的當天就抓到它,而不是幾個月之後。
到星期六結束時,十八個 DLL 全部匯出了完整的公開介面。測試二進位檔連結成功了。測試跑起來了。Linux 建置是綠的。
為什麼三個平台值得付出代價
一個很自然的問題:到底為什麼要理會 Linux?執行期的目標是 AR 眼鏡,它跑的是一種特化的作業系統,跟開發機器上的 Linux、Windows 或 macOS 都不一樣。為什麼要在支援實際目標硬體的成本之上,再付一筆跨平台的成本?
三個理由。
第一:伺服器端 AI 推論和雲端渲染發生在 Linux 上。AR 體驗的任何雲端元件(模型服務、世界狀態同步、持久化)都跑在 Linux 上。執行期有一些雲端側會呼叫的掛勾。這些掛勾必須能在 Linux 上建置和執行,雲端側才能整合。如果執行期是一頭只能在 Windows 上活的巨獸,雲端側就得另外做一層通訊墊片,或是在 Linux 相容層下跑執行期。這兩者都不是我希望合作夥伴去做的事。
第二:Linux 上的 CI 比 Windows 上的 CI 更快也更便宜。我合併的每個 PR 都會經過 CI。Linux CI 執行器比 Windows CI 執行器更小、更快、更便宜。CI 迴圈愈快,代理和我在一個星期六裡能跑的迭代就愈多。把 Linux 當成一等公民建置目標,能加速整條開發工作流。
第三:跨平台紀律能抓出臭蟲。這是最深層的理由。當一個程式碼庫只在一個平台上建置時,開發者順手拿來用的模式就是在那個平台上行得通的模式。跨平台建置迫使這些模式變得可移植:用明確的可見性屬性取代隱含的、用明確的跨平台型別寬度取代「long 在這個編譯器上是 32 位元」、用明確的執行緒語義取代「這在 Windows 上跑得動」。今天的符號可見性修正正是這種模式。修完之後的程式碼庫比修之前更強健,而且是在每一個平台上,因為這次修正讓可見性契約變得明確了。
這能推廣到什麼
幾個誠實的模式。
Linux 上的符號可見性是一筆每個人都要付一次的稅。專案第一次碰到這個問題時,它神祕又令人挫折。一旦修法到位(RAKU_API 巨集、一致地套用、加上 CI 防護),它就隱形了。成本只在第一次。早點付。
針對符號可見性回歸的 CI 防護不是可有可無的。那種要幾個月才會浮現的臭蟲——因為一個還沒有人使用的函式缺了標記——正是 CI 防護能在落地當天就抓到的臭蟲。把防護架起來。
跨平台建置讓程式碼庫更誠實。程式碼庫裡任何依賴單一平台隱含行為的地方,跨平台移植都會迫使那個行為變得明確。每一次我把程式碼庫移植到新平台,新平台都會揭露原平台一直在默默吸收的臭蟲。這些修正在每個平台上都是改進,不只是新平台。
合作夥伴和開發者該從中學到什麼
如果你是合作夥伴,正在決定要在哪個引擎上打造跨平台 AR 產品,去問問那個團隊的建置矩陣長什麼樣。一個在 Linux、macOS 和 Windows 上都建置的團隊,是一個程式碼庫被三個平台之間的差異磨練過的團隊。只在一個平台上建置的團隊,則不是。
如果你是開發者,手上有自己的跨平台共享函式庫專案,符號可見性稽核是你今天就該跑的稽核,而不是等到三月某個測試開始失敗、原因得花三天才診斷出來的時候。對你的共享函式庫跑 nm -D,和你的標頭檔比對。那些不一致之處就是稽核結果。
如果你是 AI 實驗室,你的程式碼代理會寫跨平台原生程式碼,可見性屬性巨集正是代理需要放進工作知識裡的那種東西。一個寫了新的公開函式卻忘記加標記的代理,是一個會讓你多開一個後續 PR 的代理。一個一致地為每個公開函式加標記的代理,是一個物超所值的代理。
星期六收工。十八個 DLL 在 Linux 上轉綠。CI 現在更快了。程式碼庫更誠實了。建置矩陣有三個平台寬。
回去繼續打造。
一個被它觸及的每個平台磨練過的執行期
RakuAI 是跨平台的空間執行期,涵蓋 AR 眼鏡、雲端推論以及兩者之間的一切——Linux、macOS 和 Windows,全數轉綠。看看為什麼合作夥伴選擇在可移植的基礎上打造。