Click here to Skip to main content
15,881,803 members
Articles / Desktop Programming / MFC

Interface-based Programming, Runtime Class Discovery, Dynamic Class Loading from DLL

Rate me:
Please Sign up or sign in to vote.
4.26/5 (9 votes)
8 Feb 2011CPOL6 min read 189.9K   914   59   23
Interface-based programming, Runtime class discovery, Dynamic class loading from DLL

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:

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

Can be implemented as:

C++
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:

C++
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:

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

Or more portable way is:

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

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”.

C++
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:

C++
__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.

decoration.png

Class creation function looks as follows:

C++
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;
}

Memory Management

When application calls into CreateClassInstance function, it will receive back pointer created with the C++ operator new.

C++
__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.

C++
__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.

C++
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:

C++
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.

adresses.png

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 strings 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:

C++
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:

C++
#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:

C++
#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;
};
C++
#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.

C++
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:

C++
#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;
};
C++
#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();
}
C++
#pragma once
#include "appinterfaces.h"

class CEngineEvent : public IEngineEvent
{
public:
	CEngineEvent(void);
	virtual ~CEngineEvent(void);
	virtual void OnStart();
};
C++
#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:

C++
#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

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Robotz Software
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralRules when building interfaced dll's Pin
wvdbos118-Feb-11 6:49
wvdbos118-Feb-11 6:49 
GeneralNo DEF file needed Pin
wvdbos118-Feb-11 5:58
wvdbos118-Feb-11 5:58 
You don't need a DEF file
Use
extern "C"
{
}

around the routine(s)
Because C never uses decoration the effect is the same.
Be aware that if you want to use the dll between different compilers the name might start with a '_' or not
GeneralRe: No DEF file needed Pin
steveb24-Feb-11 1:50
mvesteveb24-Feb-11 1:50 
Generalsmall correction Pin
alxxl7-Feb-11 23:23
alxxl7-Feb-11 23:23 
GeneralRe: small correction Pin
steveb8-Feb-11 1:50
mvesteveb8-Feb-11 1:50 
GeneralUm Pin
xComaWhitex31-Jan-11 14:04
xComaWhitex31-Jan-11 14:04 
GeneralRe: Um Pin
Indivara31-Jan-11 16:22
professionalIndivara31-Jan-11 16:22 
GeneralRe: Um Pin
xComaWhitex31-Jan-11 16:23
xComaWhitex31-Jan-11 16:23 
GeneralRe: Um Pin
Indivara31-Jan-11 16:34
professionalIndivara31-Jan-11 16:34 
GeneralRe: Um Pin
xComaWhitex31-Jan-11 16:36
xComaWhitex31-Jan-11 16:36 
GeneralRe: Um Pin
Rick York9-Feb-11 22:48
mveRick York9-Feb-11 22:48 
GeneralRe: Um Pin
xComaWhitex9-Feb-11 22:50
xComaWhitex9-Feb-11 22:50 
GeneralRe: Um Pin
Rick York1-Feb-11 7:42
mveRick York1-Feb-11 7:42 
GeneralRe: Um Pin
xComaWhitex1-Feb-11 7:44
xComaWhitex1-Feb-11 7:44 
GeneralRe: Um [modified] Pin
steveb10-Feb-11 16:32
mvesteveb10-Feb-11 16:32 
GeneralRe: Um Pin
Aoi Karasu 10-Feb-11 21:27
professional Aoi Karasu 10-Feb-11 21:27 
GeneralEliminate DeleteClassInstance Pin
bling31-Jan-11 5:26
bling31-Jan-11 5:26 
GeneralRe: Eliminate DeleteClassInstance [modified] Pin
steveb31-Jan-11 6:27
mvesteveb31-Jan-11 6:27 
GeneralRe: Eliminate DeleteClassInstance Pin
Aoi Karasu 10-Feb-11 21:11
professional Aoi Karasu 10-Feb-11 21:11 
GeneralRe: Eliminate DeleteClassInstance Pin
wvdbos118-Feb-11 6:28
wvdbos118-Feb-11 6:28 
GeneralMy vote of 5 Pin
brakmic31-Jan-11 3:50
brakmic31-Jan-11 3:50 
GeneralRe: My vote of 5 Pin
steveb31-Jan-11 4:53
mvesteveb31-Jan-11 4:53 
GeneralRe: My vote of 5 Pin
brakmic3-Feb-11 5:57
brakmic3-Feb-11 5:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.