Interface Basics

Environment: COM

Purpose

This article is intended for software programmers who have never been exposed to the COM technologies. The article doesn't cover the COM fundamentals; however, it does cover the basic concepts of an interface. It explains using the powerful C++ language, how a class eventually becomes an interface, and the reasons for this formation. The article is just a brief introduction with what is actually going on behind the scenes of an interface.

Contents

Introduction
Components History
Using an Object-Oriented Approach (OO)
Limitations of C++
The Basic Idea of an Interface Using C++
A Basic COM Interface Using C++
Interface Inheritance
Summary
About the Author

Introduction

It is very common among programmers to break up the code functionality into small, simpler pieces. Each piece is call a Component. The concept is to store these components into a library or libraries, and to approach them (the components, that is) using an API service (Application Programming Interface). The re-use of these libraries is very simple and powerful and can be approached easily.

Components History

Many programmers have encountered several problems while developing components. The problems were related to the component side and also to the client that uses the component. Here are some of the major problems developers have encountered:

  1. Scope—The scope of a component was a problem both to the developer and the user of the component. Any change in the component by its writer could have altered the application that uses the component and cause re-compilation of the entire client code.
  2. Versions—How can one force a programmer to check an interface version? The maintenance and version publication can be a problem.
  3. Communications—Component communication is also a problem, especially if several people were involved in the writing process.
  4. Language—If a component was written in C++, how can one approach it using Visual Basic or C, for example?

There were several solutions to the problems of developing components, but one of the powerful ones was the usage of an object-oriented approach.

Using an Object-Oriented Approach (OO)

One of the most common techniques to write a component is to use the object-oriented approach. The use of the object-oriented approach allows a programmer to approach an application in a more abstract way: Looking at an application as a collection of objects, each object has its own unique qualities and one object can communicate with an other object. Using the object-oriented approach makes it easier on a programmer to understand the complexity of the application and to find a proper solution to simplify the problem. Designing an object is much more understandable than to read procedural algorithms. One of the languages that uses the object-oriented concept is C++. In this article, the examples were written in C++ (a basic knowledge of C++ is required).

Limitations of C++

One of the programming languages to implement object oriented is C++. C++ is limited from the component access point of view:

  1. Size does matter I—Suppose we have two classes, class A and class B, and class B inherits class A. Now, suppose we want to add a new data member to class A (the parent). Automatically, the size of class B will be changed (or its virtual table size), and we will need to compile the code for class B again.
  2. Distinction—There is no clear way to distinguish between exactly where the implementation of the classes is (in the previous section, class A and class B). The implementation of the methods in both classes can be in the parent (class A) or the child (class B). C++ doesn't provide an easy distinction.
  3. Size does matter II—Sometimes a client that uses the component (classes A and B in the previous sections) needs to know the component size up front. Not knowing the size may cause synchronization problems between the client and the component.
  4. Size does matter III—Suppose we add a virtual function to the parent class (class A in the previous sections). Now the class will have a pointer to a virtual table and each inherited class will contain a pointer to the virtual table as well. This means that each inherited class will now contain four more bytes (for the pointer to the virtual table) and if a client that uses the class counts on the class size, the communication may be broken.

To solve the problems above, we use interfaces. What exactly is an interface? How do we write code for defining an interface? How can we communicate with an interface? Answers to these questions and much more can be found in the the following section.

The Basic Idea of an Interface Using C++

Consider the following class:

class CExampleArray
{
public:
  int getLength() { return m_iLength; }

private:
  int m_iLength;
  int    m_ArrVec[100];

};

If we create an instance of the above class, it will waste memory space because of the pre-definition of the m_ArrVec to be at the size of 100. What if we need an array of more than 100 integers? This class is inadequate for a definition of a general array.

We can resolve the problem by altering the fixed size to be a pointer, and storing the size of the array in another data member as in the following class:

class CExampleArray
{
public:
  int getLength() { return m_iLength; }

private:
  int        m_iLength;
  int*       m_ArrVec;
  short      m_iArrSize;
};

Because data members were changed and added to the above class, it should be re-compiled and also any client using this class needs to be re-compiled as well to work with the new class. Now, in the new version of the class, an instance of the class won't take wasteful memory space, but any client depending on the size of the array will have a problem (because one client can define an array with 20 elements of integer and one can define an array with 100 elements of integer).

Besides members that changed the size of the class in memory, we also can add virtual functions that will alter the size as well:

class CExampleArray
{
public:
  virtual void ReverseArray();
  int getLength() { return m_iLength; }

private:
  int       m_iLength;
  int*      m_ArrVec;
  short     m_iArrSize;
};

A virtual table will be created for this class because of the virtual method ReverseArray, and each instance of the class will contain a pointer to the virtual table (VPTR) that will add four bytes (pointer size) to the total class size. Again, client communication to the class might be broken.

Suppose we don't want to let a client modify the class data members. We can restrict the client from any data modification of our class and move the data handling to another class:

class CExampleArray
{
public:
  virtual void ReverseArray();
  int getLength() { return m_pDataImpl->getLength(); }

private:
  CExampleArrayDataImpl* m_pDataImpl;
};

class  CExampleArrayDataImpl
{
  // the data implementation here
}

Now, all the data members are handled in the class CExampleArrayDataImpl. To retrieve or modify the data members, we access the implementation class CExampleArrayDataImpl by invoking its methods using the m_pDataImpl pointer. The size of the implementation class can be changed (i.e. adding new data members, adding virtual functions) but this doesn't cause the base class size to be changed because it only holds a pointer to the implementation class.

The following diagram shows how both classes (CExampleArray and CExampleArrayDataImple) will be presented in memory:



Click here for a larger image.

The class CExampleArray still holds a data member (a pointer to CExampleArrayDataImple). Our goal is to create a class without any data members, a class that exposed only by its methods, and allows inherited classes to implement the exposed methods.

In order to do that, we need to change all the method headers in the class CExampleArray to be pure virtual and we also need to remove all the data members from the class:

class CExampleArray
{
public:
  virtual void ReverseArray() = 0;
  virtual int  getLength()    = 0;
};

This is an abstract class; it contains only a pure virtual declaration of functions (the " = 0 " indicates the purity). All the data members were removed.

This abstract class is known as an Interface. An interface is basically a class, containing only pure virtual functions and has no data members.

A client that will use this interface will only use a pointer to the interface, and won't need to count on the interface size. All the data of the interface are hidden from the client, and implemented in the inherited classes of the interface. Syntactically, an interface class is usually prefixed with an 'I'. From now on, I will refer to the interface class CExampleArray as IExampleArray.

So far, we've learned what an interface looks like. Now, let's take a look at what a basic COM interface looks like:

A Basic COM Interface Using C++

Let us look on how COM interfaces are created using C++. C++ does not allow us to create an instance of an abstract class; in other words, the following line of code is not valid:

IExampleArray* pNewInstance = new IExampleArray;

The interface class can only be approached by using a pointer to the virtual table that exposes the methods in the interface. The interface does not come by itself; it usually comes with an inherited class that implements the exposed method in the interface. Such a class that implements the interface exposed methods is often called a co-class. Here is an example of a co-class:

class CExampleArrayImpl : public IExampleArray
{
public:
  virtual void ReverseArray() { // implementation here }
  virtual int  getLength()    { // implementation here }
};

Because the implementation class CExampleArrayImpl has no pure virtual functions, we can easily create an instance of it. We will now write a method that performs a new operation on the implementation class (co-class) and returns a valid pointer to the interface (this can be done due to the powerful C++ technique called polymorphism). We can use a global method to create an instance of the co-class or we can use a static method as well. The technique of using a method that creates an instance of a co-class and returning a pointer to its interface is often called Class Factoring. Here is the global create instance method:

IExampleArray* CreateArrInstance()
{
  return new CExampleArrayImpl;
}

Now, if a client wants to use the interface, all it has to do is obtain a pointer to the interface object by calling the CreateArrInstance method, and then invoke the methods of the interface:

int UseTheInterfaceMethod()
{
  // obtain a pointer to the interface
  IExampleArray* pArr = CreateArrInstance();
  // invoke the interface method
  int iLength = pArr->getLength();
  // exit successfully
  return 0;
}

The client doesn't see the class CExampleArrayImpl or use the operator new directly. It only knows the virtual table of the IExampleArray interface. Interfaces can be inherited in the same way as classes in C++.

Interface Inheritance

We can create another interface that will inherit from the IExampleArray and will be an extent of the IExampleArray (will have the same two methods as IExampleArray and will add a new method of its own):

class IAnOtherExampleArray : public IExampleArray
{
public:
  // the two methods from the interface IExampleArray must appear
  // here as well

  virtual void ReverseArray() = 0;
  virtual int  getLength()    = 0;

  // new exposed method added by this interface
  virtual BOOL Find(int iKey) = 0;
};

Notice that inheritance is allowed between interfaces because they are classes, after all. We now can create a new co-class (implementation class for the IAnOtherExampleArray interface):

class CExampleArrayImpl : public  IAnOtherExampleArray
{
public:
  virtual void ReverseArray() { //implementation here }
  virtual int getLength()     { //implementation here }

  // the new method implementation
  virtual BOOL Find(int iKey) { // implementation here}
};

A client that will use the above interfaces will see both IExampleArray and IAnOtherExampleArray but will not see the CExampleArrayImpl and CAnOtherExampleArrayImpl co-classes. In order to obtain a pointer to the new interface IAnOtherExampleArray, the client must use a dynamic cast as follows:

int UseTheInterfaceMethod()
{
  // obtain a pointer to the interface
  IExampleArray* pArr = CreateArrInstance();
  // invoke the interface method
  int iLength = pArr->getLength();

  // obtain a pointer to the new interface
  IAnOtherExampleArray * pNewArr =
          dynamic_cast<IAnOtherExampleArray *> (pArr);
  // invoke new interface method
  pNewArr->Find(1);

  // exit successfully
  return 0;
}
Remark: In order to make the above code work with the Visual Studio environment, make sure you compile it with the option '/GR', which enables the run-time type information. Otherwise, you will get a compilation warning (C4541).

Summary

By now, hopefully you better understand what lies beneath the interface concept. I did not cover COM issues in my article because there are excellent articles regarding the technology both in MSDN and on the developers' sites.

About the Author

My name is Yaron Nir, yaronnir@hotmail.com. I am 28 years old, married, and live in Tel-Aviv, Israel. I have been a developer of applications for the Windows environment for almost five years now. My main specialties are: C, C++, Win32 API, MFC, and COM.



Comments

  • Excellent

    Posted by Legacy on 10/05/2003 12:00am

    Originally posted by: kshitija patwardhan

    Excellent introduction of interfaces......

    Reply
  • how to make it work?

    Posted by Legacy on 08/18/2003 12:00am

    Originally posted by: jianyang

    This is a very good article. thanks!

    when i implement this idea to my code and i am confusing in onething: the des-constructor never called when i exit the program.

    herebelow is the implemented co-class and
    ::~CExampleArrayImpl() never be called.
    why?

    how can make it work? since i need this to release some resources automatically.
    thank you.


    //////////////////////////////////
    // EcampleArrayImp1.h
    class CExampleArrayImpl : public IExampleArray
    {
    public:
    CExampleArrayImpl();
    virtual ~CExampleArrayImpl();

    public:
    virtual void ReverseArray(){return;} //
    virtual int getLength();
    virtual int getLength2();
    virtual int CreateMyWnd();

    private:
    // CWnd* pMyWnd;
    };


    //ExampleArrayImp1.cpp
    //...
    CExampleArrayImpl::~CExampleArrayImpl()
    {
    TraceEx("CExampleArrayImpl::~CExampleArrayImpl() \n");
    }
    ////////////////////////////////////////////////////////

    Reply
  • Did you read this?

    Posted by Legacy on 08/13/2003 12:00am

    Originally posted by: Dudi Avramov

    There is a goog MSDN article "From CPP to COM" with several samples that leads to COM understanding.

    Reply
  • Simple and clear

    Posted by Legacy on 08/13/2003 12:00am

    Originally posted by: Pierre Couderc

    Thank you

    Reply
  • good teaching

    Posted by Legacy on 08/12/2003 12:00am

    Originally posted by: SpaceGhost

    It is *OK* to teach subject matter that may not be horribly difficult. The article is well written and has good content, which is more than I can say for Mr Liu or the would be Gates. Sorry for those who can not accept learning proccess. They will never teach anyone anything of value get the sense of accomplishment from doing so.

    -SG

    Reply
  • Don't be put off by fools that post stupid comments

    Posted by Legacy on 08/11/2003 12:00am

    Originally posted by: Hans Wedemeyer

    Please post more good stuff like this...

    Thanks

    Reply
  • You must have javascript enabled in order to post comments.

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: May 6, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT While you likely have very good reasons for remaining on WinXP after end of support -- an estimated 20-30% of worldwide devices still are -- the bottom line is your security risk is now significant. In the absence of security patches, attackers will certainly turn their attention to this new opportunity. Join Lumension Vice President Paul Zimski in this one-hour webcast to discuss risk and, more importantly, 5 pragmatic risk mitigation techniques …

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds