基于接口的编程、运行时类发现、动态从 DLL 加载类
基于接口的编程、运行时类发现、动态从 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 模块。导出的函数必须没有修饰。

类创建函数如下所示:
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 以便以后可以释放指针。

哪些数据类型属于接口
通常,您希望在接口中仅公开 POD(普通旧数据)和其他接口。POD 是在编译器级别实现的类型(int
、float
、double
等)。任何复杂类型都必须包装在接口中。这意味着您必须尽量避免公开 std::string
、std::vector
、CString
等,因为它们都是特定于供应商的实现。一般来说,您希望通过 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 日:修正了一些拼写错误