Introduction
Interface-based programming is a well-known paradigm that has been around for a long time and it is a core technology behind frameworks such as COM or CORBA.
Interface-based programming (IBP) defines the application as a collection of independent modules which plug into each other via interface. Modules can be unplugged, replaced, or upgraded, without the need of compromising the contents of other modules. This reduces the complexity of the system and greatly increases maintainability at a later development cycles.
IBP is convenient. It is convenient when each module of a larger application must be developed by different teams. Even with the different versions of compilers and compilers in whole. You will be able to create flavors of your application like Basic, Standard, or Enterprise with the same core binary code base. When you publish your interface to a larger developer community, they can start creating additions to your software with ease, thus further enhancing its market value. It’s just one big bang for a buck any way you look at it.
What I will describe in this article is COM-like mechanism without the COM baggage. You may find this article interesting if you:
- Must develop and maintain an application in C++
- You do not need language interoperability with higher level languages like VB/C# etc.
- You want the interface-based modularity just like COM but do not want the peculiarities and baggage of COM to come along with it.
What you will gain vs. COM is:
- No need for messy registration of modules
- You are not limited to base
IUnknown
class
- You do not have to return
HRESULT
from every operation
- Encapsulation at binary level
In order to achieve these goals, your core application must be able to:
- Discover classes during runtime
- Dynamically load unknown classes that are not exported from DLL
- Enable discovered classes to pass events back to the application or among each other
- Delete the discovered classes and unload the DLL when no longer needed
Background
What I have discovered over time is that the COM is awkward for simple things.
Runtime Class Discovery
When your main application wants something done, it knows which interface can do the job. Interface is nothing more but a class of pure virtual functions with no implementation and no data members. Because the interface is a singular entity and the implementation of that interface can be a plural entity, invoking application must know one more piece of information about that interface. This 2nd piece of information is the implementation identity, or commonly known as GUID (globally unique identifier). It is possible to use the string names instead, but it’s not a good idea. You want an id that collides with other ids every say other 10,000 years. 128 bit GUID should do the trick.
In COM, the GUID interrogation is obtained via registry HKEY_CLASSES_ROOT
node. In our case, we can pass it via command line, INI file, configuration file, web site, registry node of your choosing, or just the use of __uuidof
operator if class name is known ahead.
Interface:
interface ICar
{
virtual ~ICar() = 0 {}
virtual const char* GetMake() = 0;
};
Can be implemented as:
class __declspec(uuid("DF7573B6-6E2F-4532-BD33-6375FC247F4E"))
CCar : public ICar
{
public:
virtual ~CCar(void);
virtual const char* GetMake();
};
uuid(id_name)
operator is roughly equivalent to:
class CCar: public ICar
{
static const char* get_uuid() { return "DF7573B6-6E2F-4532-BD33-6375FC247F4E"; }
virtual const char* GetMake ();
};
If you want to be portable, you can use this convention instead. One thing to keep in mind that __uuidof
operator returns "struct GUID
" and our static
function uuid
returns "const char*
". It then can be invoked with:
if( riid == __uuidof(CCar))
{
}
Or more portable way is:
if( strcmp(id_string, CCar::get_uuid()) == 0)
{
}
When particular interface implementation is housed inside DLL, without us knowing which DLL it is, we need to interrogate all DLLs in the application path to invoke the implementation we want. The following code snippet will create a search path in form “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";
}
...............
};
For us to successfully interrogate module, the DLL must implement three functions in the form:
__declspec(dllexport) void * CreateClassInstance(REFIID riid);
__declspec(dllexport) void DeleteClassInstance(void* ptr);
__declspec(dllexport) bool CanUnload();
Also make an entry into the .DEF file in the exports section. This will remove any function name decoration added by compiler.
EXPORTS
CreateClassInstance @1
DeleteClassInstance @2
CanUnload @3
Examine your DLL module with DEPENDS.EXE. Your exported functions must be decoration free.
Class creation function looks as follows:
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)
{
::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)
{
m_Dlls.insert(hDll);
return ptr;
}
else
{
::FreeLibrary(hDll);
}
}
else
{
::FreeLibrary(hDll);
}
}
}
BOOL bFound = ::FindNextFile(hFind, &ffd);
if(!bFound)
break;
}
return NULL;
}
Memory Management
When application calls into ”CreateClassInstance
” function, it will receive back pointer created with the C++ operator new
.
__declspec(dllexport) void* CreateClassInstance(REFIID riid)
{
if(riid == __uuidof(CCar))
return new CCar;
return NULL;
}
It is natural to think that you can delete pointer returned from the DLL within invoking application. However most of the time, this is not going to work. I am saying “most” because it works only in certain cases. This will work if both application and the DLL that houses class are linked to C++ runtime library dynamically. If any of them link statically, then you will get the “No man‘s land pointer” assertion. This is because in the first case, both application and the DLL share heap manager and in the second case, they do not. Therefore the pointer must be deleted inside the DLL it was allocated. We need to implement deletion routine and this brings us to “DeleteClassInstance
” function.
__declspec(dllexport) void DeleteClassInstance(void* ptr)
{
.........................
if(clsid == __uuidof(CCar))
{
CCar* p = static_cast<CCar*>(ptr);
delete p;
return;
}
}
Note the necessity of cast. This is because it is impossible to delete void
pointer, since its destructor is unknown. That said, we need a way to associate the void*
pointer passed to DeleteClassInstance
routine with the class id in order to perform successful cast back to original class. I implemented class CDllLoadedClasses
which keeps track of allocated classes.
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();
}
};
When all classes that belong to DLL were de allocated, there is no need to keep the DLL around. Function “CanUnload
” queries if there are no more allocated classes left inside DLL. If this is true
, then DLL can be unloaded to free up application address space.
Final version of exported classes is as follows:
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();
}
Caveats
You may wonder why we can’t unload the DLL right after we call “CreateClassInstance
” call. The new
operator allocates pointer from the application global heap address space anyway, so why to keep DLL loaded? The problem is that the virtual table pointer is allocated inside the DLL's data segment, so as soon as that DLL is unloaded the vtable
is deleted and in DEBUG builds filled with 0xFE
character. If you see 0xFEFEFEFE
, this mean that memory was freed. And the second reason is the implementation of “DeleteClassInstance
” function where we need the DLL loaded so we can deallocate pointer at a later time.
Which Data Types Belong in an Interface
Generally, you would want to expose in your interface POD (plain old data) only and other interfaces. POD are types implemented on a compiler level (int
, float
, double
, etc.). Any complex type must be wrapped in an interface. This means that you must try to avoid exposing even std::string
, std::vector
, CString
and so on because they are all implementation specific from one vendor to another. Generally, you want to pass string
s not by std::string
but rather by const char*
which is a POD. If you have to pass a collection – wrap it into an interface first.
Wrapping Collections
Collections are easy to wrap. Almost any indexed collection can be wrapped as follows:
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;
};
This will not only make your interface binary compatible but it will also increase maintainability at later stages of your application. You can for instance swap your std::map
class to std::tr1::unordered_map
that has performance characteristics by far surpassing std::map
implementation.
Implementation of an IWheel
interface:
#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; }
};
Implementation of 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];
}
Events
COM does have a mechanism called event sink. It is very easy to implement. Event object is an interface with a method. It has a “has-a” relationship to associated object.
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;
};
Implementation declaration:
#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;
}
Use in Application
Final program:
#include "stdafx.h"
#include <iostream>
#include "ClassFactory.h"
#include "AppInterfaces.h"
#include "EngineEvent.h"
#include "BMW.h"
IBP::CClassFactory theFactory;
int _tmain(int argc, _TCHAR* argv[])
{
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);
pCar = theFactory.Create<ICar>(__uuidof(CBMW328i));
theFactory.Delete(pCar);
return 0;
}
Using the Code
Include ClassFactory.h file in your project. Implement three exported functions as defined above. And enjoy Interface-based programming.
History
- Jan 31 2011: Initial article
- Feb 01 2011: Fixed some mispelled words