本文主要介紹C++動態庫的顯式調用方法,及其封裝。
本文源碼見【完整代碼】章節,或GitHub:https://github.com/deargo/cpphelper。
動態庫和靜態庫
動態庫全稱動態鏈接庫(dynamic link library),他包含了函數所在的DLL文件和文件中函數位置的信息(入口),在運行時被加載。靜態庫全稱靜態鏈接庫(static link library),他包含函數代碼本身,在編譯時直接將代碼加入程序當中。
從字面意思來看,區別就是靜態和動態,而這裏的靜態和動態,指的是庫的鏈接階段。可以看如下的編譯過程:
- 靜態庫:在鏈接階段庫將會與目標彙編後的目標文件.o一起打包生成可執行文件。成爲可執行文件的一部分,後續此庫就可以消失了。也就是說在編譯的最後一步(鏈接階段),如果程序需要使用靜態庫,在這一步都會一起打包到可執行文件中。
- 動態庫:而動態庫在編譯階段都不會有什麼動作,只有在程序運行時才被加載,也就是動態庫的鏈接是發生在程序運行時期的,它和可執行文件是分開的,只是可執行文件在運行的某個時期調用了它。
和靜態庫比較,動態庫並沒有在編譯的時候被編譯進目標代碼中,程序在執行相關函數時才調用該函數庫裏相應函數,因此使用動態庫的可執行文件比較小。
由於函數庫沒有被整合進可執行文件,而是程序運行時候動態申請並調用,所以程序的運行環境中必須提供相應的庫。動態函數庫的改變並不影響可執行文件,所以動態函數庫升級比較方便。
動態庫的調用方式
動態庫的調用方式有兩種:動態調用(又叫顯示調用)、靜態調用(又叫隱式調用):
1、隱式調用需要調用者寫的代碼量少,調用起來和使用當前項目下的函數一樣直接;而顯式調用則要求程序員在調用時,指明要加載的動態庫的名稱和要調用的函數名稱。
2、隱式調用由系統加載完成,對程序員透明;顯式調用由程序員在需要使用時自己加載,不再使用時,自己負責卸載。
3、由於顯式調用由程序員負責加載和卸載,好比動態申請內存空間,需要時就申請,不用時立即釋放,因此顯式調用對內存的使用更加合理, 大型項目中應使用顯示調用。
4、當動態鏈接庫中只提供函數接口,而該函數沒有封裝到類裏面時,如果使用顯式調用的方式,調用方甚至不需要包含動態鏈接庫的頭文件,而使用隱式調用時,則調用方不可避免要加上動態庫中的頭文件,編譯時還需指明包含的頭文件的位置。
需要注意的是,當動態鏈接庫中的接口函數是作爲成員函數封裝在類裏面時,即使使用顯式調用的方式,調用方也必須包含動態庫中的相應頭文件。
5、顯式調用更加靈活,可以模擬多態效果。
顯式調用是應用程序在執行過程中隨時可以加載DLL文件,也可以隨時卸載DLL文件,這是隱式鏈接所無法作到的,所以顯式鏈接具有更好的靈活性,對於解釋性語言更爲合適。
動態庫的顯式調用
在應用程序中,分三步走:加載動態庫、獲取函數並調用、卸載動態庫。
在Windows系統中,與動態庫調用有關的函數包括:
- 調用 LoadLibrary(或相似的函數)以加載 DLL 和獲取模塊句柄。
- 調用 GetProcAddress 將符號名或標識號轉換爲DLL內部地址,以獲取指向應用程序要調用的,每個導出函數的函數指針。
- 使用完 DLL 後調用 FreeLibrary。
在Linux中,#include <dlfcn.h>,提供了下面幾個接口:
- void * dlopen( const char * pathname, int mode ):函數以指定模式打開指定的動態連接庫文件,並返回一個句柄給調用進程。
- void* dlsym(void* handle,const char* symbol):dlsym根據動態鏈接庫操作句柄(pHandle)與符號(symbol),返回符號對應的地址。使用這個函數不但可以獲取函數地址,也可以獲取變量地址。
- int dlclose (void *handle):dlclose用於關閉指定句柄的動態鏈接庫,只有當此動態鏈接庫的使用計數爲0時,纔會真正被系統卸載。
- const char *dlerror(void):當動態鏈接庫操作函數執行失敗時,dlerror可以返回出錯信息,返回值爲NULL時表示操作函數執行成功。
以Windows平臺爲例,調用過程如下:
void TestDll()
{
typedef int(*pMax)(int a, int b);
typedef int(*pGet)(int a);
HINSTANCE hDll = LoadLibraryA("mydll.dll");
if (hDll == nullptr)
return;
pMax Max = (pMax)GetProcAddress(hDll, "Max");
if (Max == nullptr)
return;
int ret = Max(5, 8);
pGet Get = (pGet)GetProcAddress(hDll, "Get");
if (Get == nullptr)
return;
int ret = Get(5);
FreeLibrary(hDll);
}
顯式調用的優化
顯式調用雖然好處多多,但實則繁瑣:需要先定義一個函數指針,然後再根據名稱獲取函數地址,最後調用。如果一個dll中有上百個函數,這種繁瑣的定義會讓人不勝其煩。
其實獲取函數地址和調用函數的過程是重複邏輯,應該消除,我們不希望每次都定義一個函數指針和調用GetProcAddress,應該用一種簡潔通用的方式去調用dll中的函數。
我們希望調用dll中的函數就像調用普通的函數一樣,傳入一個函數名稱和函數的參數就可以實現函數的調用。類似於:
Ret CallDllFunc(const string& funName, T arg);
如果以這種方式調用,就能避免繁瑣的函數指針定義以及反覆地調用GetProcAddress了。下面介紹一種可行的解決方案:
首先要把函數指針轉換成一種函數對象或泛型函數,這裏可以用std::function去做這個事情,即通過一個函數封裝GetProcAddress,這樣通過函數名稱就能獲取一個泛型函數std::function,希望這個function是通用的,不論dll中是什麼函數都可以轉換成這個std::function,最後調用這個通用的function即可。
這裏還有兩個問題需解決:
1,函數的返回值可能是不同類型,如果以一種通用的返回值來消除這種不同返回值導致的差異呢?
2,函數的入參數目可能任意個數,且類型也不盡相同,如何來消除入參個數和類型的差異呢?
首先看一下如何封裝GetProcAddress,將函數指針轉換成std::function,代碼如下:
template <typename T>
std::function<T> GetFunction(const string& funcName)
{
FARPROC funAddress = GetProcAddress(m_hMod, funcName.c_str());
return std::function<T>((T*)(funAddress));
}
其中T是std::function的模板參數,即函數類型的簽名。如果要獲取上面例子中的Max和Get函數,可以這樣:
auto fmax = GetFunction<int(int, int)>("Max");
auto fget = GetFunction<int(int)>("Get");
這種方式比之前先定義函數指針在調用GetProcAddress的方式更簡潔通用。
再看看如何解決函數返回值和入參不統一的問題,通過result_of和可變參數模板來解決,最終的調用函數如下:
template <typename T, typename... Args>
typename std::result_of<std::function<T>(Args...)>::type ExcecuteFunc(const string& funcName, Args&&... args)
{
return GetFunction<T>(funcName)(arg...);
}
上面的例子中要調用Max和Get函數,這樣就行了:
auto max = ExecuteFunc<int(int, int)>("Max", 5, 8);
auto ret = ExecuteFunc<int(int)>("Ret", 5);
比之前的調用方式簡潔直觀多了,沒有了繁瑣的函數指針的定義,沒有了反覆的調用GetProcAddress及其轉換和調用。
實現的關鍵是如何將一個FARPROC變成一個函數指針複製給std::function,然後再調用可變參數執行。函數的返回值通過std::result_of來泛化,使得不同的返回值的dll函數都可以用相同的方式來調用。
完整代碼
優化後的完整代碼如下:
#pragma once
#include <string>
#include <unordered_map>
#include <functional>
#ifdef _WIN32
# include <Windows.h>
# include <libloaderapi.h>
# pragma warning(disable:4297)
#else
# include <dlfcn.h>
#endif
using namespace std;
namespace CppHelper
{
class CLibrary
{
public:
#ifdef _WIN32
typedef HMODULE Object;
typedef HANDLE Symbol;
#else
typedef void* Object;
typedef void* Symbol;
#endif
CLibrary():m_lib(nullptr)
{
}
CLibrary(const string& libarayPath):m_lib(nullptr)
{
load(libarayPath);
}
virtual ~CLibrary()
{
unload();
}
bool load(const string& libarayPath)
{
unload();
#ifdef _WIN32
m_lib = LoadLibraryA(libarayPath.c_str());
#else
m_lib = dlopen(libarayPath.c_str(), RTLD_NOW);
#endif
return nullptr != m_lib;
}
bool unload()
{
m_map.clear();
if (m_lib == nullptr)
{
return true;
}
#ifdef _WIN32
FreeLibrary(m_lib);
#else
dlclose(m_lib);
#endif
if (!m_lib)
{
return false;
}
m_lib = nullptr;
return true;
}
Object getObject()
{
return m_lib;
}
Symbol getSymbol(const string& funcName)
{
#ifdef _WIN32
Symbol addr = GetProcAddress(m_lib, funcName.c_str());
#else
Symbol addr = dlsym(m_lib, funcName.c_str());
if (NULL != dlerror())
{
addr = nullptr;
}
#endif
return addr;
}
bool exist(const string& funcName)
{
Symbol symbol = getSymbol(funcName);
return nullptr != symbol;
}
template <typename T>
std::function<T> getFunction(const string& funcName)
{
auto it = m_map.find(funcName);
if (it == m_map.end())
{
Symbol symbol = getSymbol(funcName.c_str());
if (!symbol)
{
return nullptr;
}
m_map.insert(std::make_pair(funcName, symbol));
it = m_map.find(funcName);
}
return std::function<T>((T*) (it->second));
}
template <typename T, typename... Args>
typename std::result_of<std::function<T>(Args...)>::type execute(const string& funcName, Args&&... args)
{
auto func = getFunction<T>(funcName);
if (func == nullptr)
{
throw string("can not find this function: ") + funcName;
}
return func(std::forward<Args>(args)...);
}
private:
Object m_lib;
std::unordered_map<string, Symbol> m_map;
};
}
測試代碼
動態庫頭文件代碼
文件mydll.h。
#pragma once
#ifdef _WIN32
# ifdef EXPORT_API
# define EXPORT_API __declspec(dllexport)
# else
# define EXPORT_API __declspec(dllimport)
# endif
#else
# define EXPORT_API
#endif //WIN32
extern "C" int EXPORT_API Add(int x, int y);
extern "C" int EXPORT_API Max(int x, int y);
動態庫實現文件
文件mydll.cpp。
#include "myDll.h"
int Add(int x, int y)
{
return x + y;
}
int Max(int x, int y)
{
return x > y ? x : y;
}
動態庫vs2019編譯:
項目->屬性->常規->配置類型:動態庫(.dll)
動態庫linux編譯:
腳本文件:sh mydll.sh。記得添加-fPIC和-shared參數。
#!/bin/bash
rm -fr libmydll.so
g++ -std=c++11 -fPIC -shared -o libmydll.so mydll.cpp
動態庫測試代碼
記得動態庫路徑爲絕對路徑,或者當前路徑。
#include <string>
#include <iostream>
#include "library.hpp"
using namespace std;
int main(int argc, char *argv[])
{
cout << "enter main..." << endl;
#ifdef _WIN32
CppHelper::CLibrary lib("mydll.dll");
#else
CppHelper::CLibrary lib("libmydll.so");
#endif
cout << "dll add exsit: " << lib.exist("add")<< endl;
auto fmax = lib.getFunction<int(int, int)>("Max");
cout << "dll fMax: "<<fmax(1,5)<<endl;
cout << "dll fAdd: "<<lib.execute<int(int, int)>("Add", 5, 8)<< endl;
cout << "exit main." << endl;
return 0;
}
測試運行:
加載動態庫需-ldl和LD_LIBRARY_PATH.
#!/bin/bash
rm -fr test.out
g++ -std=c++11 main.cpp -L./ -lmydll -ldl -o test.out
if [ -f "test.out" ]; then
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
./test.out
fi
參考資料
另,更多源碼見GitHub:https://github.com/deargo/cpphelper。