[开源世界]从自动导出动态链接库接口看C++的缺点

流过昼夜 提交于 2019-11-27 19:12:44

自动导出动态链接库接口在C++编程中绝对是一件烦人的事情,因为你不得不大量的重复以下几个步骤:
1.加载动态链接库
2.定义导出函数指针定义
3.定义导出函数指针变量
4.从动态链接库中导出函数
5.编写导出函数的封装函数
6.卸载动态链接库

有很多人试图将这个过程自动化,然而全部折戟沉沙。我认为终极原因在于动态链接库在导出函数的时候没有包含参数信息,而在使用的时候不得不将导出函数进行强制转型。

在C#中导出动态链接库的函数相比C++来说要简单的多:
1. 使用DllImportAttribute属性规定导出函数的文件路径、导出点、堆栈调用方式等
2.定义导出函数的C#函数原型

说简单是代码简单,不是实现过程简单。从表面上看,C#的这两部实现了C++的那6步,从原理上来看,C#仍然需要C++的那6步。C#的编译器把2、3、4、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#的简单程度还是无望。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!