C++利用模板在Windows上快速調用DLL函數

更新日誌 —————————
2021/08/01 更新V2.2 增加 GetHmodule 函數 – 允許用戶獲取HMODULE以驗證載入DLL是否成功。
2021/08/03 更新V2.3 增加 GetProcAddress_XXXX 函數 – 允許用戶快捷地、在無需顯式類型轉換的情況下獲取某個DLL函數的地址,以便為單個函數的頻繁調用節省時間和程式碼,提高程式效率。

一直覺得用winapi動態調用dll很麻煩,所以乾脆利用c++的模板函數等功能,寫了一個類,用以快速調用DLL函數、快速獲取函數地址。

  此類的程式碼都在一個頭文件中。當前版本有三大功能:調用DLL函數、快捷獲取DLL函數地址、根據原函數資訊生成C++方式修飾的函數名(即用C++方式導出到DLL中的函數名,類似這種:?XXX@YAHHJ@Z)。第二個功能尚不完善,目前不支援類成員函數名的生成、不支援參數中有結構體(struct)、枚舉(enum)、引用(reference)(只是不支援生成名字,但支援調用,可以用dumpbin工具可獲取dll中的函數列表)。

  頭文件完整程式碼:

/************************************
*  Fast Dll Runner V2.3 for CPP11+  *
*         Author: szx0427           *
*         Date: 2021/08/03          *
************************************/

#pragma once

//#include "pch.h"    /* Uncomment this line if your project has a pch.h (VS2017+) */
//#include "stdafx.h" /* Uncomment this line if your project has a stdafx.h (Old version) */
#include <Windows.h>
#include <tchar.h>
#include <cassert>
#include <string>
#include <map>

class CSzxRunDll2
{
public: // Public data types
	enum CallingConventions
	{
		Stdcall,
		Cdecl,
		Fastcall
	};

protected:
	static const std::string m_Prefix[3];
	static std::map<std::string, std::string> m_map;
	static void _InitMap()
	{
		if (m_map.size() > 0)
			return;
		m_map["void"] = "X";
		m_map["char"] = "D";
		m_map["unsigned char"] = "E";
		m_map["short"] = "F";
		m_map["unsigned short"] = "G";
		m_map["int"] = "H";
		m_map["unsigned int"] = "I";
		m_map["long"] = "J";
		m_map["unsigned long"] = "K";
		m_map["__int64"] = "_J";
		m_map["unsigned __int64"] = "_K";
		m_map["float"] = "M";
		m_map["double"] = "N";
		m_map["bool"] = "_N";
		m_map["*"] = "PA";
		m_map["const *"] = "PB";
		m_map["2 *"] = "0";
		m_map["2 const *"] = "1";
	}

protected: // Protected fields and methods
	HMODULE m_hModule;
	template <class _ReturnType, class _FxnType, class... _ParamTypes>
	_ReturnType _basicCallDllFunc2(LPCSTR lpFxnName, const _ParamTypes&... args)
	{
		assert(m_hModule);
		assert(lpFxnName);
		_FxnType _Myfxn = (_FxnType)::GetProcAddress(m_hModule, lpFxnName);
		assert(_Myfxn);
		return _Myfxn(args...);
	}

public: // Public methods
	CSzxRunDll2(LPCTSTR lpDllName = nullptr)
		: m_hModule(NULL)
	{
		if (lpDllName)
			m_hModule = LoadLibrary(lpDllName);
		_InitMap();
	}
	virtual ~CSzxRunDll2()
	{
		UnloadDll();
	}
	UINT LoadDll(LPCTSTR lpDllName)
	{
		assert(lpDllName);
		HMODULE hMod = LoadLibrary(lpDllName);
		if (hMod)
		{
			if (m_hModule)
				UnloadDll();
			m_hModule = hMod;
		}
		return GetLastError();
	}
	UINT UnloadDll()
	{
		FreeLibrary(m_hModule);
		return GetLastError();
	}
	HMODULE GetHmodule() const // ++ v2.2
	{ return m_hModule; }

	template <class _ReturnType = void, class... _ParamTypes>
	_ReturnType CallDllFunc2_stdcall(LPCSTR lpFxnName, const _ParamTypes &..._Params)
	{
		return _basicCallDllFunc2<_ReturnType, _ReturnType(__stdcall*)(const _ParamTypes ...), _ParamTypes...>
			(lpFxnName, _Params...);
	}
	template <class _ReturnType = void, class... _ParamTypes>
	_ReturnType CallDllFunc2_cdecl(LPCSTR lpFxnName, const _ParamTypes &..._Params)
	{
		return _basicCallDllFunc2<_ReturnType, _ReturnType(__cdecl*)(const _ParamTypes ...), _ParamTypes...>
			(lpFxnName, _Params...);
	}
	template <class _ReturnType = void, class... _ParamTypes>
	_ReturnType CallDllFunc2_fastcall(LPCSTR lpFxnName, const _ParamTypes &..._Params)
	{
		return _basicCallDllFunc2<_ReturnType, _ReturnType(__fastcall*)(const _ParamTypes ...), _ParamTypes...>
			(lpFxnName, _Params...);
	}
	template <class _ReturnType = void, class... _ParamTypes>
	_ReturnType CallDllFunc2_thiscall(LPCSTR lpFxnName, const _ParamTypes &..._Params)
	{
		return _basicCallDllFunc2<_ReturnType, _ReturnType(__thiscall*)(const _ParamTypes ...), _ParamTypes...>
			(lpFxnName, _Params...);
	}

	// GetProcAddress_XXXX: ++v2.3
	template <class _ReturnType = void, class... _ParamTypes>
	auto GetProcAddress_stdcall(LPCSTR lpProcName)
	{
		assert(m_hModule);
		return (_ReturnType(__stdcall*)(_ParamTypes...))::GetProcAddress(m_hModule, lpProcName);
	}

	template <class _ReturnType = void, class... _ParamTypes>
	auto GetProcAddress_cdecl(LPCSTR lpProcName)
	{
		assert(m_hModule);
		return (_ReturnType(__cdecl*)(_ParamTypes...))::GetProcAddress(m_hModule, lpProcName);
	}

	template <class _ReturnType = void, class... _ParamTypes>
	auto GetProcAddress_fastcall(LPCSTR lpProcName)
	{
		assert(m_hModule);
		return (_ReturnType(__fastcall*)(_ParamTypes...))::GetProcAddress(m_hModule, lpProcName);
	}

	template <class _ReturnType = void, class... _ParamTypes>
	auto GetProcAddress_thiscall(LPCSTR lpProcName)
	{
		assert(m_hModule);
		return (_ReturnType(__thiscall*)(_ParamTypes...))::GetProcAddress(m_hModule, lpProcName);
	}

	template <class _ReturnType = void, class... _ParamTypes>
	static std::string BuildCppDecoratedName(const std::string& sFxnName, CallingConventions cc = Cdecl)
	{
		_InitMap();
		std::string ret = "?" + sFxnName + m_Prefix[(int)cc];
		const char* szTypes[] = { typeid(_ReturnType).name(), typeid(_ParamTypes).name()... };
		int nCount = 1 + sizeof...(_ParamTypes);
		std::string tmp, par;
		int pos1, pos2, sum = 0;
		for (int i = 0; i < nCount; i++)
		{
			tmp = szTypes[i];
			// This version doesn't support struct/enum/reference
			assert(tmp.find("struct") == tmp.npos && tmp.find("enum") == tmp.npos && tmp.find("&") == tmp.npos);
			assert(tmp.find('[') == tmp.npos); // Array(x) Pointer(√)
			if ((pos1 = tmp.find(" *")) != tmp.npos)
			{
				if ((pos2 = tmp.find(" const *")) != tmp.npos)
				{
					if (i >= 1 && tmp == szTypes[i - 1])
						par += m_map["2 const *"];
					else
						par += m_map["const *"] + m_map[tmp.substr(0, pos2)];
				}
				else
				{
					if (i >= 1 && tmp == szTypes[i - 1])
						par += m_map["2 *"];
					else
						par += m_map["*"] + m_map[tmp.substr(0, pos1)];
				}
			}
			else
				par += m_map[tmp];
		}
		if (par.length() == 1)
			par += "XZ";
		else
			par += "@Z";
		ret += par;
		return ret;
	}
};

const std::string CSzxRunDll2::m_Prefix[3] = { "@@YG","@@YA","@@YI" };
std::map<std::string, std::string> CSzxRunDll2::m_map;

要使用此類, 只需要引入包含以上程式碼的頭文件.

其中:

  1. 構造函數和LoadDll函數可以載入一個DLL文件到類中.
  2. 析構函數和UnloadDll函數可以釋放當前類中的DLL.
  3. GetHmodule函數(v2.2)可以獲取當前類中的HMODULE,以驗證載入DLL的操作是否成功。
  4. CallDllFunc2_XXXX可以快速調用當前類中DLL中的函數.下劃線後面的部分指定了調用約定(stdcall/cdecl/fastcall/thiscall). 這些函數模板的第一個模板參數為返回值類型,可不填,默認為void. 後面的模板參數為dll函數參數類型列表,無需手動填入, 會根據具體函數參數自動識別. 實例化時,函數的第一個參數為DLL函數名稱(注意,如果是C++方式導出的函數,需要填入修飾過的名稱), 後面的參數是dll函數的參數列表(本函數模板使用了引用,保證不進行多餘的數據複製浪費時間,數據直接傳遞給dll函數). C語言方式導出函數的調用示例:
CSzxRunDll2 dll(TEXT("user32.dll"));
int nRet = dll.CallDllFunc2_stdcall<int>("MessageBoxA", NULL, "Hello World!", "Title", MB_ICONINFORMATION);

GetProcAddress_XXXX可以快速獲取當前類中某個DLL函數的地址。下劃線後面的部分指定了調用約定(stdcall/cdecl/fastcall/thiscall). 這些函數模板的第一個模板參數為返回值類型,默認為void。後面的模板參數是函數從參數類型列表,如函數沒有參數,則無需填寫。實例化時,第一個函數參數時DLL函數的名稱。該函數的返回值類型會自動根據模板參數計算,無需手動填寫,程式碼中使用auto即可。下面是一個示例:

CSzxRunDll2 dll(TEXT("user32.dll"));
auto MyMessageBox = dll.GetProcAddress_stdcall<int, HWND, LPCSTR, LPCSTR, UINT>("MessageBoxA");
MyMessageBox(NULL, "第一次使用MessageBox!", "11111", MB_OK);
MyMessageBox(NULL, "第二次使用MessageBox!", "22222", MB_ICONASTERISK);

使用GetProcAddress_XXXX函數時請注意:由於本類的析構函數中會自動執行FreeLibrary函數釋放DLL庫,在一個類對象被析構後,使用它獲取的函數地址將可能失效(大概率會失效)。所以,請保證一個對象獲取的函數地址不會在它被析構後調用。下面是一個錯誤示範:

auto Fun1()
{ CSzxRunDll2 dll(TEXT("XXX.dll")); return dll.GetProcAddress_cdecl<int, int, int>("Add"); }
void main()
{
	auto fun = Fun1();
	int n = fun(1, 2); // 可能崩潰
	std::cout << n;
}

此例中,Fun1函數中雖獲取了Add函數的地址,但在Fun1函數返回之前,對象「dll」已執行了析構函數,它構造時載入的「XXX.dll」已被析構函數中執行的FreeLibrary釋放,導致原有的函數地址失效。之所以說「可能崩潰」,是因為如果在一個類對象載入DLL前,就已載入過該DLL,則使用一次FreeLibrary不會將其釋放,只會減少引用次數,所以原函數地址依然有效。如果要在多個函數內調用一個函數地址,請將CSzxRunDll2對象定義為全局變數或靜態變數。

BuildCppDecoratedName函數BuildCppDecoratedName模板為靜態成員函數模板, 作用是根據原函數的資訊生成C++方式修飾過的函數名稱. 有兩種調用方法:

  1. 通過對象調用: 類對象 . BuildCppDecoratedName<…>(…);
  2. 直接調用: CSzxRunDll2::BuildCppDecoratedName<…>(…);

第一個模板參數為dll函數返回值類型, 後面的是dll函數參數類型列表. 實例化時, 第一個函數參數是dll函數名稱, 第二個參數是調用約定, 可以為以下三個值中任意一個: CSzxRunDll2::Stdcall / CSzxRunDll2::Cdecl / CSzxRunDll2::Fastcall, 可不填, 默認為cdecl. 以下是一個使用該函數調用C++方式導出函數的例子:

CSzxRunDll2 dll(TEXT("math.dll"));
// 要調用的函數原型: long __cdecl Add(int a, int b);
std::string str = dll.BuildCppDecoratedName<long, int, int>("Add", CSzxRunDll2::Cdecl);
std::cout << "Result: " << dll.CallDllFunc2_cdecl<long>(str.c_str(), 111, 22);

已知問題:

  1. BuildCppDecoratedName不支援DLL函數參數中有結構體(struct)、枚舉(enum)、引用(reference).
  2. 在使用CallDllFunc2_XXX函數調用參數列表中帶有引用(reference)的DLL函數時, 若按上述的常規方式會出現錯誤, 必須使用結構體傳參, 如下所示:
// 要調用的函數原型: void Add(const int &a, const int &b, long &result);
int re;
struct MyStruct
{
	const int& a;
	const int& b;
	int& result;
} param = { 222, 3000, re };
const char* name = "?Add@@YAXABH0AAJ@Z"; // 修飾過的函數名, 帶引用的目前本類不支援生成,可使用dumpbin工具生成
dll.CallDllFunc2_cdecl(name, param);

此類還在繼續開發完善中. 若有任何問題, 歡迎指正.

版權說明
本文由szx0427撰寫,由Icys獲得授權後在CnBlogs代為其發布發布。

知乎通道