Repository navigation
Description
事件背景
OpenCC ver.1.3.2 引入了一系列破壞性的 C++ ABI 變更,導致 librime 在運行期載入新版 libopencc 時直接 crash。
技術上,1.3.2 將 Converter 重構為純虛擬介面(abstract class),並引入 PipelineConverter 作為新的具體實作。librime 的 simplifier.cc 直接呼叫:
converter_->GetConversionChain()->GetConversions()而 PipelineConverter::GetConversionChain() 在有 normalization 設定時會回傳 nullptr,立即導致 null pointer dereference。OpenCC 為此在 ver.1.4.0 中:
- 修復了
GetConversionChain()回傳nullptr的 bug(PR #1380) - 將
SOVERSION從1.x改為1.4,專門用來阻止 librime 誤載入 ABI 不相容的1.3.2(PR #1377,PR 標題明確寫道 "to prevent librime loading ABI-incompatible v1.3.2 builds")
這次事件暴露的根本問題是:librime 沒有任何自動化測試可以在 OpenCC 升級後驗證繁簡轉換是否仍然正確運作。 問題只能在使用者端被察覺,修復也只能由 OpenCC 上游完成。
現有程式碼的層次結構
simplifier.cc 內部有兩個職責不同的層次:
第一層:rime::Opencc(私有適配器類別)
這是 librime 與 OpenCC C++ API 之間唯一的接觸點,封裝了三個操作:
| 方法 | 功能 |
|---|---|
ConvertWord(text, forms*) |
詞條級精確查詞,遍歷轉換鏈,返回所有可能形態(一對多) |
ConvertText(text, simplified*) |
整句轉換,直接呼叫 converter_->Convert() |
RandomConvertText(text, simplified*) |
整句轉換,每個前綴匹配隨機選一個變體 |
其中 ConvertWord 的邏輯完全由 librime 自己維護:它逐層遍歷 ConversionChain,在每個 Dict 上做精確匹配與前綴最長匹配,並在多個 dict 之間累積展開結果。這段邏輯並非對 OpenCC 的直接封裝,而是 librime 基於 OpenCC 底層 dict / conversion chain 介面自行實作的——也正是本次 ABI 破壞直接衝擊到的地方。
若有測試覆蓋這個邊界,GetConversionChain() 回傳 nullptr 就會在 CI 中以 crash 或測試失敗的方式立即暴露,而不是等到使用者回報。
第二層:Simplifier(RIME 過濾器)
使用 rime::Opencc,負責:
- 路由決策:
Convert()先嘗試ConvertWord,失敗再 fallback 到ConvertText;random_模式走RandomConvertText - 顯示格式化:
PushBack()處理 tips 顯示級別(無 / 單字 / 全部)、注釋位置(show_in_comment)、comment_formatter投影 - 類型過濾:
excluded_types_讓特定類型的候選詞跳過轉換
這一層深度依賴 RIME 引擎(Engine、Ticket、Candidate、Translation)。
目前的可測試性問題
rime::Opencc 被定義為 simplifier.cc 的內部私有類別,外部無法直接引用,導致:
- 無法在不啟動完整 RIME Engine 的前提下驗證轉換邏輯
- 任何對
simplifier.cc的修改,或對 OpenCC 的版本升級,都缺乏自動化驗證手段 ConvertWord中的鏈式轉換邊界行為(如注釋所說的「s2t 展開里為里和裏,再由 t2tw 將裏轉為裡」)完全沒有被測試保護
測試計劃
第一步:提取 rime::Opencc 至獨立標頭
將 rime::Opencc 從 simplifier.cc 移出,建立:
src/rime/gear/opencc.h:類別宣告(對外可見)src/rime/gear/opencc.cc:實作
simplifier.cc 改為 #include 這個新標頭。此步驟不改變任何對外行為,是純粹的內部重構,同時讓未來其他模組也能複用這個適配器。
第二步:新增 test/simplifier_test.cc
分兩組測試案例:
組一:OpenccTest(整合測試)
直接測試 rime::Opencc,使用真實的 OpenCC 詞典檔案(t2s.json、s2t.json、s2twp.json)。這組測試是未來再次升級 OpenCC 時的安全網。
| 測試案例 | 驗證內容 |
|---|---|
ConvertWord_ExactMatch |
詞條精確匹配,返回正確簡體形態 |
ConvertWord_MultipleValues |
一對多詞條返回所有變體 |
ConvertWord_NoMatch |
無對應詞條時返回 false |
ConvertWord_ChainExpansion |
鏈式轉換跨 dict 累積結果正確(對應 s2twp 的行為) |
ConvertWord_NoChange_ReturnsFalse |
轉換前後相同時正確返回 false |
ConvertText_FullSentence |
整句繁簡轉換結果正確 |
ConvertText_NoChange_ReturnsFalse |
無變化時返回 false |
Initialize_InvalidConfigPath |
詞典路徑不存在時,三個方法均安全返回 false,不拋出例外、不 crash |
最後一個案例直接對應本次事件的根因:rime::Opencc 的初始化錯誤處理必須健壯。
組二:SimplifierConvertTest(單元測試)
以 mock 的 rime::Opencc 測試 Simplifier 的路由與格式化邏輯,不依賴 OpenCC 詞典,也不需要啟動 RIME Engine。
| 測試案例 | 驗證內容 |
|---|---|
ConvertWord_MultiForm_ProducesMultipleCandidates |
ConvertWord 返回多個形態時,產生對應數量的 ShadowCandidate |
ConvertWord_OriginalFormPassesThrough |
其中一個形態與原文相同時,直接 push 原 candidate(不包裝) |
ConvertText_FallbackWhenConvertWordFails |
ConvertWord 失敗時,fallback 至 ConvertText |
NoConversion_WhenBothFail |
兩者均失敗時,Convert() 返回 false |
ExcludedType_IsSkipped |
excluded_types 中的候選詞不觸發轉換 |
RandomMode_DispatchesToRandomConvert |
random: true 時走 RandomConvertText 而非 ConvertWord |
TipsLevel_None |
tips: none 時 ShadowCandidate 的 comment 為空 |
TipsLevel_Char_SingleChar |
tips: char 對單字顯示原文提示 |
TipsLevel_Char_MultiChar |
tips: char 對多字詞不顯示提示 |
TipsLevel_All |
tips: all 對所有候選詞顯示原文提示 |
ShowInComment_SwapsTextAndTips |
show_in_comment: true 時轉換結果顯示於注釋欄 |
第三步:在 CI 中確認測試可跑
確認 make test 涵蓋新增的測試案例,在 GitHub Actions 現有 workflow 中通過。
工作量估計
| 步驟 | 估計工時 |
|---|---|
提取 rime::Opencc 至獨立標頭(純重構) |
1 小時 |
撰寫 OpenccTest 整合測試 |
2–3 小時 |
撰寫 SimplifierConvertTest 單元測試(含 mock 設計) |
3–4 小時 |
| 合計 | 約 6–8 小時 |
不在本次範圍內
SimplifierComponent::Create()的路徑解析邏輯(需要 deployer 基礎設施,建議另立提案)SimplifiedTranslation的翻譯包裝層(依賴完整的 Translation 物件,同上)- OpenCC 詞典資料本身的正確性(屬於 OpenCC 專案的責任範圍)
Activity
lotem commentedon Jul 3, 2026
多謝,多謝。真是大動作。
沒想到 OpenCC 時隔多年,又開始活躍更新了。歡迎拾柴。
frankslin commentedon Jul 3, 2026
第一步重構: #1190
frankslin commentedon Jul 4, 2026
OpenCC 側的測試已添加: BYVoid/OpenCC#1395
Rime 的測試見 #1191
CI 已全部通過。
lotem commentedon Jul 4, 2026
還需要這項修改嗎 #1184 ?或是要做更大的升級?
frankslin commentedon Jul 4, 2026
PR #1184 是針對 OpenCC 1.3.2 的 workaround,主要是為了處理當時 1.3.2 的 ABI 不一致問題。這個問題在 OpenCC 1.4.0 中已經透過 ABI version bump 與修正解決。
因此這個 patch 是否需要合併,取決於 librime 是否仍然希望支援這個壞掉的舊版 OpenCC 1.3.2:
以 OpenCC maintainer 的角度,我傾向於建議將最低支援版本提升至 1.4.0,並移除此類針對 1.3.x 的 workaround,以避免未來 ABI 行為分歧持續累積。
我可以再开一个 issue 討論 OpenCC API 層如何更明確地表達「一對多」 conversion 行為,但這個議題可以獨立於本 PR 處理。
amzxyz commentedon Jul 11, 2026
我特别希望OpenCC是其中一个预调用的模块,现在看代码已经实现,但我希望替换这个滤镜更开放一些,支持根据自身内容设置分隔符,支持自定义txt编译到bin在调用OpenCC时候通过参数设置即可,不想调用也能维持正确的键值替换能力