[開源世界]從自動導出動態連接庫接口看C++的缺點

自動導出動態連接庫接口在C++編程中絕對是一件煩人的事情,由於你不得不大量的重複如下幾個步驟:
1.加載動態連接庫
2.定義導出函數指針定義
3.定義導出函數指針變量
4.從動態連接庫中導出函數
5.編寫導出函數的封裝函數
6.卸載動態連接庫

有不少人試圖將這個過程自動化,然而所有折戟沉沙。我認爲終極緣由在於動態連接庫在導出函數的時候沒有包含參數信息,而在使用的時候不得不將導出函數進行強制轉型。

在C#中導出動態連接庫的函數相比C++來講要簡單的多:
1. 使用DllImportAttribute屬性規定導出函數的文件路徑、導出點、堆棧調用方式等
2.定義導出函數的C#函數原型

說簡單是代碼簡單,不是實現過程簡單。從表面上看,C#的這兩部實現了C++的那6步,從原理上來看,C#仍然須要C++的那6步。C#的編譯器把二、三、四、5的步驟封裝了,在C#代碼編譯成中間語言的時候進行了展開。C#在定義導出函數的函數原型的時候須要作一件事,就是判斷C#參數類型和C++參數類型的對應關係,爲何要特別注意這一點呢?就是由於C#在展開的時候須要準確的C++函數定義,不然調用過程當中會出現錯誤。

Java中有一個可以調用本地動態連接庫的組件——JNative,雖然我沒有看過它具體是怎麼實現的,可是從我瞭解到的隻字片語中推斷JNative參考了C#的實現過程並原模原樣的復刻了一份——由於JNative在實現的時候仍然須要判斷Java參數類型和C++參數類型的對應關係。

C#的實現方式和Java的實現方式在本質上和C++沒有區別,可是在編碼過程當中要簡單許多,由於C#和Java的編譯器在重複代碼的處理上比C++作了更多的工做。C++固然也能處理重複代碼,只不過不是經過編譯器,而是經過宏。

我按照C#的思路實做了C++自動導出函數的簡單版本。 編程

#pragma once

#include <WTypes.h>

#include <list>
using namespace std;

class DllImportAttribute
{
public:
	// 加載動態連接庫
	virtual BOOL Init() = 0;

	// 卸載動態連接庫
	virtual BOOL Uninit() = 0;
};

// 加載全部庫
BOOL DllImportInit();

// 卸載全部庫
BOOL DllImportUninit();

// 動態庫列表
extern list<DllImportAttribute*> gDllImportList;

#define DLLIMPORTCLASSBEGIN(CLASS, DLLPATH)	class CLASS;	\
extern CLASS g##CLASS##Dll;	\
class CLASS : public DllImportAttribute	\
{	\
protected:	\
	HMODULE  m_hModule;	\
public:	\
	CLASS() : m_hModule(NULL) {	\
		gDllImportList.push_back(this);	\
	}	\
	virtual BOOL Init() {	\
		m_hModule = LoadLibraryA(DLLPATH);	\
		return (m_hModule != NULL);	\
	}	\
	virtual BOOL Uninit() {	\
		return FreeLibrary(m_hModule);	\
	}	\

#define FUNCTIONENTRY(ENTRYTYPE, ENTRYPOINT)	protected:	\
	typedef ENTRYTYPE;	\
	ENTRYPOINT ENTRYPOINT##Ptr;	\
public:	\
	ENTRYPOINT ENTRYPOINT##Func() {	\
		ENTRYPOINT##Ptr = (ENTRYPOINT)GetProcAddress(m_hModule, "##ENTRYPOINT##");	\
		return ENTRYPOINT##Ptr;	\
	}	\

#define DLLIMPORTCLASSEND() };

#define DLLIMPORTCLASSIMPLEMENT(CLASS)	CLASS g##CLASS##Dll;

#define DLLIMPORTCALL(CLASS, ENTRYPOINT)	g##CLASS##Dll.ENTRYPOINT##Func()
#include "StdAfx.h"
#include "DllImportAttribute.h"

list<DllImportAttribute*> gDllImportList;

// 加載全部庫
BOOL DllImportInit()
{
	BOOL bSuccess = TRUE;
	for (auto iter = gDllImportList.begin(); iter != gDllImportList.end(); iter++)
	{
		bSuccess &= (*iter)->Init();
	}
	return bSuccess;
}

// 卸載全部庫
BOOL DllImportUninit()
{
	BOOL bSuccess = TRUE;
	for (auto iter = gDllImportList.begin(); iter != gDllImportList.end(); iter++)
	{
		bSuccess &= (*iter)->Uninit();
	}
	return bSuccess;
}

上面的兩個文件是實現DLL自動導出函數的基礎框架,直接引用到工程項目中。
使用上面的框架實現一個DLL函數導出的過程(以 MessageBoxA 爲例 )以下:

#pragma once

#include "DllImportAttribute.h"

DLLIMPORTCLASSBEGIN(User32, "User32.dll")
	FUNCTIONENTRY(int (WINAPI *MessageBoxA) (HWND, LPCSTR, LPCSTR, UINT), MessageBoxA)
DLLIMPORTCLASSEND()
#include "stdafx.h"
#include "User32.h"

DLLIMPORTCLASSIMPLEMENT(User32)

看起來是否是簡單多了。客戶端使用導出函數的例子:

DllImportInit();
DLLIMPORTCALL(User32, MessageBoxA)(NULL, "ad", "ad", MB_OK);
DllImportUninit();
縱觀框架的實現過程, DLLIMPORTCLASSBEGIN、FUNCTIONENTRY、DLLIMPORTCALL這3個宏意義重大:
DLLIMPORTCLASSBEGIN實現了動態連接庫的加載和卸載;
FUNCTIONENTRY實現了導出函數的定義和封裝函數;
DLLIMPORTCALL使導出函數的調用變得簡單。

雖說經過宏使得客戶端編碼相對來講變得簡單,可是仍然沒有達到C#的簡單程度。若是想要達到C#的簡單程度,至少須要作到如下幾點:
1.C++容許在頭文件裏面聲明變量,而不會引發變量重定義的錯誤。這個問題有人試圖經過引入import關鍵字解決;
2.C++可以解析一個函數定義的各個部分:Result、StackCall、Name、Params。VS版本的function模板類在具體實現的時候使用了宏展開,可以解析Result(Params)類型的參數,比Result,Params參數形式要好懂一些,可是還不夠。

最新的C++標準C++11增長了不少的語言特性,其中不定參數模板規則最可以引人興奮,我尚未具體嘗試能不能使自動處處框架變得更簡單,可是若是上述兩個問題不可以完全解決,C++可以達到C#的簡單程度仍是無望。
相關文章
相關標籤/搜索