65.9K
CodeProject 正在变化。 阅读更多。
Home

基于接口的编程、运行时类发现、动态从 DLL 加载类

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.26/5 (9投票s)

2011年1月31日

CPOL

6分钟阅读

viewsIcon

193389

downloadIcon

946

基于接口的编程、运行时类发现、动态从 DLL 加载类

引言

基于接口的编程是一种广为人知的范例,它已经存在很长时间,并且是 COM 或 CORBA 等框架背后的核心技术。

基于接口的编程 (IBP) 将应用程序定义为一系列通过接口相互连接的独立模块。模块可以被拔出、替换或升级,而无需影响其他模块的内容。这降低了系统的复杂性,并极大地提高了后期开发周期的可维护性。

IBP 很方便。当一个大型应用程序的每个模块必须由不同的团队开发时,它就显得很方便。即使使用不同版本或不同类型的编译器。您将能够使用相同的核心二进制代码库创建应用程序的各种版本,如 Basic、Standard 或 Enterprise。当您将接口发布给更广泛的开发者社区时,他们可以轻松地为您的软件创建扩展,从而进一步增强其市场价值。无论从哪个角度看,它都是物超所值。

在本文中,我将描述一种不包含 COM 负担的类似 COM 的机制。如果您符合以下条件,您可能会发现本文很有趣:

  • 必须用 C++ 开发和维护一个应用程序
  • 您不需要与 VB/C# 等更高级语言进行语言互操作
  • 您想要 COM 那样的基于接口的模块化,但又不希望 COM 的特性和负担随之而来。

与 COM 相比,您将获得的好处是:

  • 无需复杂的模块注册
  • 您不受限于基类 IUnknown
  • 不必从每个操作返回 HRESULT
  • 二进制级别的封装

为了实现这些目标,您的核心应用程序必须能够:

  • 在运行时发现类
  • 动态加载未从 DLL 导出的未知类
  • 使发现的类能够将事件传递回应用程序或在它们之间传递
  • 在不再需要时删除发现的类并卸载 DLL

背景

我随着时间的推移发现,COM 对于简单的事情来说很笨拙。

运行时类发现

当您的主应用程序需要完成某项工作时,它会知道哪个接口可以完成这项工作。接口只不过是一个纯虚函数的类,没有实现也没有数据成员。因为接口是单一实体,而接口的实现可以是多个实体,所以调用应用程序必须知道关于该接口的另一个信息。这个第二个信息是实现标识,或者通常称为 GUID(全局唯一标识符)。可以使用字符串名称,但这并非一个好主意。您需要一个 ID,例如每 10,000 年才会与其他 ID 发生一次冲突。128 位 GUID 应该可以做到这一点。

在 COM 中,GUID 的查询是通过注册表 HKEY_CLASSES_ROOT 节点获得的。在我们的例子中,我们可以通过命令行、INI 文件、配置文件、网站、您选择的注册表节点,或者仅仅使用 __uuidof 运算符(如果类名是预先已知的)来传递它。

接口

interface ICar
{
	virtual ~ICar() = 0 {}
	virtual const char* GetMake() = 0;
};

可以实现为:

class __declspec(uuid("DF7573B6-6E2F-4532-BD33-6375FC247F4E"))
CCar : public ICar
{
public:
	virtual ~CCar(void);
	virtual const char* GetMake();
};

uuid(id_name) 运算符大致等同于:

class CCar: public ICar
{
    static const char* get_uuid() { return "DF7573B6-6E2F-4532-BD33-6375FC247F4E"; }
    virtual const char* GetMake ();
};

如果您希望具有可移植性,可以使用此约定。需要注意的是,__uuidof 运算符返回“struct GUID”,而我们的 static 函数 uuid 返回“const char*”。然后可以使用以下方式调用:

if( riid == __uuidof(CCar))
{
    // invoke class here
}

或者更具可移植性的方式是:

if( strcmp(id_string, CCar::get_uuid()) == 0)
{
    // invoke class here
}

当某个接口实现驻留在 DLL 中,而我们又不知道是哪个 DLL 时,我们就需要查询应用程序路径中的所有 DLL 来调用我们想要的实现。以下代码段将创建一个搜索路径,形式为“C:\Bin\*.dll”。

class CClassFactory
{
  std::string m_sSearchPath;

  public:
	CClassFactory()
	{
	  char path[MAX_PATH] = {0};
	  char drive[_MAX_DRIVE] = {0};
	  char dir[_MAX_DIR] = {0};
	  char fname[_MAX_FNAME] = {0};
	  char ext[_MAX_EXT] = {0};

	  HMODULE hMod = ::GetModuleHandle(NULL);
	  ::GetModuleFileName(hMod, path, MAX_PATH);

	   _splitpath(path, drive, dir, fname, ext);
	   m_sSearchPath += drive;
	   m_sSearchPath += dir;
	   m_sSearchPath += "*.dll";
        }
     ...............
};

为了成功查询模块,DLL 必须实现三种形式的函数:

__declspec(dllexport) void * CreateClassInstance(REFIID riid);
__declspec(dllexport) void DeleteClassInstance(void* ptr);
__declspec(dllexport) bool CanUnload();

还在 .DEF 文件中的导出部分添加一个条目。这将删除编译器添加的任何函数名称修饰。

EXPORTS
CreateClassInstance	@1
DeleteClassInstance	@2
CanUnload		@3

使用 DEPENDS.EXE 检查您的 DLL 模块。导出的函数必须没有修饰。

decoration.png

类创建函数如下所示:

template< typename T >
T* Create(REFIID iid)
{
   FUNC_CREATE CreateFunc;
   WIN32_FIND_DATA ffd={0};
   HANDLE hFind;

   hFind = ::FindFirstFile(m_sSearchPath.c_str(), &ffd);
   while(hFind != INVALID_HANDLE_VALUE)
   {
      if(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      {
	// skip the directories
	::FindNextFile(hFind, &ffd);
	continue;
      }
      else
      {
	  HMODULE hDll = NULL;
	  hDll = ::LoadLibrary(ffd.cFileName);
	  if(hDll)
	  {
	     CreateFunc = (FUNC_CREATE)(::GetProcAddresshDll, "CreateClassInstance"));
	     if(CreateFunc)
	     {
		T* ptr = static_cast<T*>(CreateFunc(iid));
		if(ptr)
		{
		   // Save dll hmodule
		   m_Dlls.insert(hDll);
		   return ptr;
		}
		else
		{
		   // Unload dll
		   ::FreeLibrary(hDll);
		}
	    }
	    else
	    {
	        // Unload dll
		::FreeLibrary(hDll);
	    }
	}
    }
    BOOL bFound = ::FindNextFile(hFind, &ffd);
    if(!bFound)
	break;
  }
  return NULL;
}

内存管理

当应用程序调用 CreateClassInstance 函数时,它将收到由 C++ 运算符 new 创建的指针。

__declspec(dllexport) void* CreateClassInstance(REFIID riid)
{
	if(riid == __uuidof(CCar))
		return new CCar;

	return NULL;
}

很自然地会认为,您可以删除从 DLL 返回的指针。然而,大多数情况下,这不会奏效。我说“大多数”是因为它只在某些情况下有效。当应用程序和包含类的 DLL 都动态链接到 C++ 运行时库时,这会奏效。如果其中任何一个静态链接,您将收到“无法访问的指针”断言。这是因为在第一种情况下,应用程序和 DLL 共享堆管理器,而在第二种情况下,它们不共享。因此,指针必须在分配它的 DLL 内部删除。我们需要实现删除例程,这就引出了“DeleteClassInstance”函数。

__declspec(dllexport) void DeleteClassInstance(void* ptr)
{
    .........................

	if(clsid == __uuidof(CCar))
	{
		CCar* p = static_cast<CCar*>(ptr);
		delete p;
		return;
	}
}

注意强制转换的必要性。这是因为不可能删除 void 指针,因为它析构函数未知。也就是说,我们需要一种方法将传递给 DeleteClassInstance 例程的 void* 指针与类 ID 关联起来,以便成功地强制转换回原始类。我实现了 CDllLoadedClasses 类,该类跟踪已分配的类。

class CDllLoadedClasses
{
	typedef std::map<void*, CLSID> MapPtrToId;
	MapPtrToId m_mapObjects;

public:
	void Add(void* ptr, REFIID iid)
	{
		m_mapObjects[ptr] = iid;
	}

	bool FindAndRemove(void* ptr, IID& refIID)
	{
		MapPtrToId::iterator it = m_mapObjects.find(ptr);
		if(it != m_mapObjects.end())
		{
			refIID = it->second;
			m_mapObjects.erase(it);
			return true;
		}
		return false;
	}

	bool IsEmpty() const
	{
		return m_mapObjects.empty();
	}

};

当属于 DLL 的所有类都被释放后,就没有必要保留该 DLL 了。函数“CanUnload”查询 DLL 中是否还有已分配的类。如果为 true,则可以卸载 DLL 以释放应用程序地址空间。

导出的类的最终版本如下所示:

IBP::CDllLoadedClasses theClassTracker;

__declspec(dllexport) void* CreateClassInstance(REFIID riid)
{
	if(riid == __uuidof(CCar))
	{
		void* ptr = static_cast<void* >(new CCar);
		theClassTracker.Add(ptr, riid);
		return ptr;
	}

	return NULL;
}

__declspec(dllexport) void DeleteClassInstance(void* ptr)
{
	CLSID clsid = {0};
	if(!theClassTracker.FindAndRemove(ptr, clsid))
		return;

	if(clsid == __uuidof(CCar))
	{
		CCar* p = static_cast<CCar*>(ptr);
		delete p;
		return;
	}
}

__declspec(dllexport) bool CanUnload()
{
	return theClassTracker.IsEmpty();
}

注意事项

您可能想知道为什么在调用“CreateClassInstance”后不能立即卸载 DLL。new 运算符反正会从应用程序全局堆地址空间分配指针,所以为什么还要保留 DLL 加载?问题是,虚函数表指针在 DLL 的数据段内分配,因此一旦该 DLL 被卸载,vtable 就会被删除,并且在 DEBUG 版本中会被填充为 0xFE 字符。如果您看到 0xFEFEFEFE,则表示内存已被释放。第二个原因是“DeleteClassInstance”函数的实现,我们需要加载 DLL 以便以后可以释放指针。

adresses.png

哪些数据类型属于接口

通常,您希望在接口中仅公开 POD(普通旧数据)和其他接口。POD 是在编译器级别实现的类型(intfloatdouble 等)。任何复杂类型都必须包装在接口中。这意味着您必须尽量避免公开 std::stringstd::vectorCString 等,因为它们都是特定于供应商的实现。一般来说,您希望通过 const char* (这是一个 POD)而不是通过 std::string 来传递字符串。如果您必须传递集合,请先将其包装在接口中。

包装集合

集合很容易包装。几乎任何索引集合都可以如下包装:

template<typename T>
interface ICollection
{
   virtual ~ICollection() = 0 {};
   virtual void Clear() = 0;
   virtual unsigned long Count() = 0;
   virtual int Add(T pVal) = 0;
   virtual void Remove(unsigned long index) = 0;
   virtual bool Next(T* ppVal) = 0;
   virtual T operator[](unsigned long index) = 0;
};

interface IWheel
{
	virtual ~IWheel() = 0 {}
	virtual const char* GetBrand() const = 0;
	virtual int GetPSIPressure()const = 0;
};

//
interface IWheelCollection : public ICollection <IWheel* >
{
};

interface ICar
{
	virtual ~ICar() = 0 {}
	virtual const char* GetMake() = 0;
	virtual const char* GetPrice() = 0;
	virtual IWheelCollection* GetWheelCollection() = 0;
};

这不仅将使您的接口具有二进制兼容性,而且还能提高应用程序后期阶段的可维护性。例如,您可以将 std::map 类替换为 std::tr1::unordered_map ,它的性能特性远远优于 std::map 实现。

IWheel 接口的实现

#pragma once
#include "AppInterfaces.h"

class CWheel : public IWheel
{
public:
	CWheel(void);
	virtual ~CWheel(void);

	virtual const char* GetBrand() const { return "Michelin"; }
	virtual int GetPSIPressure()const { return  40; }
};

IWheelCollection 的实现

#pragma once
#include <vector>
#include "AppInterfaces.h"
#include "Wheel.h"

class CWheelCollection : public IWheelCollection
{
public:
	CWheelCollection(void);
	virtual ~CWheelCollection(void);

	virtual void Clear();
	virtual unsigned long Count();
	virtual int Add(IWheel* pVal);
	virtual void Remove(unsigned long index);
	virtual bool Next(IWheel** ppVal);
	virtual IWheel* operator[](unsigned long index);

private:
	std::vector<IWheel*> m_coll;
};
#include "StdAfx.h"
#include "WheelCollection.h"

CWheelCollection::CWheelCollection(void)
{
}

CWheelCollection::~CWheelCollection(void)
{
	Clear();
}

void CWheelCollection::Clear()
{
	for(size_t i = 0; i < m_coll.size(); i++)
		delete m_coll[i];

	m_coll.clear();
}

unsigned long CWheelCollection::Count()
{
	return m_coll.size();
}

int CWheelCollection::Add(IWheel* pVal)
{
	m_coll.push_back(pVal);
	return m_coll.size() - 1;
}

void CWheelCollection::Remove(unsigned long index)
{
	std::vector<IWheel*>::iterator it = m_coll.begin() + index;
	delete *it;
	m_coll.erase(it);
}

bool CWheelCollection::Next(IWheel** ppVal)
{
	static int nIndex = 0;

	if(nIndex >= m_coll.size())
	{
		nIndex = 0;
		return false;
	}

	*ppVal = m_coll[nIndex++];
	return true;
}

IWheel* CWheelCollection::operator[](unsigned long index)
{
	return m_coll[index];
}

事件

COM 有一个称为事件接收器 (event sink) 的机制。它很容易实现。事件对象是一个带有方法的接口。它与关联对象具有“has-a”关系。

interface IEngineEvent
{
	virtual ~IEngineEvent() = 0 {}
	virtual void OnStart() = 0;
};

interface IEngine
{
	virtual ~IEngine() = 0 {}
	virtual const char* GetHP() const = 0;
	virtual const char* GetFuelEconomy() = 0;
	virtual const char* GetSpec() = 0;
	virtual void SetEventHandler(IEngineEvent* ptr) = 0;
	virtual void StartEngine() = 0;
};

实现声明

#pragma once
#include "AppInterfaces.h"

class CEngine : public IEngine
{
public:
	CEngine(void);
	virtual ~CEngine(void);

	virtual const char* GetHP() const { return "230 hp"; }
	virtual const char* GetFuelEconomy() { return "28 mpg hwy"; }
	virtual const char* GetSpec() { return "3 liter, 6 cylinder"; }
	virtual void SetEventHandler(IEngineEvent* ptr);
	virtual void StartEngine();

private:
	IEngineEvent* m_pEngineEvent;
};
#include "StdAfx.h"
#include "Engine.h"

CEngine::CEngine(void):
  m_pEngineEvent(nullptr)
{
}

CEngine::~CEngine(void)
{
}

void CEngine::SetEventHandler(IEngineEvent* ptr)
{
	m_pEngineEvent = ptr;
}

void CEngine::StartEngine()
{
	if(m_pEngineEvent)
		m_pEngineEvent->OnStart();
}
#pragma once
#include "appinterfaces.h"

class CEngineEvent : public IEngineEvent
{
public:
	CEngineEvent(void);
	virtual ~CEngineEvent(void);
	virtual void OnStart();
};
#include "StdAfx.h"
#include "EngineEvent.h"
#include <iostream >


CEngineEvent::CEngineEvent(void)
{
}

CEngineEvent::~CEngineEvent(void)
{
}

void CEngineEvent::OnStart()
{
	std::cout << "Wharooooom!!!!" << std::endl;
}

在应用程序中使用

最终程序

#include "stdafx.h"
#include <iostream>
#include "ClassFactory.h"
#include "AppInterfaces.h"
#include "EngineEvent.h"

// if you use __uuidof operator
#include "BMW.h"

IBP::CClassFactory theFactory;

int _tmain(int argc, _TCHAR* argv[])
{
	// Invoke with string
	ICar* pCar = theFactory.Create <ICar>
		(L"{DF7573B6-6E2F-4532-BD33-6375FC247F4E}");

	std::cout << "Make:\t" << pCar->GetMake() << std::endl;
	std::cout << "Price:\t" << pCar->GetPrice() << std::endl;

	IEngine* pEngine = pCar->GetEngine();

	std::cout << "Fuel Economy:\t" << pEngine->GetFuelEconomy() << std::endl;
	std::cout << "Power:\t" << pEngine->GetHP() <<std::endl;
	std::cout << "Specs:\t" << pEngine->GetSpec() << std::endl << std::endl;

	IWheelCollection* pWheelColl = pCar->GetWheelCollection();

	std::cout << "Has " << pWheelColl->Count() << " wheels" << std::endl;

	IWheel* pWheel = nullptr;
	int i = 1;
	while(pWheelColl->Next(&pWheel))
	{
		std::cout << "Wheel No\t:" << i++ << std::endl;
		std::cout << "Wheel Brand:\t" << pWheel->GetBrand() << std::endl;
		std::cout << "Wheel Pressure:\t" << pWheel->GetPSIPressure() << 
				" PSI" << std::endl << std::endl;
	}

	CEngineEvent eventEngine;

	pEngine->SetEventHandler(&eventEngine);

	std::cout << "Staring engine!" << std::endl << std::endl;

	pEngine->StartEngine();

	theFactory.Delete(pCar);

	// Invoke with __uuidof
	pCar = theFactory.Create<ICar>(__uuidof(CBMW328i));
	theFactory.Delete(pCar);
	return 0;
}

Using the Code

ClassFactory.h 文件包含到您的项目中。按照上述定义实现三个导出函数。然后享受基于接口的编程。

历史

  • 2011 年 1 月 31 日:初稿
  • 2011 年 2 月 1 日:修正了一些拼写错误
© . All rights reserved.