系列: 與 AI 一起學寫程式

十八個 DLL 在 Linux 上全數轉綠

讓 Linux 建置轉綠的符號可見性大清掃。

十八個函式庫在 Linux 上全數轉綠 標記 347 個函式、一道 CI 防護、三個平台 libraku_01 libraku_02 libraku_03 libraku_04 libraku_05 libraku_06 libraku_07 libraku_08 libraku_09 libraku_10 libraku_11 libraku_12 libraku_13 libraku_14 libraku_15 libraku_16 libraku_17 libraku_18 RAKU_API __attribute__((visibility("default"))) nm -D 防護:任何遺漏符號的 PR 都會失敗
十八個函式庫、一份一致的可見性契約,以及一道讓它保持誠實的防護。

跨平台建置不只是擴大你的觸及範圍——它會迫使你的程式碼庫不再依賴某一個編譯器悄悄給的方便。你每新增一個平台,執行期在原有平台上就變得更誠實一分。

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 上建置執行期,對每個產出的 .sonm -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,全數轉綠。看看為什麼合作夥伴選擇在可移植的基礎上打造。

← 所有文章