Page #25 (Hardware Component Model)


Separating Interface from Implementation

Looking at our VCR code, at first glance it appears that the public member functions of our CVcr class definition form our video interface specification. By changing the private specifications, we still honor the basic rule of C++ encapsulation: Thou shall not change the public specifications of a C++ class. Then why did our TV simulation program break?

The reason is that, the encapsulation mechanism that C++ supports is semantic (in terms of public and private members) and not binary. To understand this distinction, let s examine how the compiler produced the object code for our TV program.

When the TV code was compiled, the compiler used two pieces of information from the CVcr class definition:

  1. The interface specifications: To ensure that the TV program calls only the publicly available methods from the CVcr class and that the parameters being passed to these methods match the specifications.

  2. The implementation details: The compiler needs to know the binary size of the class, that is, the amount of memory occupied by an instance of the class. This helps the compiler allocate appropriate memory when creating an instance of the class.

When we added the new variable to CVcr class, we changed its size. The first version of the class had a size of four bytes. The second version of the class had a size of eight bytes. As we didn t recompile our TV program with the second version of the class, the program continues to allocate just four bytes for the CVcr instance. However, the VCR code was recompiled with the second version, and therefore expects a CVcr instance to occupy eight bytes. When the VCR code writes to the memory location for the variable m_nCurCount, it ends up stomping on a memory location that was being used by variable i in the TV program. That explains the unexpected behavior.

Our modifications to the VCR code would have worked correctly had we ensured binary encapsulation, i.e., had we modified only the implementation code without actually adding any member variables to the CVcr class.

Of course, I deliberately modified the VCR code such that the resulting behavior was an infinite loop. In general, whenever binary encapsulation gets broken, the behavior is completely unpredictable. The application may hang, crash right away, or the bug may stay dormant during the whole testing period and manifest itself when the customer uses it for the first time.

To ensure binary encapsulation, an obvious solution is to separate interface specifications from the implementation details. We can achieve this by breaking our original class into two classes an interface class and an implementation class. The interface class should only define the methods that a client is allowed to call; it should not reveal any implementation details. The implementation class contains the entire implementation specifics. The idea here is that the binary layout of the interface class will not change as we add or remove member variables from the implementation class. The clients do not need to know any details from the implementation class. This effectively hides all the implementation details from the client.

Once we define the interface class and the implementation class, we need a way to traverse from the interface class to the implementation class. Defining an opaque pointer as a member variable in the interface class can do the trick. An opaque pointer requires just a forward declaration of the class it is pointing to, not the full definition of the class. Any method called on the interface class will just turn around and invoke an appropriate method through the opaque pointer. The logic is illustrated in the following code snippet:

 // Video.h Definition of interface IVideo  // Forward declaration of CVcr class  class CVcr;  class IVideo  { public:    IVideo();    ~IVideo();    long GetSignalValue();  private:    CVcr* m_pActualVideo;  };  // Video.cpp Implementation of interface IVideo  IVideo::IVideo()  {   m_pActualVideo = new CVcr;  }  IVideo::~IVideo()  {   delete m_pActualVideo;  }  long IVideo::GetSignalValue()  {   return m_pActualVideo->GetSignalValue();  }  ; File VCR.DEF  ; Need to export  ; IVideo::IVideo  ; IVideo::~IVideo  ; IVideo::GetSignalValue  LIBRARY   VCR.DLL  EXPORTS    ?GetSignalValue@IVideo@@QAEJXZ    ??0IVideo@@QAE@XZ    ??1IVideo@@QAE@XZ 


We are referring to our interface definition class as IVideo. By prefixing I to a class name, we are just setting up a convention to represent an interface class.

With the IVideo/CVcr mechanism in place, the TV program will create an instance of IVideo class and use it, as shown here:

 #include "Video.h"  #include <iostream.h>  int main(int argc, char* argv[])  {   int i;    IVideo vcr;    for(i=0; i<10; i++) {     long val = vcr.GetSignalValue();      cout << "Round: " << i << " - Value: " << val << endl;    }    return 0;  } 

As long as we do not change the definition of IVideo, our TV program will continue to work with any upgraded version of vcr.dll. As a VCR vendor, we should not have any problems in guaranteeing the immutability for IVideo class, as we are not being held back from changing the implementation anytime later.

It seems like our original goal of field-replacing a component without any adverse affect on the overall system has been accomplished. By adding dynamic linking capability, we are able to field-replace a DLL. By separating interface from implementation and ensuring that the binary layout of the interface will never change, we have removed any adverse affect of field-replacing the DLL.

There are still a few weaknesses in the technique that was used to associate the interface with its implementation. These weaknesses are as follows:

  • Compiler dependency: In order to use the interface class, all of the methods that the class exposes must be exported. If you recall, these methods need to be exported as name-mangled symbols. The problem is, the compiler vendors have not agreed upon a standard name-man-gling scheme. Each compiler vendor implements its own name-mangling scheme. Lack of a name-mangling standard forces us to use the same compiler for each component.

  • Performance penalty: The call to each method incurs the cost of two function calls: one call to the interface and one nested call to the implementation.

  • Error prone: For a large class library with hundreds of methods, writing the forward call is not only tedious, but also susceptible to human errors.

Let s begin by focusing on the first problem, compiler dependency. In order to ensure that the interface class mechanism works across compiler boundaries, we need to limit the interface class definition to aspects of C++ language that are implemented uniformly across all C++ compilers. Let s identify such aspects of the C++ language.


COM+ Programming. A Practical Guide Using Visual C++ and ATL
COM+ Programming. A Practical Guide Using Visual C++ and ATL
ISBN: 130886742
Year: 2000
Pages: 129 © 2008-2017.
If you may any questions please contact us: