C++Helper--動態庫的顯式調用

  本文主要介紹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

參考資料

  《深入應用C++11:代碼優化與工程級應用

另,更多源碼見GitHub:https://github.com/deargo/cpphelper

 

 

 

發佈了16 篇原創文章 · 獲贊 11 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章