Rajinder Yadav - Windows/Linux C++ Development Tools & Resources :Design, Code, Test, Deploy

Componentware, An Introduction to Component-Based Development

Author: Rajinder Yadav
Date: Oct 24, 2000

Introduction
This article introduces the basic concepts of component-based development. It is a great starting point for those of you wanting to get into using Microsoft's Component Object Model (COM). Surprisingly enough COM is not limited to Microsoft or Windows, nor is it by any means a Microsoft Specific technology. In this article, you will soon see just how easy it is to implement a component using C++.

This article will guide you through the steps of turning a simple C++ class into a standalone component. It is assumed that the reader has a good understanding of C++ concepts.

Benefits of Componentware

Interface and Component
Simply stated a component is an object that provides its services through a set of interfaces, which themselves provide access to a collection of methods. An interface can be viewed as a contract, meaning that once a service is defined the conditions surrounding its use will never change.

NOTE: this definition of a component is not to be confused with the COM specification, which is a standard for component development.

Below is a diagram of a small component labeled "Cow". The diagram shows that the Cow component has two interfaces, IBase and IMouth. The IBase interface provides three services: QueryInterface, AddRef and Release, while the IMouth interface provides Talk and Eat.
  +----+
  |Cow |----O IBase        +-------------+  +----------------+
  |    |----O IMouth       | <interface> |  |   <interface>  |
  +----+                   |   IMouth    |  |     IBase      |
                           +=============+  +================+
                           |Talk()       |  |QueryInterface()|
                           |Eat()        |  |AddRef()        |
                           +-------------+  |Release()       |
                                            +----------------+

NOTE: By convention all interface names are prefixed with a capital "I"

In order to use a component, the application developer will require the following information:

We will see later how this gets implemented within a component.

Component is a Binary Image
The application developer using a component will not have access to its source code. This should not matter, as the developer does not need to concern himself as to how each interface is implemented. To a developer the component is presented as a binary image that gets loaded during runtime.

Component as a Black Box
To the application developer a component is seen as a black box. The interface is the only way for a developer to communicate with the component. The developer cannot alter the state of the component directly or gain access to its data. This has the benefit of data hiding, an interface in this sense forces data and state integrity. The advantage of componentware is gained when you consider the idea of binary compatibility. With this flexibility, components become plug-able objects that can be used as interchangeable building blocks.

Binary Compatibility
Components with the same interface signature can be interchanged with one another. Now, the full power of componentware is realized when components become dynamic loadable objects. In this framework, applications no longer need to be recompiled. Given this ability it's easy to swap a slow spell checker component with a faster one later on.

An entire application can be componentized and put together with "throwaway code" that glues each component together. This approach makes upgrading, extending, enhancing and customizing software a lot easier! Just think of the possibilities :-)

Improved Resource Management
Memory footprint and resource allocation can be reduced substantially with componentware. In this model, the entire application no longer needs to get loaded into memory, only what is required gets loaded. After all, why would you want to load the entire word processor into memory? You only need the printer component during a print request, likewise you don't need the html converter unless you want to publish to the web, and forget email support if you're not online.

Component Versioning
To ensure that successive releases of a component continue to run with previous applications, the component developer must make certain to stick to the following golden rule:

An Interface once released cannot change, that is the signature (methods) that define the interface must remain the same. However, the code behind the interface is free to change.

We will take a look at interface requirements in more details later, but for now let's look at some examples.

// Example 1: Component Mapping
//
#include <stdio.h>

// interface definition
class IAction 
{
   
public:
   
virtual void act()  0;
   virtual void 
sing() 0;
};

// component Actor
class Actor : public IAction 
{
   
public:
   
void act()  { printf("Look mom, I'm acting\n"}
   
void sing() { printf("Sorry I can't sing!\n")}
}
;

// component BadActor
class BadActor : public IAction 
{
   
public:
   
void act()  { printf("Act???\n")}
   
void sing() { printf("Sing???\n")}
}
;

void 
main() 
{
   
// interface
   
IAction* pIAction = 0;

   
// create components on the stack for now!
   
Actor    good;
   
BadActor bad;

   
// components with the same interface signature
   // can be swapped! Like a good actor for a bad one!!!

   // Take one...
   
pIAction &bad;   // get interface
   
pIAction->act();
   
pIAction->sing();

   
// Next!
   
pIAction &good;  // get interface
   
pIAction->act();
   
pIAction->sing();
}
Colorized by: CarlosAg.CodeColorizer

NOTE: how both components have the same signature for interface IAction

Interfaces are defined as pure abstract classes. What this mean is the interface is independent from the class that supports it. Thus many components can declare the same interface. Later we'll see how all components are required to support the base interface IBase.

The following example illustrates how a component can support multiple interfaces.

//Example 2: Interface Mapping
//
#include <stdio.h>

#define Interface struct

Interface IAction 
{
   
virtual void act()  0;
   virtual void 
sing() 0;
};

Interface IStunt 
{
   
virtual void shop(const char* pItem)    0;
   virtual void 
phone(const char* pNumber) 0;
};

class 
SuperActor : public IAction, public IStunt
{
   
public:
   SuperActor() {
      printf(
"SuperActor component created\n");
   
}

   
virtual ~SuperActor() {
      printf(
"SuperActor component destroyed\n");
   
}

   
void act()  { 
      printf(
"Look mom, I'm acting\n"
   
}

   
void sing() { 
      printf(
"Sorry I can't sing!\n")
   
}

   
void shop(const char* pItem) { 
      printf(
"I need some new %s\n", pItem);
   
}

   
void phone(const char* pNumber) { 
      printf(
"Dialing %s\n", pNumber);
   
}
}
;


void 
main() 
{
   IAction*    pIAction = 0
;
   
IStunt*     pIStunt = 0;

   
// create the component
   
SuperActor* pSuper = new SuperActor;

   
// get the IAction interface
   
pIAction pSuper;
   
pIAction->act();
   
pIAction->sing();

   
// get the IStunt interface
   
pIStunt pSuper;
   
pIStunt->shop("shoes");
   
pIStunt->phone("911");

   
// release the component
   
delete pSuper;
}
Colorized by: CarlosAg.CodeColorizer

In this example component SuperActor has two interfaces, IAction and IStunt. The code shows how each interface is used to gain access to the services. The following diagram shows how this is done through v-table lookup in C++.

V-Table Layout
+----------+
|SuperActor|
+==========+
|act()     |<----- pIAction (pIAction = pSuper)
|sing()    |                 
+----------+                 
|shop()    |<----- pIStunt  (pIStunt = pSuper)
|phone()   |
+----------+

NOTE: how we #define "Interface" to be a struct in the last example

#define Interface struct

Having an interface defined as a struct rather than a class makes all the methods public by default. This allows us to define interface IAction with two methods like this:

Interface IAction 
{
   
virtual void act()  0;
   virtual void 
sing() 0;
};
Component X then provides support for the IAction interface with a declaration like this:
class X : public IAction 
{
public:
   
void act()  {...}
   
void sing() {...}
}
;

NOTE: The class must inherit's the interface publicly, so it is visible to the outside.

Interface Rule Revisited
In order for any idea to work there must be standards, and with components there are no exceptions. Most of the rules you come across will have to do with interfaces and lifetime management.

*** Interfaces Don't Change ***

Versioning
Once an interface is released, it cannot be changed. Instead when a component requires an interface to change you must declare a new interface and add it to the component. This way existing code will continue to work with the new component and new code can take advantage of the latest interfaces.

Lifetime Management
To manage a component's lifetime, the component designers took advantage of a technique called reference counting. The idea behind this is to keep track of the usage count on a component. When a request is made for an interface, the reference count is increased on the component. When the interface is no longer needed, the interface is released and the reference count is reduced on the component. When the reference count becomes zero the component is no longer needed and is destroyed to free up memory and resources.

In order to support reference counting, we need to prevent the developer from being able to create and destroy the component directly. Since components are dynamic they should never be created on the stack, but component programming doesn't rule out this technique.

We don't want the developer to be able to use the new and delete operators to create and destroy a component. Allowing this would render reference counting useless, since one piece of code may be referencing an interface on a component that another piece of code just blew away with the delete operator!

Steps to componentizing a C++ class

1. We must declare the component's constructor and destructor to be private, this prevents the developer from creating the component on the heap with "new" and prematurely destroying it with "delete".

class Cow 
{
private:
   Cow() { }
   
virtual ~Cow() { }
}
;

2. Next we need to add an interface to the component. We'll name the interface IMouth and give it two methods, Talk and Eat. For the Cow component to support the interface, the class definition must be updated to inherit it.

Interface IMouth 
{
   
virtual long Talk() 0;
   virtual long 
Eat()  0;
};

class 
Cow : public IMouth 
{
public:

   
long Talk() {
      printf(
"Mooove over\n");
      return 
EC_OK;
   
}

   
long Eat() {
      printf(
"Chew, chew, chew\n");
      return 
EC_OK;
   
}

private:
   Cow() { }
   
virtual ~Cow() { }
}

3. Next we need to add a reference count member to the component class.

class Cow : public IMouth 
{
...
private:
   Cow() : m_lRef (
0) { }
   
virtual ~Cow() { }

   
long m_lRef;
}

4. Now we must provide a way to increase and decrease the reference count, so we can manage the components lifetime. To do this we need to declare a new interface called IBase.

Interface IBase 
{
   
virtual long AddRef()  0;
   virtual long 
Release() 0;
};

Also our interface now must support the base services provided by IBase interface. To allow this, we must declare IBase to be a super class of our interface.

Interface IMouth : public IBase 
{
   
virtual long Talk() 0;
   virtual long 
Eat()  0;
};

The IBase interface provides two methods AddRef() and Release(), which our component must now support to manage it's lifetime.

class Cow : public IMouth
{
public:
   
long AddRef() { 
      
return ++m_lRef
   
}

   
long Release() {
      --m_lRef
;
      if
(m_lRef == 0) {
         
delete this;
         return 
0;
      
}
      
return m_lRef;
   
}

friend 
class Factory;

   long 
Talk() {
      printf(
"Mooove over\n");
      return 
EC_OK;
   
}

   
long Eat() {
      printf(
"Chew, chew, chew\n");
      return 
EC_OK;
   
}

private:
   Cow() : m_lRef (
0) { 
      printf(
"Cow component created\n")
   
}

   
virtual ~Cow() { 
      printf(
"Cow component distroyed\n")
   
}

   
long m_lRef;
};

5. Now we need to declare a class factory in order to be able to create a component. If you noticed earlier, we declared class Factor to be a friend of component Cow. This allows the class factory to create an instance of a Cow component using the new operator.

class Factory 
{
public:
   
static long Create(const long clsid, void** ppVoid) {
      
long lRet EC_NOINTERFACE;
      
*ppVoid   = NULL;

      switch
(clsid) {
         
case CLSID_Cow:
            *ppVoid 
= static_cast<void*>(new Cow);
            break;
      
// switch

      // update the reference count
      
if(*ppVoid) {
         
static_cast<IBase*>(*ppVoid)->AddRef();
         
lRet EC_OK;
      
}

      
return lRet;
   
}
}
;

The factory class had only one static method called Create that accepts two parameters. The first argument is the class id, it tells the factory what component we want created. The second argument is a void pointer to pointer that gets assigned a return interface of the created component.

6. Let's take a look at the code that creates an instance of the Cow components and uses referencing to manage its lifetime.

void main()
{
   IMouth* pIMouth 
= NULL;
   long    
lRet    EC_FAIL;

   
// create component
   
lRet Factory::Create(CLSID_Cow, (void**)&pIMouth);

   if
(lRet == EC_OK) {
      pIMouth->
Talk();
      
pIMouth->Eat();

      
// release interface
      
pIMouth->Release();
      
pIMouth = NULL;
   
}
}

Putting it all together now!

#include <STDIO.H>

// component id
const long CLSID_Cow       0x01;

// error codes
const long EC_OK           0x00;
const long 
EC_FAIL         0x01;
const long 
EC_NOINTERFACE  0x03;

#define 
Interface struct

// base interface
Interface IBase 
{
   
virtual long AddRef()  0;
   virtual long 
Release() 0;
};

Interface IMouth : public IBase 
{
   
virtual long Talk() 0;
   virtual long 
Eat()  0;
};

// component cow
class Cow : public IMouth 
{
public:
   
long AddRef() { 
      
return ++m_lRef
   
}

   
long Release() {
      --m_lRef
;
      if
(m_lRef == 0) {
         
delete this;
         return 
0;
      
}
      
return m_lRef;
   
}

   
long Talk() {
      printf(
"Mooove over\n");
      return 
EC_OK;
   
}

   
long Eat() {
      printf(
"Chew, chew, chew\n");
      return 
EC_OK;
   
}

friend 
class Factory;

private
:
   Cow() : m_lRef (
0) { 
      printf(
"Cow component created\n")
   
}

   
virtual ~Cow() { 
      printf(
"Cow component destroyed\n")
   
}

   
long m_lRef;
};


// component factory
class Factory 
{
public:
   
static long Create(const long clsid, void** ppVoid) 
{
      
long lRet EC_NOINTERFACE;
      
*ppVoid   = NULL;

      switch
(clsid) 
{
         
case CLSID_Cow:
            *ppVoid 
= static_cast<void*>(new Cow);
            break;
      
// switch

      // update the reference count
      
if(*ppVoid) 
{
         
static_cast<;IBase*>(*ppVoid)->AddRef();
         
lRet EC_OK;
      
}

      
return lRet;
   
}
}
;

void 
main()
{
   IMouth* pIMouth 
= NULL;
   long    
lRet    EC_FAIL;

   
// create component
   
lRet Factory::Create(CLSID_Cow, (void**)&pIMouth);

   if
(lRet == EC_OK) {
      pIMouth->
Talk();
      
pIMouth->Eat();

      
// release interface
      
pIMouth->Release();
      
pIMouth = NULL;
   
}
}

Error Codes
One thing that you will notice with all interface methods is that they all return an error code. The only exceptions are AddRef() and Release(), which return the current reference count.

IBase Interface
All components must support the base interface IBase, it is the only way to manage a component's lifetime.

If you've studies the code carefully, you will notice that the component has no way to support multiple interfaces! What's missing is a way to request for a particular interface from the component.

Requesting an Interface
A component with a single default interface is useless. What we need to add is the logic to be able to request a component for a supported interface. To do this we need to extend the IBase interface with one more method:

   virtual long QueryInterface(const long iid, void** ppVoid) =0;

The first parameter is the interface id
The second parameter is a pointer that gets assigned a returned interface

Interface IBase 
{
   
virtual long AddRef()  0;
   virtual long 
Release() 0;
   virtual long 
QueryInterface(long iid, void** ppVoid) 0;
};

Extending the component to support a second interface

1. Define a new interface

Interface ILegs : public IBase 
{
   
virtual long Walk()  0;
   virtual long 
Stand() 0;
   virtual long 
Run()   0;
};

2. Update the component definition and support the new interface methods

class Cow : public IMouth, public ILegs 
{
public:
   
long Walk() {
      printf(
"Walking\n");
      return 
EC_OK;
   
}

   
long Stand() {
      printf(
"Just hanging\n");
      return 
EC_OK;
   
}

   
long Run() {
      printf(
"Sorry, but I got to run!\n");
      return 
EC_OK;
   
}
}
;

3. Add support for QueryInterface

class Cow : public IMouth, public ILegs 
{
...
public:
   
long QueryInterface(const long id, void** ppIFace) 
{
      
long lRet EC_NOINTERFACE;
      
*ppIFace  = NULL;

      switch
(id) 
{
      
case IID_IBase:
      
case IID_IMouth:
         *ppIFace 
= static_cast<IMouth*>(this);
         break;

      case 
IID_ILegs:
         *ppIFace 
= static_cast<ILegs*>(this);
         break;
      
// switch

      
if(*ppIFace) {
         
static_cast<IBase*>(*ppIFace)->AddRef();
         
lRet EC_OK;
      
}

      
return lRet;
   
}
...
}
;

4. Update the class factory so we can request an interface during the component's creating process

// component factory
class Factory 
{
public:
   
static long Create(const long clsid, const long iid, void** ppIFace) 
   {
      
long lRet   EC_NOINTERFACE;
      void
* pVoid = NULL;
      
*ppIFace    = NULL;

      
// supported components
      
switch(clsid) 
      {
         
case CLSID_Cow:
            pVoid 
= static_cast<void*>(new Cow);
            break;
      
// switch(clsid)

      
if(pVoid) 
      {
         
// initailly increase reference count
         // if QI fails it won't increase the reference
         // count, so the call to release will destroy
         // the component rather then leave it in limbo!!!
         
static_cast<IBase*>(pVoid)->AddRef();
         
lRet = static_cast<IBase*>(pVoid)->QueryInterface(iid, ppIFace);
         static_cast
<IBase*>(pVoid)->Release();
      
}
      
      
return lRet;
   
}
}
;

What we've learned...

Home

Rajinder Yadav Copyright © 2001, All Rights Reserved