版本號與 ABI 相容
Plugin 身上有兩種版本,很容易搞混:
| 是什麼 | 誰決定 | 用途 | |
|---|---|---|---|
| Plugin 版本號 | 你的 Plugin 自己的 1.0.0 | 你 | 使用者看的版本、線上目錄比對「有沒有更新」 |
| ABI 版本 | 主程式與 Plugin 之間的介面記憶體佈局是第幾版,目前是 4.1 | 主程式的 SDK 標頭 | 決定這顆 DLL 主程式載不載得進來 |
兩者互不相干:改你自己的功能不會動 ABI 版本,主程式升級介面也不會動你的 Plugin 版本號。
Plugin 版本號:只改一個地方
每顆 Plugin 在 src\ 下放一個 plugin_version.h,只有四個 #define:
#define PLUGIN_VER_MAJOR 1
#define PLUGIN_VER_MINOR 0
#define PLUGIN_VER_PATCH 0
#define PLUGIN_VER_STR "1.0.0" /* 必須和上面三個數字逐字對得上 */三個數字各是 0~65535。範本與四顆範例都已經附好,你複製範本時就有。
這份檔案同時餵給兩個地方,所以不會走樣:
.c:NexusPluginGetInterface()把IcPlugin的version_major/version_minor/version_patch設成這三個巨集(version_rsv填 0)。.rc:VERSIONINFO資源用同一份巨集組出FileVersion/ProductVersion, 所以在檔案總管對 DLL 按右鍵 →「內容」→「詳細資料」就看得到版本,不必啟動主程式。
改版只改 plugin_version.h 這一份,.c 與 .rc 都是引用巨集。改完兩種架構(x86 與 x64)都要重新建置(見架構要對:x86/x64)。
plugin_version.h 有兩個限制:
- 必須是純 ASCII,而且只有 include guard 和
#define:資源編譯器(rc.exe、windres)也會讀它,它看不懂 C 宣告,也用系統的 ANSI 字碼頁解析。 0.0.0保留表示「未宣告」:三段全是 0 的話,主程式視為版本未知,顯示(-)。請從1.0.0(至少0.0.1)開始編號, 不要拿全零當起點——否則一顆忘記填版本號的 Plugin 在 Plugin Manager 眼中永遠「可以更新」。複製範本忘了改版本號是最常見的原因,見把 Plugin 交給使用者。
在主程式裡怎麼看
> plugin list
PLUGIN: 2 -> canvas_demo(1.0.0) hello_ic(1.0.0)
> plugin version hello_ic
hello_ic 1.0.0plugin list在每顆 Plugin 名稱後面直接接版本號;讀不到版本(更舊 ABI 的 Plugin,或0.0.0)顯示(-),不會讓整條指令失敗。plugin list的輸出有長度上限(192 位元組),Plugin 多了會被截斷;只想查一顆就用plugin version <名稱>: 已知版本印名稱 x.y.z,版本未知印名稱 -(版本未知不是錯誤),名稱根本沒有註冊過才回錯誤(not found)。- 主程式自己的版本與 ABI 版本用
ver看:
> ver
nexus_tui 1.0.0.8 (abi 4.1)ABI 版本:什麼情況載得進來
ABI 版本有主版本與次版本(現在是 4.1)。你的 Plugin 在 IcPlugin 前三個欄位填 abi_major、abi_minor、size_of_struct(範例都用 SDK 的常數與 sizeof(IcPlugin)):
| 情況 | 結果 |
|---|---|
| 主版本不同 | 拒絕載入。Log 記一則錯誤:plugin dll rejected: xxx.dll abi major 3 (expected 4); plugin abi 3.1 size ..., host abi 4.1 size ...,同時印出雙方的版本與結構大小;接著另有一行說明方向:DLL 比主程式舊(例如 3.x 的 Plugin 載入 4.0 的主程式)要「用目前主版本的 PDK 重新編譯」,DLL 比主程式新則要「更新主程式」 |
| 主版本相同、次版本比主程式舊 | 載入。新加在結尾的欄位主程式視為不存在,行為依 size_of_struct 降級 |
| 主版本相同、次版本比主程式新 | 載入,但記一則警告 ... declares abi minor N, newer than host M; fields beyond host size ... are ignored:超出主程式認得範圍的欄位不會生效 |
size_of_struct 太小(連 cleanup 都沒涵蓋) | 拒絕載入,Log 記錯誤 |
主版本只在「刪掉或改動既有欄位」這種會讓舊 DLL 讀寫到錯位置的變更時才增加。 次版本只在結構結尾追加新欄位時增加。這就是規則:新欄位只加在結尾。
所以你用 SDK 編出的 Plugin,只要主版本相同,就能在同主版本、較新或較舊次版本的主程式上載入。
實例:4.1 在結尾追加了 on_int_event 與五支 Dongle 服務(見 Dongle 專屬功能)。4.0 的 Plugin 不用重編就能在 4.1 主程式上跑,只是看不到這些欄位;反過來,4.1 的 Plugin 在 4.0 主程式上要先用 NX_ABI_HAS_FIELD 確認再呼叫。
用到比較新的欄位:NX_ABI_HAS_FIELD
如果你的 Plugin 想用比你編譯時的基準更晚才新增的 NxHostApi 欄位,要先檢查對方是不是真的有這個欄位:
if (NX_ABI_HAS_FIELD(host->size_of_struct, NxHostApi, SomeNewService) &&
host->SomeNewService != 0)
{
/* 可以用 */
}
else
{
/* 較舊的主程式:退回別的做法 */
}兩道檢查缺一不可:NX_ABI_HAS_FIELD 確認那個欄位完整落在對方宣告的 size_of_struct 之內;然後再檢查函式指標本身不是 NULL(主程式也可能合法地把它填成 NULL,表示不提供)。
絕對不要用 abi_minor 的大小判斷「有沒有某個欄位」。 abi_minor 只說明契約是哪一版,size_of_struct 才說明對方實際填了多少。
主程式與 Plugin 各自獨立升版時,用 minor 判斷會讀到沒初始化的記憶體。
(abi_minor 的正當用途是判斷「對方的行為約定是哪一版」,例如 NexusPluginGetInterface(host_abi_major, host_abi_minor) 收到的參數。)
NexusPluginGetInterface 裡怎麼寫
範例的寫法是:主版本不符就回 NULL(Plugin 自己拒絕載入),不要在這裡比較次版本——次版本不同不是拒載的理由:
if (host_abi_major != NX_PLUGIN_ABI_MAJOR)
{
return (struct IcPlugin *)0;
}之後所有欄位用具名賦值填寫(不要用位置初始化式,欄位順序將來若有增加,位置初始化式會靜默錯位)。
主程式與 PDK 的版本
PDK 的版本號就是主程式的版本號:那一版主程式配那一版 PDK,sdk\ 裡的 ABI 才會相符。
要支援還停在舊版主程式的客戶,請下載對應版本的 PDK 來編譯。
接下來
- 版本號填好了,就可以把 Plugin 交給使用者。
- 載不進來時的 Log 訊息對照:除錯與常見錯誤。