Loading

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不会被执行

其他

开摆了 下次一定

posted @ 2022-03-23 09:18  _FeiFei  阅读(911)  评论(0编辑  收藏  举报