基礎知識
extern "C"
使用extern "C",並不代表當前代碼只能使用C語言的格式及語法,而是告訴編譯器,對作用域內的函數不要進行Name mangling(Name mangling使得C++支持函數重載),而是按照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();
}
動態調用
靜態調用需要在程序開始運行的時候就把所有依賴的動態鏈接庫加載到內存中,這可能影響程序的啟動速度;而動態調用則是通過代碼動態的加載動態鏈接庫,然后手動卸載
例如若在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:枚舉變量,有
Cdecl
,StdCall
等,需要與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不會被執行
其他
開擺了 下次一定