1. 概述
Pluma 是一個用 C++ 開發的可用于管理插件的開源架構。該架構是個輕量級架構,非常易于理解。
Pluma 架構有以下基本概念:
1)插件的外在行為體現為一個純虛類,可以叫作插件接口;
2)繼承于同一個插件接口的若干派生類,被認為屬于同一種插件,可以叫作插件類;
3)每一個插件接口或插件類都有個一一對應的 Provider 類,其中,插件接口對應的 Provider 類里會定義一個特殊字符串常量:PLUMA_PROVIDER_TYPE,表示這一類 “插件 Provider” 共同的類型名稱,而這個類型名稱其實就是插件接口的類名字符串。
4)多個插件類可以被放入一個插件動態庫中,而這個動態庫文件名(不包括后綴部分)可以叫作 “插件名”。
5)插件機制使用者可以在自己的架構中包含一個 Pluma 管理類,該類支持從所指定的位置加載一個或多個插件動態庫,并將每個插件類對應的 Provider,記錄進內部的表中。
6)插件機制使用者可以在合適時機,利用 Pluma 獲取內含的插件 Provider,并調用某個插件 Provider 的 create () 函數,創建出對應的插件對象。
7)使用完插件對象后,不要忘了 delete 它。
現在我們畫一張示意圖:
2. Pluma 管理類
我們剛剛也說了,插件機制使用者可以包含一個 Pluma 管理類。
該類繼承于 PluginManager 類。
【pluma-1.1/include/pluma/PluginManager.hpp】
從上面的 load () 函數和 loadFromFolder () 函數可以看出,插件管理器既允許用戶單獨加載某個插件動態庫,也允許批量性加載某個目錄下所有的插件動態庫。另外,值得注意的是,getProviders () 函數是 protected 的成員,也就是說,這套架構是不希望用戶直接使用這個 PluginManager 類的,即便用了,你也拿不到 Provider。正確的做法是,使用 PluginManager 的子類:Pluma 管理類。 ?? 另外,上面的成員變量 libraries,就是記錄所有已加載的插件動態庫的映射表。而成員變量 host 則負責記錄每個插件類對應的 Provider 信息。之所以被稱為 host(宿主),是針對插件而言的。也就是說插件本身實際上是沒資格知道其真實宿主的全貌的,它只能訪問和它相關的很小一部分數據而已,因此 Pluma 將這一小部分數據整理成一個 host 代理,供插件使用。?? ? Pluma 管理類的代碼截選如下: 【pluma-1.1/include/pluma/Pluma.hpp】
請大家注意上面代碼中最后一行,這個 Pluma.hpp 還真是有點手黑,偷偷摸摸 #include 了個 Pluma.inl 文件,其實展開來就是 acceptProviderType () 和 getProviders () 這兩個模板函數的實現。Pluma.inl 文件的內容如下: 【pluma-1.1/include/pluma/Pluma.inl】
看到了吧,重新定義了個 getProviders (),還搞成一個模板函數,在函數體內會反過來通過模板參數,進一步得到所涉及的插件 Provider 的 PLUMA_PROVIDER_TYPE 信息,這個技巧挺重要。也就是說,外界傳來的是 vector
2.1 Host 代理 【pluma-1.1/include/pluma/Host.hpp】
正如前文所說,Host 代理是針對插件而言的。而 Host 只有一個 public 成員函數 add (),說明其主要對外行為就是讓插件將對應的 provider 注冊進 Host。
3. 插件類和其對應的 Provider 類
在說了一大堆插件管理類代碼后,現在終于要開始說插件部分了。前文已經說過,插件的外在行為體現為一個純虛類,可以叫作插件接口。我們現在就以 Pluma 源碼中給出的例子為準,來說明一些細節。
3.1 Warrior 接口和 WarriorProvider 類
Pluma 中的插件接口例子是 Warrior,其源碼截選如下: 【pluma-1.1/example/src/interface/Warrior.hpp】
這個接口里只象征性的寫了一個成員函數 getDescription (),大家明白意思即可。 ?? ? 需要注意的是類定義之后的那句 PLUMA_PROVIDER_HEADER,這個宏負責定義和插件接口對應的 Provider 類。相關的宏定義如下: 【pluma-1.1/include/pluma/Pluma.hpp】
基于這些宏定義,我們可以將 PLUMA_PROVIDER_HEADER (Warrior) 展開為:
代碼很清晰,為 Warrior 接口聲明一個配套的 WarriorProvider 類。這個類里包含著重要的 PLUMA_PROVIDER_TYPE 常量,以及最關鍵的 create () 函數。 Warrior 的實現文件更加簡單: 【pluma-1.1/example/src/interface/Warrior.cpp】
也在使用宏,展開宏后可見:
因為 Warrior 本身是個純虛類,所以 WarriorProvider 里也不用實現 create () 函數。
3.2 Warrior 派生類和派生 Provider
在 pluma 源碼的例子中,提供了三個 Warrior 派生類,SimpleWarrior、Eagle 和 Jaguar。默認的是 SimpleWarrior,它被集成進 example/src/host 目錄。也就是說,即便我們一個額外的插件庫都不提供,示例至少還可以使用 SimpleWarrior。而 Eagle 和 Jaguar 則位于 example/src/plugin 目錄,可以打包進一個插件動態庫。 【pluma-1.1/example/src/host/SimpleWarrior.hpp】
前文我們已經看到,對于插件接口(Warrior)來說,用到的宏是PLUMA_PROVIDER_HEADER(Warrior),現在針對實際插件類(SimpleWarrior),會用到另一個宏PLUMA_INHERIT_PROVIDER(SimpleWarrior, Warrior)。這個宏的定義如下: 【pluma-1.1/include/pluma/Pluma.hpp】
展開后可見:
很簡單,就是在完成 Provider 的核心使命,提供一個創建插件類對象的 create () 函數。與 SimpleWarriorProvider 類似,另外兩個 Warrior 派生類 Eagle 和 Jaguar 大體也是這么寫的。示意圖如下: 在研究 Pluma 所給示例時,我已事先將 Pluma 封裝成靜態庫了,現在要把 Eagle 和 Jaguar 編譯并封裝成一個動態庫,就需要鏈接 Pluma 靜態庫,除此之外,還需要編譯其他一些輔助文件,列舉如下: 1)Connector.cpp 2)dllmain.cpp 3)Eagle.hpp 4)Jaguar.hpp 5)Warrior.cpp 其中 Connector.cpp 文件,是插件動態庫向外界 Host 注冊自己所有 Provider 的地方。它必須實現一個 connect () 函數,代碼截選如下:
我們先不要著急分析上面的 connect () 動作,可以先跟著我看看插件的加載流程,后文我們就會知道,connect () 只是加載流程的一環而已。
4. 插件加載流程
我們看一下 Pluma 架構所給例子的 main () 函數,就可以了解插件的加載流程了:
其中和加載插件相關的句子主要就是 pluma.acceptProviderType 和 pluma.load 兩句了。前者主要負責在 Host 的knownTypes映射表中添加一個 ProviderInfo 節點,后者負責加載插件動態庫,并將動態庫里匹配的 Provider 指針記入 ProviderInfo 節點。
4.1 pluma.acceptProviderType<>()
我們先說 pluma.acceptProviderType 一句。在前文介紹 Pluma.inl 文件的內容時,我們已經看到一個叫作 acceptProviderType 的模板函數了,當時沒有細說,現在我把它的代碼再貼一下: 【pluma-1.1/include/pluma/Pluma.inl】
里面調用的是 PluginManager 基類的 registerType () 函數。 我們前文主要關心的是 PLUMA_PROVIDER_TYPE,現在再說一下后兩個參數。PLUMA_INTERFACE_VERSION 表示管理器當前應該使用的插件接口的版本,因為我們不能確定更高版本的插件接口會不會增加或刪除成員函數,所以這個值其實是個限定值,如果后續用戶嘗試加載更高版本的插件,那么是無法通過校驗的。 第三個參數 PLUMA_INTERFACE_LOWEST_VERSION 則是限定最低值,如果嘗試加載比這個值更低版本的插件,肯定也是不會通過的。 在剛剛看到的 main () 函數里,是這樣寫的:
pluma.acceptProviderType也就是說,Pluma 插件管理器對 Warrior 接口對應的 WarriorProvider 類感興趣。而當初定義 Warrior 時,在 Warrior.cpp 文件里的確指明了 WarriorProvider 能限定的當前版本號和最低版本號:();
PLUMA_PROVIDER_SOURCE(Warrior, 1, 1);這些類型信息、版本號限定信息都會被注冊在 Host 的 knownTypes 映射表中,每種接口類型對應一個 ProviderInfo 節點。注冊動作的代碼如下:
【pluma-1.1/src/pluma/PluginManager.cpp】
【pluma-1.1/src/pluma/Host.cpp】
當然,新加的 ProviderInfo 節點的 providers 列表是個空列表,待后續再添加 Provider * 內容。
4.2 pluma.load()
接著,我們繼續看 main () 函數里調用的 pluma.load (),其實調用的是其父類 PluginManager 的 load ()。相關代碼截選如下:
【pluma-1.1/src/pluma/PluginManager.cpp】
可以看到,一開始就在著手加載動態庫,并調用動態庫里的 connect () 函數。前文我們實際上已經列舉過示例代碼里的 connect () 函數了,現在再貼一次:
前文在闡述到 connect () 時,暫時沒有細說 add () 動作,現在我們來看看它的代碼:
【pluma-1.1/src/pluma/Host.cpp】
上面代碼中那個 plumaGetType () 函數其實是 Provider 的私有成員,一般人訪問不了,但 Host 是它的友元類,所以可以訪問。代碼中會先校驗待添加的 Provider 是否合格,如果合格則以 plumaGetType () 返回值為 key 值,并向臨時映射表 addRequests 中添加該 Provider 指針。所謂合格是指,這個 Provider 的類型是 Host 感興趣的,并且其版本號也是合適的。
值得注意的是,待添加的 Provider*,只是臨時先放進一個 addRequests 映射表中。addRequests 映射表的定義如下:
【pluma-1.1/include/pluma/Host.hpp】
那么這個臨時性的 addRequests 映射表的內容會怎樣處理呢?說起來也簡單,會被 “搬移” 進 Host 的 knownTypes 映射表中某個 ProviderInfo 的內部列表去。main () 在調用完 connect () 函數后,調用的 confirmAddictions () 就是做這個事情的:
【pluma-1.1/src/pluma/Host.cpp】
我們畫一張調用關系圖看看:
??
我們可以通過這張調用關系圖回顧一下,主要流程就是在加載插件動態庫,并執行動態庫里的 connect () 函數。該函數會將動態庫里可用的所有 Provider * 記入 Host 的 knownTypes 映射表中。
同時,動態庫對應的 DLibrary 對象,也會插入 Pluma 管理類內部的 libraries 映射表中。
為了鞏固知識,我們把前文的兩張圖再整合一下。
5. 使用插件 Provider
5.1 pluma.getProviders()
在 Providers 都添加進 Pluma 管理類后,我們就可以在需要時獲取 provider 了,為此 Pluma 類提供了 getProviders () 函數:
【pluma-1.1/src/pluma/PluginManager.cpp】
【pluma-1.1/src/pluma/Host.cpp】
代碼很簡單,就是幫使用者把感興趣的某類插件 Provider 全部找出來。如果當初我們已經通過 acceptProviderType () 注冊了對應的類型(PLUMA_PROVIDER_TYPE),那么至少可以拿到一個 list,否則就只能拿到 NULL 了。如果我們可以拿到若干 Provider,就可以調用其 create () 函數創建對應的插件對象了。
當工作做完后,用戶應該及時 delete 掉之前創建出的插件對象。在程序退出之前,用戶應該調用 pluma.unloadAll () 刪除所有插件 Provider 及 DLibrary 對象。DLibrary 對象析構時,會自動關閉已經打開的動態鏈接庫。
【pluma-1.1/src/pluma/PluginManager.cpp】
6. 結束
至此,Pluma 架構的主體代碼就分析完畢了,希望對大家有所幫助。
?
審核編輯:劉清
-
管理器
+關注
關注
0文章
246瀏覽量
18510 -
字符串
+關注
關注
1文章
579瀏覽量
20516 -
開源架構
+關注
關注
0文章
8瀏覽量
6955
原文標題:聊聊Pluma插件管理框架
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論