1. 程式人生 > 其它 >C++中的靜態連結庫與動態連結庫

C++中的靜態連結庫與動態連結庫

基礎知識

extern "C"

使用extern "C",並不代表當前程式碼只能使用C語言的格式及語法,而是告訴編譯器,對作用域內的函式不要進行Name mangling(Name mangling使得C++支援函式過載),而是按照C編譯器的方式去生成符號表符號

為什麼需要extern "C"?

預編譯標頭檔案

Precompiled Headers in C++

靜態連結庫

靜態連結庫,Static Link Library,檔案格式為.lib

以Visual Studio舉例,當專案構建生成靜態連結庫時,會產生Name.lib,以及Name.pdb(略)

當我們構建了一個靜態連結庫要供別人使用時,需要提供兩個檔案

  • 編譯生成的靜態連結庫本身
  • 標頭檔案,標頭檔案中包含了靜態連結庫暴露出來可呼叫的函式

相反的,在VS中,當我們構建一個程式要使用別人的靜態連結庫時,需要做如下的配置

  • 連結器-附加庫目錄:提供lib檔案的路徑
  • 連結器-附加依賴項:提供lib檔案的檔名
  • C/C++-附加包含目錄:提供標頭檔案的路徑

假設我有如下程式碼,它們將產出一個靜態連結庫

// Feilib-Static.h
void fnFeiLibStatic();

// Feilib-Static.cpp (忽略了一些#inlcude預處理指令)
void fnFeiLibStatic() {
	std::cout << "This is FeiLib-Static!!!!!!!" << std::endl;
}

再假設我有如下程式碼,他將呼叫靜態連結庫中的方法,產出一個可執行檔案

// SandBox.cpp
#include <Feilib-Static.h>

int main() {
    fnFeiLibStatic();
    return 0;
}

由於預處理指令本質上是對文字的複製貼上,因此#include <Feilib-Static.h>操作實際上就是在SandBox.cpp檔案中添加了fnFeiLibStatic的函式宣告,成功渡過編譯階段,然後連結器在連結階段,去先前設定的附加依賴項路徑中找到FeiLib-Static.lib,然後將它連結進SandBox.exe

這個可執行檔案中

這裡需要注意的是,連結階段並不會把FeiLib-Static.lib整個塞進SandBox.exe中,而是隻會複製SandBox中用到的objects的二進位制資料到可執行檔案中

由此也可以得出靜態連結庫的缺點

  • 當靜態連結庫本身發生改變時,需要重新編譯可執行檔案才能應用變更
  • 見下文貼出的網站

額外的,在VS中,如果你本人是庫的作者,且你有一個可執行檔案專案用於除錯你的靜態連結庫,那麼你並不需要新增附加庫目錄和附加依賴項,只需要在可執行檔案專案中新增對靜態連結庫專案的引用即可

添加了其他專案引用代表著:在我們生成可執行檔案專案的時候,VS會去檢測對應的引用專案,檢視它們是否有新的更改,若有新的更改那麼會先生成引用專案,然後自動將引用專案輸出的結果供可執行檔案連結,為我們省去了很多麻煩

演練:建立並使用靜態庫

動態連結庫與靜態連結庫相比,優勢和劣勢都在哪裡?

動態連結庫與靜態連結庫有什麼區別?

動態連結庫

上文中提到靜態連結庫會在連結階段將需要的二進位制檔案塞進.exe中。動態連結庫則不同,首先動態連結庫會產生的檔案如下

  • Name.lib:靜態引入庫
  • Name.dll:動態連結庫,包含了實際的函式和資料
  • Name.pdb:

動態連結庫在使用時同樣需要一個匯出函式宣告的標頭檔案;以及一個靜態引入庫,其中記錄了被dll匯出的函式和變數的符號名;在連結階段時,只需要連結引入庫,dll中的函式程式碼和資料並不複製到可執行檔案中,而是在執行時去動態或靜態載入dll

由於dll是在程式執行時再去載入的,那麼這意味著當dll發生更新時,不需要重新編譯可執行程式,只需要對dll檔案進行替換即可

靜態呼叫

通過如下方法進行動態連結庫的靜態呼叫。在可執行檔案連結的時候,會將動態連結庫提供的.lib檔案連結進應用程式中,然後在可執行程式啟動的時候,再將.dll全部全部載入到記憶體中

// 需要在VS的DLL專案中新增FEILIB_EXPORTS預處理巨集
#ifdef FEILIB_EXPORTS
#define FEILIB_API extern "C" __declspec(dllexport)
#else
#define FEILIB_API extern "C" __declspec(dllimport)
#endif

// Core.h
FEILIB_API void foo();

// Core.cpp
void foo() {}
// 提供函式的宣告
#include "src/Core.h"

int main() {
    foo();
}

演練:建立和使用自己的動態連結庫 (C++)

動態呼叫

靜態呼叫需要在程式開始執行的時候就把所有依賴的動態連結庫載入到記憶體中,這可能影響程式的啟動速度;而動態呼叫則是通過程式碼動態的載入動態連結庫,然後手動解除安裝

例如若在Unity中靜態呼叫第三方連結庫,那麼是無法在Unity開啟的情況下去更換此第三方庫的,這個時候就需要使用到動態呼叫

下面演示通過C++動態呼叫動態連結庫

#include <Windows.h>

using FuncType = void(*)(int);
int main()
{
    // 動態載入dll
    if (HINSTANCE loadedDLL = ::LoadLibrary(L"FeiLib.dll"); loadedDLL != nullptr)
    {
        // 查詢dll中的函式
        if (FuncType p = (FuncType)::GetProcAddress(loadedDLL, "add"); p != nullptr)
        {
            // 呼叫函式
            p(10);
        }
        // 解除安裝庫
        FreeLibrary(loadedDLL);
    }
    return 0;
}

強制記憶體對齊

預處理巨集

// 強制該結構體以1B為單位進行對齊 sizeof(T) = 18
#pragma pack(push)
#pragma pack(1)
struct test_struct
{
	char data[10];
	double d;
};
#pragma pack(pop)

使用預處理巨集的對齊引數有最大上限,最大值為預設對齊引數

alignas關鍵字

// 強制該結構體以16B為單位進行對齊 sizeof(T) = 32
struct alignas(16) test_struct {
	char data[10];
	double d;
};

關鍵字的對齊引數有最小下限,最小值為預設對齊引數

C#中struct的對齊方式

// 強制該結構體以1B為單位進行對齊 結構體的大小是9
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct test_struct
{
    public char data;
    public double pi;
}

函式呼叫協議

__stdcall(Windows API標準呼叫協議)與__cdcel(C/C++預設呼叫協議)

  • 共同點:函式都是從右往左入棧
  • 不同點:__stdcall要求函式呼叫結束時,由函式本身清除呼叫棧;__cdcel要求函式呼叫結束時,由呼叫方清除呼叫棧

C++與C#中資料型別對應

C++ C#
std::int32_t Int32
std::int64_t long
const char* string
char char
double double
short short
float float
void* IntPtr,[]
int*, int& ref int

C#呼叫第三方庫

對於Unity來講,第三方庫的位置應該位於Assets\Plugins\;對於C#命令列程式來講,第三方庫的位置應該與可執行檔案的目錄一致

DllImport(靜態呼叫)

  • dllName:代表呼叫的DLL庫的名稱
  • CallingConvention:列舉變數,有CdeclStdCall等,需要與DLL庫中的宣告保持一致,預設是Cdecl
  • CharSet:字串編碼方式,預設傳遞Ascii碼
  • EntryPoint:函式入口名稱,若不指定則預設入口函式與當前函式名字相同
namespace FeiLib.Core
{
    public class FileManager
    {
        [DllImport("FeiLib", EntryPoint = "push_message")]
        public static extern void PushMessage(string path, string data, bool isAppend);
    }
}

動態呼叫

為了能讓程式在執行期間去替換更新DLL,就應該要對DLL進行動態呼叫。那麼我採取的方式是通過封裝一箇中間層DLL,靜態呼叫這個中間層,然後通過中間層去動態載入和解除安裝DLL

// Core.h
#pragma once
#include <Windows.h>
#include <vector>
#include <unordered_map>
#include <thread>
#include <future>
#include <mutex>


#define DLLCALLER_API extern "C" __declspec(dllexport)

namespace DLLCaller
{
    DLLCALLER_API bool LoadDLL(const char* dllPath);

    DLLCALLER_API void* GetMethod(const char* dllPath, const char* methodName);

    DLLCALLER_API bool RemoveDLL(const char* dllPath, const char* freeFuncName = "free_library");

    DLLCALLER_API void RemoveAllDLL(const char* freeFuncName = "free_library");
}
#include "Core.h"

namespace DLLCaller
{
	using FreeFuncType = void(*)();

	/// <summary>
	/// 全域性變數 記錄當前載入的動態連結庫
	/// </summary>
	std::unordered_map<std::string, DLLInstance> instanceMap;

	class NonCopyable
	{
	public:
		NonCopyable() = default;

		NonCopyable(const NonCopyable&) = delete;

		NonCopyable& operator=(const NonCopyable&) = delete;
	};

	class DLLInstance : public NonCopyable
	{
	public:
		DLLInstance(const char* dllPath) : instance(::LoadLibraryA(dllPath)) {}

		DLLInstance(DLLInstance&& another) noexcept : instance(another.instance) {
			another.instance = nullptr;
		}

		~DLLInstance() {
			if (instance != nullptr)
				::FreeLibrary(instance);
		}

		HINSTANCE instance;
	};

	bool LoadDLL(const char* dllPath)
	{
		auto result = instanceMap.emplace(dllPath, dllPath);
		return result.second && result.first->second.instance != nullptr;
	}

	void RemoveAllDLL(const char* freeFuncName)
	{
		for (auto it = instanceMap.begin(); it != instanceMap.end(); ++it)
		{
			// 釋放DLL
			auto pFunc = (FreeFuncType)::GetProcAddress(it->second.instance, freeFuncName);
			if (pFunc != nullptr)
				pFunc();
		}

		instanceMap.clear();
	}

	bool RemoveDLL(const char* dllPath, const char* freeFuncName)
	{

		auto result = instanceMap.find(dllPath);
		if (result != instanceMap.end())
		{
			// 釋放DLL
			auto pFunc = (FreeFuncType)::GetProcAddress(result->second.instance, freeFuncName);
			if (pFunc != nullptr)
			{
				pFunc();
				return instanceMap.erase(dllPath) > 0;
			}
		}
		return false;
	}

	void* GetMethod(const char* dllPath, const char* methodName)
	{
		std::string str = dllPath;

		// 動態連結庫未被載入
		if (instanceMap.find(str) == instanceMap.end())
		{
			bool loadResult = LoadDLL(dllPath);
			if (loadResult == false)
				return nullptr;
		}

		return ::GetProcAddress(instanceMap.find(str)->second.instance, methodName);
	}
}

若在DLL中開啟了新的執行緒,那麼只有當執行緒執行完畢時,它才能正確的被解除安裝,否則process detach不會被執行

其他

開擺了 下次一定