Developing a Custom Interface-Based API: The Rect API

 < Free Open Study > 



Let's shift gears now, and examine how we might provide functionality to work with classes supporting multiple interfaces. We will be developing our own custom API to create and extract interface pointers from the C3DRect object examined in this chapter. In "real COM" the API we use is collectively called the COM library, and our API will provide similar, but extremely limited, functionality. We will need to contend with the following:

  • A client needs to be able to create a C3DRect object.

  • A client must be able to ask for access to a given interface on the object.

  • A client must be able to destroy the object when finished.

To keep things simple, our API will allow clients to work with a single global C3DRect pointer called ptheRect that may be created by the global CreateThe3DRect() function:

// Create the global 3D rectangle. C3DRect* ptheRect = NULL; void CreateThe3DRect() {      ptheRect = new C3DRect;      }

The DestroyThe3DRect() function simply deallocates the memory acquired from the CreateThe3DRect() call:

// Custom API to destroy a valid object void DestroyThe3DRect() {      if(ptheRect)        delete ptheRect; }

No problems so far. A client can activate and deactivate the C3DRect object with these two functions of our 3DRect API. In the code block below, notice that the client does not directly create or destroy a C3DRect object:

// Clients create and destroy interface-based objects indirectly. void main(void) {      // Activate the rect.      CreateThe3DRect();      // Terminate the rect.      DestroyThe3DRect(); }

Identifying Interfaces

In order for a client to extract a specific interface from the object, we need to set up some further API level support. We will create a method that is able to return an interface pointer to a client. But first, we need a way to uniquely identify every possible interface used in our current application. One approach would be to assign a text literal to each interface and perform some sort of string comparisons to test which interface pointer to return.

A simpler approach (and the one taken by COM) is to use unique numerical values. Let's use numerical tags and define a custom enumeration called INTERFACEID:

// In our system, we assign a unique numerical value to each interface. enum INTERFACEID {      IDRAW            = 0,           // ID for IDraw.      ISHAPEEDIT       = 1,           // ID for IShapeEdit.      IDRAW2           = 2,           // ID for IDraw2.      IDRAW3           = 3            // ID for IDraw3. };

Now that we have a way to refer to an interface by name in our code, we can create the final function in our API, GetInterfaceFrom3DRect(). This function will take two parameters: the requested interface identifier (from the INTERFACEID enum) and some place in memory to store the retrieved interface pointer. Furthermore, we will return a Boolean to indicate that the object did (or did not) support the requested interface. This will be handy for the client, as it can be used to test the success or failure of the function invocation (after all, who wants to call methods off a NULL pointer). Here is the prototype for the GetInterfaceFrom3DRect() function:

// Send in the number of the interface (INTERFACEID) and this method will return // a valid interface pointer. bool GetInterfaceFrom3DRect(INTERFACEID iid, void** iFacePtr);

Recall that an interface is really a pointer to a vPtr (which points to the vTable and to the implementation). We may generically represent any interface pointer as a void** parameter (an empty pointer to a pointer). Here is an implementation of the GetInterfaceFrom3DRect() API function:

// Dish out interface pointers to the client. bool GetInterfaceFrom3DRect(INTERFACEID iid, void** iFacePtr) {      if(ptheRect == NULL){           cout << "You forgot to create the 3DRect!" << endl;           return false;      }      if(iid == IDRAW){          // They want access to IDraw.           // Cast the client's pointer to the IDraw interface of ptheRect.           *iFacePtr = (IDraw*) ptheRect;           return true;      }      if(iid == ISHAPEEDIT) {     // They want access to IShapeEdit.           // Cast the client's pointer to the IShapeEdit interface of ptheRect.           *iFacePtr = (IShapeEdit*) ptheRect;           return true;      }      // I have no clue what you want.      *iFacePtr = NULL;          // Just to be safe.      cout << "C3DRect does not support interface ID: " << iid << endl;      return false; } 

Hopefully, the inner workings of this function remind you of the custom RTTI example assembled in the previous chapter (remember the employee hierarchy?). What did we do when we wished to extract the essence of a salesperson from a generic CEmployee base class pointer? We performed an explicit cast:

// Do we have a sales employee? if(e == SP) {      cout << "—>My Sales are "      << ((CSalesEmployee*)theStaff[i])->GetNumberOfSales()      << endl; }

We are doing the exact same operation here in the implementation of GetInterfaceFrom3DRect():

// Hand off pointer to IShapeEdit, and place it in the void** parameter (iFacePtr). *iFacePtr = (IShapeEdit*) ptheRect; 

When a client requests access to a given interface from the global C3DRect object, we first check if we support the required interface, and if so, we cast the empty void** to the given interface pointer by making an explicit cast. In effect, we are handing out the "IDraw- ness" or "IShapeEdit-ness" of the object. Don't let the slightly modified syntax fool you.

Using the 3D Rect API: Client Code

Using our brand new API, we may write client-side code that creates, destroys, and accesses interfaces off the C3DRect object. Here is our custom API in action:

// Client side code (assume proper #includes...) void main(void) {      bool retVal = false;      IDraw* pDraw = NULL;      IDraw3* pDraw3 = NULL;           IShapeEdit* pShapeEdit = NULL;      // Activate the 3DRect object.      CreateThe3DRect();      // Can I get the IDraw interface from object?      retVal = GetInterfaceFrom3DRect(IDRAW, (void**)&pDraw);      if(retVal)           pDraw->Draw();      // Get IShapeEdit from object?      retVal = GetInterfaceFrom3DRect(ISHAPEEDIT, (void**)&pShapeEdit);      if(retVal){           pShapeEdit->Fill(POLKADOT);           pShapeEdit->Inverse();           pShapeEdit->Stretch(90);      }      // Does this object support IDraw3?      retVal = GetInterfaceFrom3DRect(IDRAW3, (void**)&pDraw3);      if(retVal)           pDraw3->DrawToMetaFile();           // Done.      DestroyThe3DRect(); }     // End of main. 

In this code sample, a client first must activate the global C3DRect object using the CreateThe3DRect() function. To access a given interface from this object, we call the GetInterfaceFrom3DRect() function, passing in an interface ID and an empty pointer to a pointer (void**) to hold the acquired interface. Well, actually we don't send it a void** directly. We send in a pointer to the interface variable (by reference) we are looking for and cast it to a void** (this quiets down the compiler):

// Get IShapeEdit from object? retVal = GetInterfaceFrom3DRect(ISHAPEEDIT, (void**)&pShapeEdit);

GetInterfaceFrom3DRect() returns a Boolean, which the client may test against; if this call succeeds, we have access to each method in the requested interface. Finally, when we are all done with our C3DRect object, we indirectly destroy it with a call to DestroyThe3DRect().

Strange new world, don't you agree? While interface-based programming can take a bit of getting used to, there is no escaping it when developing and using COM components. To finish up this chapter, you will be creating and extending the interface-based API we have just examined. After this, you have prepped considerably for "real COM" development, as you will see beginning in Chapter 3, and throughout the remainder of this book.

Lab 2-1: Interface-Based Programming

The purpose of this lab is to get you prepped for real COM development. As you will see, the separation of interface from implementation is what COM-based programming is all about. This lab will give you the chance to define some interfaces in C++ as well as develop two classes supporting a number of these interfaces. You will create your own API that creates, destroys, and extracts interface pointers from a given object. Finally, you will have a chance to work with interface hierarchies and functions which take interface pointers as parameters.

 On the CD   The solution for this lab can be found on your CD-ROM under:
Labs\Chapter 02\Shapes

Step One: Prepare the Project Workspace and Interface Design

This lab will get you comfortable creating and using objects that support multiple interfaces. Begin by creating a new Win32 Console Application (an empty project) named Shapes, and insert a new text file named main.cpp. In this file, define an empty main loop.

Next, insert another new text file which will be used to define the interfaces in this program. Save it as interfaces.h. Now, in this new header file, define the IDraw and IShapeEdit interfaces. Be sure to wrap your interface definitions in #ifndef/#define/#endif syntax to prevent redefinition errors, as numerous project files will include interfaces.h. Finally, create an enumeration for use by the Fill() method of IShapeEdit:

// Don't forget to #include <windows.h> to use the interface keyword! #ifndef _IFACES #define _IFACES enum FILLTYPE { HATCH = 0, SOLID = 1, POLKADOT = 2 }; interface IDraw {      virtual void Draw() = 0; }; interface IShapeEdit {      virtual void Fill (FILLTYPE fType) = 0;      virtual void Inverse() = 0;      virtual void Stretch(int factor) = 0; }; #endif // _IFACES 

Step Two: Implement the C3DRect Class

Insert a new C++ class into your project named C3DRect, which supports both the IDraw and IShapeEdit interfaces using standard C++ multiple inheritance:

// C3DRect supports IDraw and IShapeEdit. class C3DRect : public IDraw, public IShapeEdit { public:      C3DRect();      virtual ~C3DRect();      // IDraw      virtual void Draw();      // IShapeEdit      virtual void Fill (FILLTYPE fType);      virtual void Inverse();      virtual void Stretch(int factor); };

Go ahead and implement the inherited interface methods using simple cout statements. For example, here is a possible implementation of Fill():

// Print out the fill pattern. void C3DRect::Fill(FILLTYPE fType) {      char* FillString[] = {"Hatch", "Solid", "Polkadot"};      cout << "Filling a 3D Rect as: " << FillString[fType] << endl; }

Use similar cout statements to implement Draw(), Inverse(), and Stretch(). You may also wish to place cout statements in the constructor and destructor of this class, which will be enlightening when running this lab later.

Step Three: Develop the Initial Rect API

We will be building our own API to create, destroy, and retrieve interface pointers from a C3DRect object. Insert another new file called rectfunctions.h. For simplicity, our API will only work with one object instance at a time, so go ahead and define a global level C3DRect pointer inside your new header file (yes this is contrived, but where better to be contrived than a lab?):

// Here is the global 3D rect. C3DRect* ptheRect;

Now, add prototypes for the methods that activate and destroy the object on behalf of the client. Feel free to use the same signatures as used during this chapter:

// Functions to operate on the 3D rect. void CreateThe3DRect(); void DestroyThe3DRect(); 

Implementing CreateThe3DRect() and DestroyThe3DRect() is trivial. Simply use the new and delete keywords to create and destroy the object:

// Creation function. void CreateThe3DRect() {      // Create a 3d-rect.           ptheRect = new C3DRect(); } // Destroy the rectangle. void DestroyThe3DRect() {      // See ya!      delete ptheRect; }

Now that we have provided a way to create and destroy the global rectangle, we need to provide a way to grab interfaces from an activated object. GetInterfaceFrom3DRect() needs to return an interface pointer to the client, based on an interface identifier. This function may be prototyped as:

// Global function used to fetch interface pointers. bool GetInterfaceFrom3DRect(INTERFACEID iid, void** iFacePtr);

Add a new enumeration to your interfaces.h file called INTERFACEID. Assign a numerical cookie for the IDraw and IShapeEdit interfaces:

// Custom enumeration to identify each interface. enum INTERFACEID { IDRAW = 0, ISHAPEEDIT = 1};

To implement GetInterfaceFrom3DRect(), examine the incoming interface ID, and perform an explicit cast against your global C3DRect object. Set the client's void** parameter to the result of this cast. For example:

// If we support the interface, fill the void** with the interface pointer. *iFacePtr = (IShapeEdit*)ptheRect;

Be sure that your implementation of GetInterfaceFrom3DRect() returns a Boolean, which the client may test against before calling the methods of the requested interface. Again, here is a possible implementation of GetInterfaceFrom3DRect():

// This method returns interfaces to the client. bool GetInterfaceFrom3DRect(INTERFACEID iid, void** iFacePtr) {      if(ptheRect == NULL){           cout << "You forgot to create the 3DRect!" << endl;           return false;      }      if(iid == IDRAW){          // They want access to IDraw.           // Cast the client's pointer to the IDraw interface of ptheRect.           *iFacePtr = (IDraw*) ptheRect;           return true;      }      if(iid == ISHAPEEDIT) {     // They want access to IShapeEdit.           // Cast the client's pointer to the IShapeEdit interface of ptheRect.           *iFacePtr = (IShapeEdit*) ptheRect;           return true;      }      // I have no clue what they want.      *iFacePtr = NULL;          // Just to be safe.      cout << "C3DRect does not support interface ID: " << iid << endl<< endl;      return false; } 

Step Four: Code the Initial Client

Let's take our C3DRect class out for a spin. Add code in your main loop to perform the following tasks:

// Fill up your main loop as so... void main() {      // 1) Call CreateThe3DRect().      // 2) Request interface pointers from the object by calling      //    GetInterfaceFrom3DRect(). Ask the object      //    for each interface defined in the INTERFACEID enumeration.      // 3) Call the methods off the acquired interface pointer.      // 4) Call DestroyThe3DRect() when you are finished. }

This code was seen previously in the chapter, so take a look if you get stuck. When you have succeeded, move onto the next step where we will work with a versioned rectangle.

Step Five: Version an Existing Interface

Let's insert another new class into our existing project called C3DRectEx. This class will support a number of versioned interfaces: IDraw, IDraw2, and IDraw3. First off, define IDraw2 and IDraw3 in your interfaces.h file using interface inheritance. Using the interface hierarchy in Figure 2-11 as your guide, add in whichever extra interface methods you wish for IDraw2 and IDraw3 (in this lab I will assume the same interface methods described in the chapter text).

click to expand
Figure 2-11: Drawing interface hierarchy.

// Additional interfaces in the drawing hierarchy. interface IDraw2 : public IDraw {      virtual     void DrawToMemory() = 0;      virtual     void DrawToPrinter() = 0; }; interface IDraw3 : public IDraw2 {      virtual      void DrawToMetaFile() = 0; }; 

Next, update your INTERFACEID enumeration to identify these additional interfaces:

// A client could ask an object for any of the following interfaces. enum INTERFACEID { IDRAW = 0, ISHAPEEDIT = 1, IDRAW2 = 2, IDRAW3 = 3};

Derive C3DRectEx directly from IDraw3. Remember—IDraw3 inherits the pure virtual methods from IDraw2 and IDraw as well. Therefore, your new rectangle class must support each and every method defined up the chain:

// C3DRectEx supports three interfaces. class C3DRectEx : public IDraw3      // Brings in layout of IDraw3, IDraw2 & IDraw. { public:      C3DRectEx();      virtual ~C3DRectEx();      // IDraw      virtual void Draw();      // IDraw2      virtual void DrawToMemory();      virtual void DrawToPrinter();      // IDraw3      virtual void DrawToMetaFile(); };

As we did for C3DRect, implement each method using simple cout statements. For example:

// Be sure each method of C3DRectEx identifies the method name and class // in the cout statement. void C3DRectEx::DrawToMemory() {      cout << "C3DRectEx::DrawToMemory" << endl; }

Go ahead and build the application once C3DRectEx is fully assembled.

Step Six: Update the Rectangles API

As we now have two types of rectangles to worry about, we need to extend our custom API to work with C3DRectEx objects as well. Add the following functionality to your current rectfunctions.h file:

  • Declare a global C3DRectEx pointer.

  • Create a global function to set this pointer to a new instance.

  • Create another global function to destroy this instance.

  • Implement a function to test this object for the IDraw, IDraw2, and IDraw3 interfaces.

The prototypes mimic what we already have for the original C3DRect object:

// Here is the global 3DRectEx. C3DRectEx* ptheRectEx; // Functions to operate on the 3DRectEx object. bool GetInterfaceFrom3DRectEx(INTERFACEID iid, void** iFacePtr); void CreateThe3DRectEx(); void DestroyThe3DRectEx(); 

The GetInterfaceFrom3DRectEx() function must be able to return interface pointers to each version of IDraw. Go ahead and implement each new API function. As much of this code will be similar to your existing functions, feel free to leverage clipboard inheritance (just be very careful of copy/paste typos!). Here is the relevant code:

// The updated rectangle API functions. void CreateThe3DRectEx() {      // Create a 3d-rect EX.      ptheRectEx = new C3DRectEx(); } void DestroyThe3DRectEx() {      // See ya.      delete ptheRectEx; } bool GetInterfaceFrom3DRectEx(INTERFACEID iid, void** iFacePtr) {      if(ptheRectEx == NULL){           cout << "You forgot to create the 3DRectEx!" << endl;           return false;      }      if(iid == IDRAW){           *iFacePtr = (IDraw*)ptheRectEx;           return true;      }      if(iid == IDRAW2){           *iFacePtr = (IDraw2*)ptheRectEx;           return true;      }      if(iid == IDRAW3){           *iFacePtr = (IDraw3*)ptheRectEx;           return true;      }      // I have no clue what they want.      *iFacePtr = NULL;           cout << "C3DRectEX does not support interface ID: " << iid << endl << endl;      return false; }

Step Seven: Update the Client

With our new interfaces, class, and API methods in place, update main() to work with your C3DRectEx object and execute the program. Here is some possible client-side code to get you going:

// Interface client version two. void main(void) { ...      // Make a 3DRectEx      CreateThe3DRectEx();      // Get interfaces from the RectEx object.      GetInterfaceFrom3DRectEx(IDRAW, (void**)&pDraw);      GetInterfaceFrom3DRectEx(IDRAW2, (void**)&pDraw2);      GetInterfaceFrom3DRectEx(IDRAW3, (void**)&pDraw3);      // Use the interfaces!      pDraw2->DrawToMemory();      pDraw2->DrawToPrinter();      pDraw3->DrawToMetaFile();      pDraw->Draw();      // Kill the rectangle.      DestroyThe3DRectEx(); ... }

The new program should run like a champ (see Figure 2-12). We can now create objects supporting multiple interfaces and manipulate them with the Rect API. When we are all done with the object's functionality, we clean up by calling the correct DestroyXXX() method.

click to expand
Figure 2-12: Interface client version two.

In the final step of this lab, we will extend the API with one final function that can work on any object supporting the IDraw interface. After this, it is on to "real COM" from here on out.

Step Eight: Interface-Based Polymorphism (or fun with pointers)

Recall that we achieve polymorphism through interface pointers. To test this behavior for yourself, add one final function to the Rect API that will draw any object supporting the IDraw interface:

// I am just waiting to draw something... void ExerciseRect(IDraw* pDraw) {      cout << endl << "Drawing some IDraw compatible object..." << endl;      pDraw->Draw();      cout << endl; }

Now, go back into your existing main loop, and send it the IDraw pointer you received from C3DRect to ExerciseRect() (before you call DestroyThe3DRect() of course):

// Get IDraw from Object. retVal = GetInterfaceFrom3DRect(IDRAW, (void**)&pDraw); // Draw it. ExerciseRect(pDraw); 

Next, send in the IDraw interface from the C3DRectEx object using the IDraw2 pointer:

// Take a valid IDraw2 pointer and cast it to an IDraw*. ExerciseRect((IDraw*)pDraw2);

Here is the final output for the Shapes.exe lab (your run may look a bit different, depending on your cout statements and the order of method invocations):

click to expand
Figure 2-13: The final client.

Here is one possible iteration of the main() function:

// Your implementation may vary! int main(void) {      bool retVal = false;      cout << "******* Interfaces *******" << endl << endl;      // Create an object to work with.      CreateThe3DRect();      IDraw* pDraw = NULL;      IDraw2* pDraw2 = NULL;      IDraw3* pDraw3 = NULL;      IShapeEdit* pShapeEdit = NULL;      // Get IDraw from Object.      retVal = GetInterfaceFrom3DRect(IDRAW, (void**)&pDraw);      if(retVal)      {           pDraw->Draw();           ExerciseRect(pDraw);      // Send in a rect.      }      // Get IShapeEdit from object.      retVal = GetInterfaceFrom3DRect(ISHAPEEDIT, (void**)&pShapeEdit);      if(retVal)      {           pShapeEdit->Fill(POLKADOT);           pShapeEdit->Inverse();           pShapeEdit->Stretch(90);      }      // Get IDraw3?      retVal = GetInterfaceFrom3DRect(IDRAW3, (void**)&pDraw3);      if(retVal)      {           pDraw3->DrawToMetaFile();      }      // Done.      DestroyThe3DRect();      // Now make a RectEx      CreateThe3DRectEx();      // Get interfaces      GetInterfaceFrom3DRectEx(IDRAW, (void**)&pDraw);      GetInterfaceFrom3DRectEx(IDRAW2, (void**)&pDraw2);      GetInterfaceFrom3DRectEx(IDRAW3, (void**)&pDraw3);      GetInterfaceFrom3DRectEx(ISHAPEEDIT, (void**)&pShapeEdit);      pDraw2->DrawToMemory();      pDraw2->DrawToPrinter();      pDraw3->DrawToMetaFile();      ExerciseRect((IDraw*)pDraw2);      DestroyThe3DRectEx();      return 0; }

This lab has shown you how to create and implement interfaces in C++. What is missing, however, is a way to allow clients to make any number of objects (currently they can only work with a single global instance of either). Also, we do not have a tight connection between the global functions and the objects on which they operate. There is a lot of redundancy in our current API, which would rapidly grow out of control if we were to add more objects. These are just some of the issues COM resolves.



 < Free Open Study > 



Developer's Workshop to COM and ATL 3.0
Developers Workshop to COM and ATL 3.0
ISBN: 1556227043
EAN: 2147483647
Year: 2000
Pages: 171

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net