Print
Category: Zeus Framework
Hits: 4519

Index of Interface Concept


1. Introduction

The big difference between Zeus-Framework and C++ Frameworks such as STL and BOOST is that Zeus is using interfaces. We will see why interfaces are necessary meeting the goal of this framework and how it is implemented.


2. Problem

The main problem of a modular software concept is that we want to use libraries built with different development tools. This is mainly the case if our project is huge and we have to operate in a heterogeneous world of software libraries. To show you where the problem exactly is I want to explain it referring following situation:

This situation is more complex than it seams, specially if we want to use classes from VC++ libraries in Borland C++ applications. The main problem is that VC++ and Borland C++ uses different heap structures. Each system has its own heap. It can be accessed by the other system, but new() and delete() operators will fail.

If you want to see the problem and play around, I created the example project 'LocalHeap'. Download the latest Zeus-Framework Source Code. The following code snips are taken from this example project.

Note for Linux:
I don't know if there is such a problem with linux libraries. But using interfaces is a good OO design and decouples the class code of libraries. There fore I recommend to use interfaces also for modular Linux applications.


3. Interface Design

What are interfaces and how are they implemented? In C++ we have to define the interface concept ourself, because the C++ language does not actually know what interfaces are (other than Java). Interfaces

3.1 Creating and deleting objects

First we have to solve the new() and delete() problem. To create an object in a foreign library (such as VC++ in our case), we export a factory function from that library. Note that we don't return an interface yet. We will fix that later.


extern "C" __declspec(dllexport) void __stdcall 
  createMyObject(TMyObject*& rpObject)
{
  rpObject = new TMyObject();
}

The objects of class TMyObject is created on the local heap of the VC++ library. How can we free the allocated memory now? Following code will fail in Borland C++, because we want to delete a pointer allocated on a foreign heap:


TMyObject* pObject = NULL;
createMyObject(pObject);
pObject->foo();
..
delete pObject; // <- abnormal program termination

So we need a method to free the object. We extend our class with the method release() witch deletes the this pointer. The code of release() will be executed in the context of VC++. Now the delete() works.


void __stdcall TMyObject::release()
{
  delete this;
}

3.2 Calling methods

The next problem we run into are inline methods. If the class TMyObject has any inline methods allocating or releasing memory resources, we have the problem again, that we want to manipulate the foreign heap. This is basically one of the main reasons working with interface. Our TMyObject must have an interface IMyObject with abstract methods only. The Borland C++ will know only about the interface declaration and not about the concrete class TMyObject. Besides solving our local heap problems this is also useful to hide code of the VC++ library, the main reasons interfaces are used on many system.

3.3 Exceptions

I was ask why does Zeus is not using exceptions? Exceptions is a great concept for error handling if it works. In our case here it unfortunately doesn't!! Throwing an exception in VC++ library can not be catched by the Borland C++ application. The program will be terminated with abnormal program termination. Up to now I have not more informations about that, rather than the debug information of VC++:


(NTDLL.DLL): 0xC0000025: Noncontinuable Exception.
(NTDLL.DLL): 0xC0000025: Noncontinuable Exception.
...
(KERNEL32.DLL): 0xC00000FD: Stack overflow 

Maybe it has something to do with an exception handler or local heap as well.

3.4 Basic Interface

Now we go one step back and start from the base. We create an interface called IZUnknown, witch will be the basic interface of all our interfaces in the future. We declare our release() method here. The methods addRef() and askForInterface() I will explain later in this text. We also declare an interface called IMyObject for our TMyObject class.


class IZUnknown
{
  public:
    virtual Retval __stdcall askForInterface(Uint uiInterfaceID, 
                                             IZUnknown*& rpIface)=0;
    virtual void __stdcall addRef() const=0;
    virtual void __stdcall release() const=0;
};

class IMyObject : public IZUnknown
{
  public:
    virtual void __stdcall foo() = 0;
    ...
};

class TMyObject : public IMyObject
{
  public:
    TMyObject();
    virtual ~TMyObject();
    
    //Methods of IMyObject
    virtual void __stdcall foo();
    ...
    
    //Methods of IZUnknown
    virtual Retval __stdcall askForInterface(Uint uiInterfaceID, 
                                             IZUnknown*& rpIface);
    virtual void __stdcall addRef();
    virtual void __stdcall release();
  
  private:
    ...
};

extern "C" __declspec(dllexport) void __stdcall 
  createMyObject(IMyObject*& rpObject)
{
  rpObject = new TMyObject();
}


The Borland C++ will see only the interfaces IZUnknown and IMyObject. Because the methods are abstract, the linker can not link the methods, we call this late binding.

3.5 Interface Methods

After a while we want to extend our interface calling methods more complex than foo() like setting and getting strings. We want to use the STL std::string as our string container.


class IMyObject : public IZUnknown
{
  public:
    virtual void __stdcall foo() = 0;
    virtual std::string __stdcall getData() const = 0;
    virtual void __stdcall setData(const std::string& rData) = 0;
    ...
};

Unfortunately we have to find out that this will cause some code guard error like "pointer arithmetic in invalid memory". Looks like Borland C++ has not the same STL like VC++ does. So finally we see that using STL in interfaces wont work in any way. Now we replace the std::string with TString from Zeus. Because Borland C++ and VC++ uses the same TString-declaration we have no "pointer salad" this time. But we see that calling the getData() method will cause still a abnormal program termination. The reason is that the method getData() will create a TString Object on the stack as return value. The constructor of TString will allocate memory in VC++ heap. Returning from the method getData() (on the Borland C++ side now), the object is poped from the stack and afterwards destroyed. The destructor of TString will try to release the allocated memory now in the foreign VC++ heap.

At the end we see that we better work with parameters instead of returning objects by value. We remember that we also had a problem with inline methods. To solve all those problems we better use real interfaces for method parameters. That means we need a interface for strings, maps, sets, lists etc. And thats one of the big differences to other development libraries like STL or BOOST. Inside a library from Borland or VC++ we might use STL, but when we want to communicate throw interfaces we need wrapper classes for STL. Any how lets see how our interface looks like now:


class IMyObject : public IZUnknown
{
  public:
    virtual void __stdcall foo() = 0;
    virtual void __stdcall getData(IString& rData) const = 0;
    virtual void __stdcall setData(const IString& rData) = 0;
    ...
    //This method needs a string member variable of the implementation
    // class. 
    virtual const IString& __stdcall getData2() const = 0;
};

3.6 Zeus Implementation

The Zeus-Framework provides the base class of interfaces already. You must include zeusbase/System/Interfaces/IZUnknown.hpp. Other interfaces you might often use are

To create the exported createIMyObject() function you might use macros as well.


#include <MyObject.h>
///////////////////////////////////////////////
// Exported Factories
MExportObjectFactory(IMyObject, TTMyObject);
///////////////////////////////////////////////

//This are registring functions to use global singletions 
MregisterLibrary(L"WebServer");
MunregisterLibrary()

MExportObjectFactory creates the exported function createIMyObject. For Linux GNU GCC and Bodland C++ this will create the correct exported function. Unfortunately VC++ creates something like _createIMyObject@4. We need a def-file for the correct signature.


EXPORTS

createIMyObject
registerLibrary
unregisterLibrary

To get the exported createIMyObject() function you have to load the library dynamically. The Zeus-Framework provides the singleton LibraryManager to manage the libaraies.


4. Memory Management

4.1 Self made

An other well known problem of C++ software developers is the memory management. Specially at huge applications the management is very important. We have to release allocated memory and take care once we deleted an object, no one else accesses the object again. Mostly the problem appears while sharing a pointer with other software modules. Following questions has to be answered:

We met one method already for memory management using interfaces, our release(). Now to answer the two questions we want to extend our object, that each time someone needs the pointer of the object, he needs to increase a counter (addRef()). If he does not use the pointer any more he needs to decrement the counter (release()). If this counter reaches 0 (no one has a pointer of the object anymore) we can delete the object. Following code will implement this functionality:


TMyObject::TMyObject()
{
  m_lRefCounter = 1;
}

void __stdcall TMyObject::addRef()
{
  InterlockedIncrement(&m_lRefCounter);
}

void __stdcall TMyObject::release()
{
  if (InterlockedDecrement(&m_lRefCounter) == 0)
  {
    delete this;
  }
}

Note for Linux
InterlockedIncrement() and InterlockedDecrement() are thread safe methods
The InterlockedIncrement(&m_lRefCounter) is similar to ++m_lRefCounter;
The InterlockedDecrement(&m_lRefCounter) is similar to --m_lRefCounter;

Now the questions are answered. The release() call who decrements the counter to zero will delete the object. So every release() call can potentially delete the object, depending if there are other pointers of this object allocated with addRef() or not.

4.2 Zeus Implementation

The Zeus-Frameworks implements such an implementation already. The base class of TZObject implements the memory management. To use this memory management you must use TZObject as your base class.

Header file:


#include <zeusbase/System/ZObject.h>
#include <IMyObject.hpp>

class TMyObject : public TZObject, public IMyObject
{
  public:
    TMyObject();
    virtual ~TMyObject();
    
    //Methods of IMyObject
    virtual void __stdcall foo();
    ...
    
    //Macro for memory manager declarations 
    //(Methods of IZUnknown)
    MEMORY_MANAGER_DECL
  
  private:
    ...
};

Source file:


TMyObject::TMyObject() : TZObject()
{}

TMyObject::~TMyObject()
{}

void __stdcall TMyObject::foo()
{
  ...
}

//Macros of memory management
MEMORY_MANAGER_IMPL(TMyObject);
  // see next chapter 'Inheritance of interfaces'
  INTERFACE_CAST(IMyObject, INTERFACE_IIMyObject); 
MEMORY_MANAGER_IMPL_END;

Note for portable coding:
If you want to use your code on other platforms as well we recommend to replace the __stdcall with the Zeus macro MQUALIFIER, because Linux does not know standard calls.


5. Inheritance of interfaces

Since C++ has no keyword to implement an interface (like Java 'implements'), we have to use the regular inhertitance mechanism of C++. We are lucky that multiple inheritance is still allowed in C++ ;-) otherwise we wont be able to use interfaces economically. But using multiple inheritance in C++ has its price. Lets explain some problems and bug fixes in this chapter.

5.1 C++ casts

Using interfaces with C++ needs a special note about casting. I made a test application to show the differences between the casts. It will be included after releasing version 0.5.1.

Assume we have following class hierarchy:

The IZUnknown is our base interface and we use the base class TZObject for our memory management (see previous chapter). Now we designed two interfaces IInterfaceA and IInterfaceB and their implementation TClassA and TClassB.

In the terminology of casting we are using following expressions

C++ knows 4 kind of castings and includes the C cast using brackets.

The reinterpret cast casts a pointer explicit to an other pointer type, checking only the sizes of the pointers to convert (function to object casts and inverse). This cast is not useful for our interface casting, because it does not care about multi inheritance, witch we need since we have no implements-keyword like Java does.

The static cast is similar to the C-cast using brackets. But static cast can not really down cast. The down cast works only for single inheritance. And therefore the cross cast does not work at all. So we can not use the static cast for our interface casting.

The const cast is only used to cast the const qualifier away.

At the end we have a look at the dynamic cast. The dynamic cast is able to down and cross cast, because it uses the type information of the classes. The cast is done at runtime, an it takes more process time than any other cast. I've found out that there are still two problems using dynamic cast.

5.2 Problems with dynamic casts

The first problem occurs if a base class or interface is inherited more than once. Sets look at the following example. The dynamically casted variable IA* pA is set to NULL. We find the reason at the multiple inheritance of class TD. IA has been inherited twice, one over IB and once over IC.


class IA
{
  public:
    virtual void fooA() = 0;
};

class IB : public IA
{
  public:
    virtual void fooB() = 0;
};

class IC : public IA
{
  public:
    virtual void fooC() = 0;
};

class TD : public IB, public IC
{
  public:
    virtual void fooA(){}
    virtual void fooB(){}
    virtual void fooC(){}
};

int main(int argc, char* argv[])
{
  TD* pD = new TD();
  IA* pA = dynamic_cast<IA*>(pD);

...
  delete pD;
  return 0;
}

The second problem occurs if the dynamic cast has no type informations to access to or the type informations are built in a different way. This happened to me when I tried using a VC++ library within Borland C++. It looked to me like it could not access the type information inside the VC++ library.

I received following error:


Assertion failed: topTypPtr != 0 && IS_STRUC(topTypPtr->tpMask), 
  file xxtype.cpp, line 847

Abnormal program termination

5.3 The solution

To solve these problems we have to create our own casting method. So we extend our base interface with the third method askForInterface(), witch is used to cast an interface to a different interface type. Since we are gentle we ask the object if it implements also a requested interface calling this method. If it does, it will return a casted interface pointer, otherwise it will return an error.

To ask the object for specific interfaces we need a interface ID to specify the requested interface. In Zeus-Framework we use a simple number as interface ID. The method is pretty simple to implement then.


Retval TClassB::askForInterface(Uint uiInterfaceID, 
                                IZUnknown*& rpIface)
{
  Retval retValue = RET_REQUEST_FAILED;
  
  switch(uiInterfaceID)
  {
    case INTERFACE_IInterfaceB :
      rpIface = static_cast<IInterfaceB>(this);
      rpIface->addRef();
      retValue = RET_NOERROR;
    break;
    
    default:
      //call the super class to avoid implementing any redundanc code
      retValue = TClassA::askForInterface(uiInterfaceID, rpIface);
    break;
  }
  
  return retValue;
}

This solves the problem of dynamic casts because we're using only up casts with static_cast. This is also a nice feature to hide information of the implementation.

5.4 Zeus Implementation

The base class TZObject implements the askForInterface() method for IZUnknown interface already. You can use the macros of the memory management also for this casting method.

TClassA inheriting directly the base class TZObject


//Implementation of Class A
MEMORY_MANAGER_IMPL(TClassA);
  INTERFACE_CAST(IInterfaceA, INTERFACE_IInterfaceA);
MEMORY_MANAGER_IMPL_END;

The macro INTERFACE_CAST is building the switch-case structure. The macro MEMORY_MANAGER_IMPL_END ends the switch and delegates the call to the TZObject at the default part.

TClassB inheriting TClassA


//Implementation of Class B
MEMORY_MANAGER_IMPL(TClassB);
  INTERFACE_CAST(IInterfaceB, INTERFACE_IInterfaceB);
MEMORY_MANAGER_IMPL_PARENT_END(TClassA);

The macro MEMORY_MANAGER_IMPL_PARENT_END is used to delegate the call to a different class than TZObject. In our case we delegate to our super class TClassA.

 


6. Summary

As you can see working with interfaces is a nice thing, but you have to pay a price as well. Using interfaces brings you a lot of advantages:

Of course there are some disadvantages as well. We didn't find the world term yet :-)

Be strict and consequent using interfaces!!!