透過QLibrary載入動態函式庫並利用Qt Plugin設計軟體插件
===
參考原文:
+ [QLibrary Document](http://doc.qt.io/qt-5/qlibrary.html)
+ [QPluginLoader](http://doc.qt.io/qt-5/qpluginloader.html#details)
QLibrary是一個能夠在執行階段透過指定檔案名稱或是路徑來進行動態共享函式庫(shared Library,如:.dll或.so)載入的一個物件,常見且重要的應用就是可以透過這個物件直接指定載入某個資料夾底下的所有共享函式庫。
不過其實Qt在軟件插件的部份還透過了QPluginLoader進一步將QLibrary封裝,可以進一步的實現軟體插件,筆記將在這篇文的後半解析。
## QLibrary
QLibrary的文件對這個類別的敘述非常重要,所以在筆記一開始要先把它翻譯並且重點整理。
> 一個`QLibrary`類別的實例對於一個共享函式庫做動作,`QLibrary`提供了一個獨立的途徑來存取在函式庫中的函式。你可以對類別的建構子傳入檔案名稱,或透過`QLibrary::setFileName()`來做明確的設定。當載入函式庫時,`QLibrary`會去搜索系統指定的函式庫存放位置,除非送入的參數是一個檔案的絕對位置,也就是說,可以利用傳入絕對位置來指定`QLibrary`載入的對象。
> 如果filename是一個絕對位置,那麼`QLibrary`將會最先載入這個位置,但是如果檔案不能被找到,`QLibrary`將會透過檔案名稱在不同平台上的檔案類型,就像在Mac或Unix上的".lib"以及Mac上的".dylib"、Unix上的".so"還有Windows上的".dll"。
> 如果filename並非絕對位置,那麼QLibrary將會改變搜尋方法,系統將會嘗試在指定的位置,並加入系統指定的前綴(prefix)或是後綴(suffix)來尋找指定文件。
> 盡量讓`QLibrary`在識別文件的時候僅透過他們的基本檔案名(basename)(沒有檔案型態後綴),這樣的話可以讓相同的程式碼在不同的作業系統上面運行,而且可以減少尋找目標函式庫的次數。
### 在Qt上編寫一個共享函式庫(shared library)
參考文章:[Creating Shared Libraries](http://doc.qt.io/qt-5/sharedlibrary.html)
首先我們要先選擇"New Project">"Library">"C++ Library"然後我們創件一個專案叫做sharedLibrary。
當創建完成之後我們就會得到這些檔案。
```
├── sharedlibrary.cpp
├── sharedlibrary_global.h
├── sharedlibrary.h
└── sharedLibrary.pro
```
不過我們要有一些修改,在**sharedLibrary.h**裡面我們可以發現Qt Creator預設幫我們寫了一個class不過我們現在是要寫函式庫所以暫時不需要定義class。
所以我們要將shareLibrary.h裡的內容改成下面這樣,並把shareLibrary中的function body定義刪除。不過也可以將function head定義在.h中並把function body移到.cpp檔中。
```cpp
#ifndef SHAREDLIBRARY_H
#define SHAREDLIBRARY_H
#include "sharedlibrary_global.h"
#include <QDebug>
extern "C" SHAREDLIBRARYSHARED_EXPORT void share () {
qDebug()<<"shareLibCalled" ;
}
#endif // SHAREDLIBRARY_H
```
至於如何產生一個Library請見QtDocument:[Creating Shared Library](http://doc.qt.io/qt-5/sharedlibrary.html)
當改好之後我們就可以進行建置(build),如果使用的環境是Linux的話,因為建置步驟涉及函式庫安裝,所以這個步驟希望在terminal底下進行:
```shell
~$ qmake
```
```shell
~$ make
```
```shell
~$ sudo make install
```
這個指令下完之後你就會發現在**/usr/lib**底下出現編譯完成的檔案。如:`libsharedLibrary.so.1.0.0`
如果使用的環境是Windows的話,就直接將.dll放置在想要的地方。
### 透過系統指定位置去尋找目標函式庫
我們直接來試試看`QLibrary`的效果。
```cpp
QLibrary sharedLibrary("shareLibrary") ;
shareLibrary.load();
if ( ! shareLibrary.isLoaded() )
{
qDebug() << shareLibrary.errorString() ;
}
typedef void (*shareFunction) () ;
shareFunction share = (shareFunction) shareLibrary.resolve("share") ;
if ( share )
{
share() ;
}
else
{
qDebug() << shareLibrary.errorString() ;
}
```
`QLibrary::load()`用於動態載入,`QLibrary::isLoaded()`用來確定共享函式庫是否載入成功,`QLibrary::resolve()`用來解析函式庫中的symbol。如果要卸載(unload)函式庫的話必須使用`QLibrary::unload()`,否則這個函式庫會一直保留在記憶體中直到應用程式停止。不過若是有其他的`QLibrary`實例正在使用相同的函式庫,那麼`QLibrary::unload()`將會失效,只有所有載入相同的函式庫的`QLibrary`都卸載,卸載的動作才會被執行。
如前面所說,如果傳給建構子的參數不是絕對路徑的話,QLibrary就會去系統指定的地方尋找,並自動給予系統指定的前後綴,如:傳入建構子的字串為"shareLibrary"但是系統呼叫的卻是"libshareLibrary.so"就是這個原因。
### 透過絕對路徑去載入共享函式庫
在一開始的類別特性的那堆文字中有說到,如果提供QLibrary的參數為絕對路徑,那麼它就會依照絕對路徑去載入共享資料庫。
假設剛才我們編譯出來的共享函式庫不是利用`make install`去將函式庫放進系統指定位置,而是放在我們的專案資料夾下面,那麼我們也可以這樣做。
```cpp
QLibrary lib("/PATH/TO/sharedLibrary.so") ;
```
## Qt Plugin與QPluginLoader
### Qt Plugin開發
如果想要進行Plugin的開發,那麼必須先有一個Plugin Interface,然後讓所有的Plugin繼承這個Interface。
首先先來建立一個Plugin的Interface,這個interface需要繼承:
```cpp
#include <QtPlugin>
class PluginInterface
{
public :
PluginInterface () ;
virtual ~PluginInterface () ;
virtual void pluginInit () ;
}
Q_DECLARE_INTERFACE(PluginInterface,"this.is.example") ;
// Q_DECLARE_INTERFACE( className , Identifier ) ;
```
之後要要開發的Plugin就直接繼承`PluginInterface`,這樣子繼承出來的plugin就等於是QObject以及PluginInterface的衍生類別:
```cpp
#include <QtPlugin>
#include <QObject>
#include "plugininterface.h"
class PluginAdd : public PluginInterface , public QObject
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "this.is.example" FILE "pluginadd.json")
Q_INTERFACES(PluginInterface)
public :
pluginAdd () ;
virtual void pluginInit() ; // PluginInterface的實做
}
```
這樣就定義出了一個Plugin的實做。
### QPluginLoder
透過`QPluginLoader`類別能夠動態加載Qt Plugin,因為Qt Plugin也會以共享函式庫的方式存在,因此`QPluginLoader`其實就是`QLibrary`的進一步封裝。
這裡就要來解釋一下如何動態載入插件:
首先我們需要將**plugininterface.h**保留起來,然後在需要的地方引入。
```cpp
#include "plugininterface.h"
#include <QPluginLoader>
//...(ignore)...
QPluginLoader loader("/PATH/TO/PLUGIN.so") ;
loader.load();
PluginInterface *plugin ;
if ( !loader.isLoaded() ) {
qDebug() << loader.errorString();
}
else
{
QObject *object = pluginLoaded.instance() ;
if ( object ) {
plugin = qobject_cast<PluginInterface *>(object);
plugin -> pluginInit() ;
}
}
```
這樣就可以成功將插件動態載入到程式中。如果要載入某資料夾底下所有的`.so`檔作為插件,那麼就可以利用`QFile`或`QDir`等處理文件系統的類別來做處理就完成了。