Written and developed by Steve Robinson and Alex Krassel
Panther Software (http://www.panthersoft.com/)
August 8, 1997
Click here to copy the COMponent sample files associated with this technical article.
Part 1: Duplicating Interfaces
Part 2: Mixing Class Inheritance with Interfaces Inheritance
Traditional software development requires application executables to be compiled and linked with their dependencies. Every time a developer wants to use a different processing logic or new capabilities, he or she needs to modify and recompile the primary application to support them.
In manufacturing, this limitation would never be allowed. Can you imagine if you had to rebuild your car engine if you wanted to change your tires from the ones the manufacturer issued to high-performance tires? While this might appear to be a windfall for mechanics, the excessive maintenance costs would actually diminish demand for automobiles, and everyone—consumers, automobile manufacturers, mechanics, and parts manufacturers alike—would suffer. In fact, one of the keys to the success of the industrial revolution was the ability to "interchange machine parts," that is, the use of components. Today, we change components and plug new accessories into our cars without even giving it a second thought.
Cars know nothing about the tires they use. Tires have properties (wheel width, diameter, and so forth). If their properties are consistent, two (or more) tires can be interchangeable. Lighting fixtures know nothing about the light bulbs they use. If the properties of the light bulbs (such as the diameter of the base that screws into the socket) meet the requirements of the fixture manufacturer, they are interchangeable. Isn't it about time the software industry catches up with the rest of the world and builds components that require no knowledge of how they will be used? For an industry that thinks it is so far advanced, we are actually way behind.
At first glance, dynamic-link libraries (DLLs) seem to provide a solution for the problems listed above. The following fictitious story will demonstrate why this is not so.
You need to develop an application for CompanyX Gas Tanks. The application will display the level of gasoline in CompanyX's new prestigious 1,000-gallon fuel tank. First, you create an ActiveX™ gauge control that shows three data points: the tank's current level, the lowest possible safe level, and the highest possible safe level. You write a DLL called GasTankLevelGetterDLL, which exports the following functions:
Internally, GasTankLevelGetterDLL supports the device capability to read data continuously from the new 1,000-gallon CompanyX gas tank. Your application works like a charm and ships "bugless."
A couple weeks later, Ricky Rich calls you up and says your ActiveX gauge control is the prettiest thing he has ever seen in his life. Ricky says he wants to use it to monitor the level of his 5,000-gallon fish tank. He says the gauge needs to show the same three levels as your gas control. You tell him you will get back to him tomorrow while you think it over.
The next day, you conclude that you are going to name every DLL the same—LevelGetterDLL. LevelGetterDLL will export the same three functions with different internal processing. Ricky's fish tank monitoring problem is solved. He uses your application 24 hours a day to be sure his fish are safe and secure. You ship a new LevelGetterDLL to CompanyX as well. Other companies contact you about using your ActiveX gauge control. You respond by saying, "No problem. Export these three functions, name your DLL LevelGetterDLL, and you are ready to go." You had to recompile your application once to support the new LevelGetterDLL—but as long as everyone in the world names their DLL the same (LevelGetterDLL), and uses the same three immutable methods, everything will work perfectly and you will never have to recompile your application again. You go home that night feeling very smart.
The next day you open The Wall Street Journal to find that Ricky Rich has died in a helicopter accident. While en route to Rich Inc. headquarters, Ricky's helicopter ran out of gas. It seems that Ricky was an CompanyX client, and he ran two applications on his PC simultaneously. Application 1 was the fish tank monitor you developed for him using the LevelGetterDLL. Application 2 was CompanyX's fuel tank monitoring application that also used a version of LevelGetterDLL, the one shipped with Ricky's helicopter. Although Ricky ran both applications, Application 2, CompanyX's fuel tank monitoring application, used the fish tank's LevelGetterDLL and reported the levels in the fish tank instead of CompanyX's 1,000-gallon gas tank, because the version for the fish tank was copied to the computer last. Ricky never knew that his helicopter was about to run out of gas. The Rich estate sues CompanyX who, in turn, sues you. The other companies you advised decide to sue you as well. Had you used COM, Ricky Rich would be alive today, and you would not be in the courthouse.
Rule If two or more DLLs export the same functions (immutability), you can link either. However, a single application cannot use both DLLs, nor can they both reside on the same computer. The Component Object Model (COM) solves this problem. Two COM servers with identical interfaces (and therefore methods) can be used by two different applications and can reside on the same computer, because they have different class IDs (CLSIDs) and therefore are different binaries. Further, the two COM servers are technically interchangeable.
The inability to interchange parts (components) has been tolerated in software development because of the youth of our industry. Now, however, COM provides that ability for software components. By understanding CLSIDs and interface immutability, one can write a complete plug-in without any knowledge of the client. This means Application1 can run using either Plug-In1 or Plug-In2. Better yet, Application1 can dynamically switch between Plug-In1 and Plug-In2. Designing applications to use dynamically interchangeable plug-ins will do for software development what machine parts did for the industrial revolution.
With all the excitement over the ActiveX Template Library (ATL) and Distributed COM (DCOM), we tend to forget the primary reason COM was developed. DCOM's ability to leverage remote procedure calls (RPC) and marshal data is a very popular perk (and may be the reason why COM has grown in popularity during the past year), but it is not the main reason COM was developed. COM was developed so that software manufacturers could plug new accessories into existing applications without requiring a rebuild of the existing application. COM components should be designed as interchangeable plug-ins whether the COM component is a local in-process DLL or a remote server executable.
This article will demonstrate COM plug-in components that allow reuse in ways analogous to automobile tires. Using COM allows software product lines to be developed in significantly less time. Knowing how to create COM objects and interfaces is the key to building interchangeable plug-ins.
Through the course of this article, we will develop a series of interchangeable COM plug-ins:
To build the samples, you will need Microsoft Visual C++® 5.0. While you do not need 10 years of experience developing for the Microsoft Windows® platform and with the C language, you do need to have some experience with Visual C++, MFC, inheritance, and polymorphism. The samples will build and run under either Windows NT® or Windows 95. We will use the OLE/COM Object Viewer, a handy utility that ships with Visual C++ 5.0 and Microsoft Visual Basic® 5.0.
In the Ricky Rich story, we saw how the fish tank DLL and the gas tank DLL could not reside on the same computer, because neither the client application nor the two DLLs were COM components. Whichever DLL was copied to the computer last would be the one utilized by the client application. As we have seen, using the incorrect DLL could to lead to disastrous results. We suggested that the software developer could have used COM and had both DLLs on the machine. Because the two DLLs would be distinguishable by their CLSIDs, they could be used within one single application. With COM, both DLLs would expose identical methods, through interfaces, and would be interchangeable.
To prove this point, we are going to create a single GUI application that uses and displays information taken from two different COM servers, GasTankLevelGetter.dll and GasTankLevelGetter.dll. We will also create one application that will get information from each COM DLL and display the data. Requests to each COM DLL will be toggled intermittently by a four-second timer. To emphasize immutable interfaces and that COM is a binary standard, we are going to write the GUI application and the FishTankLevelGetter.dll entirely based on information in the GasTankLevelGetter.dll. However, we are not going to give you the source code to the GasTankLevelGetter.dll. If you download the sample code, you will find the GasTankLevelGetter.dll in the binaries folder. We are not even going to tell you whether GasTankLevelGetter was built with Delphi, Visual C++, Java™, COBOL, Turbo Pascal, or Visual Basic. You will, however, have to register the GasTankLevelGetter.dll with RegSvr32.
Once you have registered GasTankLevelGetter.dll with RegSvr32, you are ready to start with the OLE/COM Object Viewer. You can launch the OLE/COM Object Viewer from the Visual C++ 5.0 program group located in the Programs menu of the Windows Start button.
Once you have started the OLE/COM Object Viewer, click Expert Mode on the View menu to view Type Libraries. Scroll down the tree and open the folder titled Type Libraries. Scroll down this folder until you find GasTankLevelGetter 1.0 TypeLibrary (Ver 1.0). When this item is selected (single click), the type library ID and the path to the server will be displayed, as seen in Figure 1.
Figure 1. Viewing type library information in the OLE/COM Object Viewer
Double-clicking the GasTankLevelGetter entry brings up a window showing the complete type library. This information is generated from registry entries that were created when you registered the DLL. Type library information is kept under HKEY_CLASSES_ROOT\TypeLib.
Figure 2. Viewing a complete type library in the OLE/COM Object Viewer
The coclass statement provides a list of the supported interfaces for a component object. An object can have any number of interfaces listed in its body, specifying the full set of interfaces that the object implements, both incoming and outgoing. A quick look at the coclass entry shows the COM object's CLSID (8A544DC6-F531-11D0-A980-0020182A7050) and interface (ILevelGetter):
[
uuid(8A544DC6-F531-11D0-A980-0020182A7050),
helpstring("LevelGetter Class")
]
coclass LevelGetter {
[default] interface ILevelGetter;
};
By extracting the interface information just below the coclass, we can determine:
[
odl,
uuid(8A544DC5-F531-11D0-A980-0020182A7050),
helpstring("ILevelGetter Interface")
]
interface ILevelGetter : IUnknown {
HRESULT _stdcall GetLowestPossibleSafeLevel([out, retval] long*
plLowestSafeLevel);
HRESULT _stdcall GetHighestPossibleSafeLevel([out, retval]
long* plHighestSafeLevel);
HRESULT _stdcall GetCurrentLevel([out, retval] long*
plCurrentLevel);
HRESULT _stdcall GetTextMessage([out, retval] BSTR*
ppbstrMessage);
};
A more detailed look at the items in the type library verifies the methods and our interface IDs (Figure 3).
Figure 3. Viewing the interface ID and methods of ILevelGetter
Now that we know how to construct the ILevelGetter interface, let's create our own COM component with this information. If you desire to work with the existing sample, all source code is located in the LevelViewer folder. Start Visual C++ 5.0 and create a new project. Specify ATL Com AppWizard as the project type and "FishTankLevelGetter" as the project name. We suggest you create a new project folder. The Projects tab of the New dialog box should look like Figure 4.
Figure 4. The Projects tab of the New dialog box
In Step 1 of 1 in the AppWizard (Figure 5), choose Dynamic Link Library (DLL) as the Server Type. Check both Allow merging of proxy/stub code and Support MFC.
Figure 5. The ATL COM AppWizard
Once you have created the new FishTankLevelGetter project, choose New Class from the Insert menu to create a new ATL class. You can specify anything you desire as the class name but be sure the interface is named ILevelGetter and the interface type is Custom, which implies that ILevelGetter will be derived from IUnknown. Had ILevelGetter in the GasTankLevelGetter.dll been derived from IDispatch, we would have selected interface type Dual, which would cause the new interface to be derived from IDispatch. If your New Class dialog box looks like Figure 6, click OK to create the new class.
Figure 6. Creating your new ATL class
The next step is to edit the FishTankLevelGetter.idl file. In the IDL file, you should have the new ILevelGetter interface derived from IUnknown. If you are working with the samples, you will see the following code, which contains the four identical immutable methods of ILevelGetter that appeared in the GasTankLevelGetter ILevelGetter interface.
[
object,
uuid(7F0DFAA2-F56D-11D0-A980-0020182A7050),
helpstring("ILevelGetter Interface"),
pointer_default(unique)
]
interface ILevelGetter : IUnknown
{
HRESULT GetLowestPossibleSafeLevel([out, retval] long* plLowestSafeLevel);
HRESULT GetHighestPossibleSafeLevel([out, retval] long* plHighestSafeLevel);
HRESULT GetCurrentLevel([out, retval] long* plCurrentLevel);
HRESULT GetTextMessage([out, retval] BSTR* ppbstrMessage);
};
If you are writing the code as we go along, you will want to add the code above so that your interface implements the four identical immutable methods. The easy way to add the code is to copy and paste directly from the ITypeLib viewer. Your code should look the same as the sample, with the exception of the interface ID (uuid entry).
Open LevelGetter.h and declare the four methods in the class. Your class declaration should appear like the code that follows, which adds the four new methods:
class LevelGetter :
public ILevelGetter,
public CComObjectRoot,
public CComCoClass<LevelGetter,&CLSID_LevelGetter>
{
public:
LevelGetter(){}
BEGIN_COM_MAP(LevelGetter)
COM_INTERFACE_ENTRY(ILevelGetter)
END_COM_MAP()
//DECLARE_NOT_AGGREGATABLE(LevelGetter)
// Remove the comment from the line above if you don't want your object to
// support aggregation.
DECLARE_REGISTRY_RESOURCEID(IDR_LevelGetter)
// ILevelGetter
public: //THE FOUR NEW METHODS
STDMETHOD (GetLowestPossibleSafeLevel) (long* plLowestSafeLevel);
STDMETHOD (GetHighestPossibleSafeLevel) (long* plHighestSafeLevel);
STDMETHOD (GetCurrentLevel) (long* plCurrentLevel);
STDMETHOD (GetTextMessage) (BSTR* ppbstrMessage);
};
You now need to implement the four methods. For demonstration purposes, let's keep the methods simple. Implement them any way you desire, or copy the following code from the samples.
//---------------------------------------------------------
STDMETHODIMP LevelGetter::GetLowestPossibleSafeLevel(long* plLowestSafeLevel)
{
*plLowestSafeLevel = 70;
return S_OK;
}
//---------------------------------------------------------
STDMETHODIMP LevelGetter::GetHighestPossibleSafeLevel(long* plHighestSafeLevel)
{
*plHighestSafeLevel = 98;
return S_OK;
}
//---------------------------------------------------------
STDMETHODIMP LevelGetter::GetCurrentLevel(long* plCurrentLevel)
{
*plCurrentLevel = 94;
return S_OK;
}
//---------------------------------------------------------
STDMETHODIMP LevelGetter::GetTextMessage(BSTR* ppbstrMessage)
{
*ppbstrMessage = ::SysAllocString(L"All clear, water level is fine");
return S_OK;
}
When you have implemented the methods, compile and link your COM DLL. Then we can begin to create a client application.
We are going to create a client application that simultaneously supports the two COM objects—GasTankLevelGetter and FishTankLevelGetter. Using AppWizard, create an MFC dialog-based application that supports both Automation and ActiveX controls (check box options during the AppWizard process).
Once you have created your application, edit your main dialog box in the resource editor, so it resembles Figure7.
Figure 7. The main dialog box for your application
Note You may want to review the control IDs in the sample code, because we are going to change the values of the entries.
The next step is to add message handlers for the two new buttons, Gas Tank Level and Fish Tank Level. In the sample code, these methods are called OnGas and OnFish, respectively.
Once you have created the dialog class and added the message handlers for the buttons, you need to open the class and add a few additional members and methods. The first thing we do is forward declare the ILevelGetter interface so that we can have a class member of this interface type. The second step is to add the two additional class methods, ClearMembers and SetNewData, and class members m_pILevelGetter and m_sLastCalled methods. Then, use Class Wizard to add the OnDestroy and OnTimer methods. Once this is complete, your class declaration should be similar to the following class declaration shipped as sample code:
//forward declaration so for our class member
interface ILevelGetter;
class CLevelViewerDlg : public CDialog
{
DECLARE_DYNAMIC(CLevelViewerDlg);
friend class CLevelViewerDlgAutoProxy;
public:
CLevelViewerDlg(CWnd* pParent = NULL); // standard constructor
virtual ~CLevelViewerDlg();
//{{AFX_DATA(CLevelViewerDlg)
enum { IDD = IDD_LEVELVIEWER_DIALOG };
//}}AFX_DATA
//{{AFX_VIRTUAL(CLevelViewerDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
CLevelViewerDlgAutoProxy* m_pAutoProxy;
HICON m_hIcon;
BOOL CanExit();
//added by manually typing these into the class
void ClearMembers();
void SetNewData(const CLSID& clsid, const IID& iid);
ILevelGetter* m_pILevelGetter;
CString m_sLastCalled;
// Generated message map functions
//{{AFX_MSG(CLevelViewerDlg)
virtual BOOL OnInitDialog();
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnClose();
virtual void OnOK();
virtual void OnCancel();
//added by the Class Wizard
afx_msg void OnFish();
afx_msg void OnGas();
afx_msg void OnDestroy();
afx_msg void OnTimer(UINT nIDEvent);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
The next step is to modify the implementation file. In the class constructor, initialize the member variables so the constructor appears as follows:
CLevelViewerDlg::CLevelViewerDlg(CWnd* pParent /*=NULL*/)
: CDialog(CLevelViewerDlg::IDD, pParent)
{
//{{AFX_DATA_INIT(CLevelViewerDlg)
//}}AFX_DATA_INIT
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
m_pAutoProxy = NULL;
m_pILevelGetter = NULL;
m_sLastCalled = _T("CheckedGas");
}
Implement the ClearMembers method as noted below. This function clears the dialog-box controls. (Note that we could have used dialog data exchange with class members.)
void CLevelViewerDlg::ClearMembers()
{
CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_LOWEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_CURRENT);
if(pWnd != NULL)
pWnd->SetWindowText("");
pWnd = GetDlgItem(IDC_MESSAGE);
if(pWnd != NULL)
pWnd->SetWindowText("");
}
OnDestroy, which follows, is used to clean up when the dialog box is destroyed:
void CLevelViewerDlg::OnDestroy()
{
CDialog::OnDestroy();
KillTimer(1);
}
The class uses OnTimer to call the two button methods, OnFish and OnGas, so that data is updated without the user having to click a push button.
void CLevelViewerDlg::OnTimer(UINT nIDEvent)
{
if(m_sLastCalled == _T("CheckedFish"))
OnGas();
else
OnFish();
}
Note In the real world, it would be more appropriate to use push technology and the IConnectionPoint interface.
The virtual function OnInitDialog is used primarily for starting the timer, although it also retrieves data from the GasTankLevelGetter.dll.
//--------------------------------------------------------------------
BOOL CLevelViewerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
OnGas(); //obtain data
SetTimer(1, 4000, NULL); //set timer for 4 seconds
return TRUE; // return TRUE unless you set the focus to a control
}
Now we are ready to implement our two button methods, OnFish and OnGas, which are called intermittently every four seconds. Both of these functions are identical in procedure; they pass a CLSID and interface ID (IID) to SetNewData. The only difference is that the CLSID and IID passed by OnGas are for GasTankLevelGetter, and the CLSID and IID passed by OnFish are for FishTankLevelGetter.
OnGas retrieves the CLSID based on the string globally unique identifier (GUID) that appeared with the coclass in the TypeLib information. The IID is retrieved in a similar manner based on the interface entry in the TypeLib information also shown in the OLE/COM Object Viewer. Once the GUIDs are retrieved, SetNewData is called.
void CLevelViewerDlg::OnGas()
{
m_sLastCalled = _T("CheckedGas");
CLSID clsid;
IID iid;
HRESULT hRes;
hRes = AfxGetClassIDFromString(
"{8A544DC6-F531-11D0-A980-0020182A7050}",
&clsid);
if(SUCCEEDED(hRes))
{
hRes = AfxGetClassIDFromString(
"{8A544DC5-F531-11D0-A980-0020182A7050}", &iid);
if(SUCCEEDED(hRes))
SetNewData(clsid, iid);
}
}
The following code shows the SetNewData method, which creates an instance of either the GasTankLevelGetter COM object or the FishTankLevelGetter COM object, depending on the CLSID used. Once the COM object is instantiated, SetNewData calls methods in the ILevelGetter interface to obtain data.
void CLevelViewerDlg::SetNewData(const CLSID& clsid, const IID& iid)
{
ClearMembers();
ASSERT(m_pILevelGetter == NULL);
HRESULT hRes = CoCreateInstance(clsid, NULL, CLSCTX_ALL,
iid, (void**)&m_pILevelGetter);
if(!SUCCEEDED(hRes))
{
m_pILevelGetter = NULL;
return;
}
long lLowestSafeLevel, lHighestSafeLevel, lCurrentLevel;
BSTR bstrMessage = NULL;
m_pILevelGetter->GetLowestPossibleSafeLevel(&lLowestSafeLevel);
m_pILevelGetter->GetHighestPossibleSafeLevel(&lHighestSafeLevel);
m_pILevelGetter->GetCurrentLevel(&lCurrentLevel);
m_pILevelGetter->GetTextMessage(&bstrMessage);
m_pILevelGetter->Release();
m_pILevelGetter = NULL;
CString sLowest, sHighest, sCurrent, sMessage;
sLowest.Format("%d",lLowestSafeLevel);
sHighest.Format("%d",lHighestSafeLevel);
sCurrent.Format("%d",lCurrentLevel);
sMessage = bstrMessage;
::SysFreeString(bstrMessage);
CString sItem;
if(m_sLastCalled == _T("CheckedFish"))
{
//We are checking the fish tank now.
sItem = _T("Fish Tank");
}
else //m_sLastCalled == _T("CheckedGas")
{
//We are checking the fish tank now.
sItem = _T("Gas Tank");
}
CWnd* pWnd = GetDlgItem(IDC_TANK_TYPE);
if(pWnd != NULL)
pWnd->SetWindowText(sItem);
pWnd = GetDlgItem(IDC_LOWEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText(sLowest);
pWnd = GetDlgItem(IDC_HIGHEST_SAFE);
if(pWnd != NULL)
pWnd->SetWindowText(sHighest);
pWnd = GetDlgItem(IDC_CURRENT);
if(pWnd != NULL)
pWnd->SetWindowText(sCurrent);
pWnd = GetDlgItem(IDC_MESSAGE);
if(pWnd != NULL)
pWnd->SetWindowText(sMessage);
}
Because the interfaces are the same, we are assured that the methods will work with both COM objects. The final two steps are to implement OnFish and to include an interface definition.
void CLevelViewerDlg::OnFish()
{
m_sLastCalled = _T("CheckedFish");
CLSID clsid;
IID iid;
HRESULT hRes = AfxGetClassIDFromString(
"{7F0DFAA3-F56D-11D0-A980-0020182A7050}", &clsid);
if(SUCCEEDED(hRes))
hRes = AfxGetClassIDFromString(
"{7F0DFAA2-F56D-11D0-A980-0020182A7050}", &iid);
if(SUCCEEDED(hRes))
SetNewData(clsid, iid);
}
The interface definition, made up of pure virtual members, is included at the top of our implementation file (although it can also be in the class declaration file or a separate .h file) so that our class member m_pILevelGetter of type ILevelGetter* knows its methods. The interface declaration follows:
interface ILevelGetter : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE GetLowestPossibleSafeLevel(long*
plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetHighestPossibleSafeLevel(long*
plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCurrentLevel(long* plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE GetTextMessage(BSTR* pbstrMessage) = 0;
};
We are now ready to compile, link, and run the application. When you run the application, you have the option of clicking the buttons to swap the COM components, or allowing the timer to change them every four seconds. You are now dynamically interchanging components. Now Ricky Rich can fly his helicopter and monitor his fish tank without worries.
In Part 1 of the article, we showed the importance of interface immutability and demonstrated how a developer can build applications that can easily interchange components once an interface is developed. What if the interface of the existing COM server had hundreds of methods? In the sample in Part 1, we made it easy to clone the ILevelGetter interface because it had only four methods. Using the OLE/COM Object Viewer, take a quick look at some of the other type libraries on your computer. As you can see, many components have interfaces with a significant number of methods. Cloning an interface that implemented hundreds of methods would be quite a burden if you needed to override only a few methods.
The rules of COM specify that if you derive an interface from an existing interface, you need to implement all its methods, because interface declarations contain pure virtual functions. The same rule that guarantees interchangeable "machine parts" might also create laborious, unnecessary implementation burdens for developers!
But what if you could inherit interfaces without having to reimplement all the methods? What if you could create a component, inherit interfaces and functionality, and override functionality as you saw fit? Today, it is impossible to do this with COM objects developed outside your organization. However, if the developers within your enterprise use a programming language that supports inheritance and polymorphism, such as Visual C++, you can do exactly this. In fact, MFC makes it extremely easy to do, as we will demonstrate.
At the root of MFC is CCmdTarget. CCmdTarget is not only the base class for the message-map architecture, but it also contains dispatch maps that expose interfaces such as IDispatch and IUnknown. Every direct descendant of CCmdTarget created with Class Wizard contains these interfaces and its own CLSID. CCmdTarget is a key framework class and a base class for MFC classes used every day, such as CView, CWinApp, CDocument, CWnd, and CFrameWnd. Accordingly, every class derived from CCmdTarget can implement its own CLSID and interfaces.
The sample code we are going to review will show inheritance of interfaces by deriving new C++ classes from classes derived from a CCmdTarget. In our base class, we will implement an interface with methods that call virtual member functions in the C++ class. Our derived class will override some of the selected virtual functions. Most importantly, instead of implementing the derived class in the same DLL, we will create a distinct DLL with its own CLSID. The effective result will be inheritance of interface implementation from one binary to another without reimplementing the original interface.
Let's start by reviewing the code in project BaseLevelGetterDLL. BaseLevelGetterDLL is a typical MFC DLL. It was created with AppWizard as a "regular DLL using the shared MFC DLL." It also supports Automation. Once the AppWizard process was complete, the file BaseLevelGetterExport.h was created, and BASE_LEVEL_GETTER_DLL was included as a preprocessor definition in the C++ tab of the Project Settings dialog box (click on the Project tab and then click Project). The BaseLevelGetterExport.h code follows and the Project Settings dialog box is shown in Figure 8.
//BaseLevelGetterExport.h
#ifndef BASE_LEVEL_GETTER_EXPORT_DLL_H
#define BASE_LEVEL_GETTER_EXPORT_DLL_H
#if defined(BASE_LEVEL_GETTER_DLL)
#define BASE_LEVEL_GETTER_EXPORT __declspec(dllexport)
#else
#define BASE_LEVEL_GETTER_EXPORT __declspec(dllimport)
#endif
#endif //BASE_LEVEL_GETTER_EXPORT_DLL_H
Figure 8. The Project Settings dialog box
With BASE_LEVEL_GETTER_DLL defined, we can create classes and export them from our DLL.
The next step is to create the C++ class that will contain our interface. Using Class Wizard, we will create a class derived from CCmdTarget with just a few mouse clicks. By selecting Createable by type ID in Class Wizard's New Class dialog, our new class will be created with the IMPLEMENT_OLECREATE macro, giving the class its own CLSID and IDispatch interface.
Figure 9. Creating a class derived from CCmdTarget
Glancing at BaseLevelGetter.cpp, we see the CLSID:
//Here is our CLSID
// {C20EA055-F61C-11D0-A25F-000000000000}
IMPLEMENT_OLECREATE(BaseLevelGetter, "BaseLevelGetterDLL.BaseLevelGetter", 0xc20ea055, 0xf61c, 0x11d0, 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0)
and an interface called IBaseLevelGetter, which is of type IDispatch:
// {C20EA054-F61C-11D0-A25F-000000000000}
static const IID IID_IBaseLevelGetter =
{ 0xc20ea054, 0xf61c, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } };
BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget)
INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch)
END_INTERFACE_MAP()
Rather than working with the default interface provided by Class Wizard, we are going to add our own custom interface to demonstrate how easy it is to add interfaces to CCmdTarget-derived classes. The first thing we need to do is to define our interface. Defining an interface is always the same. Every interface must have an IID and have IUnknown as a base interface somewhere in its hierarchy. And it needs to implement the three methods of IUnknown. In ILevelGetter.h, we used guidgen.exe (located in \Program Files\DevStudio\VC\Bin) to generate a unique IID for our interface and the derived interface from IUnknown. In addition to the three pure virtual functions of IUnknown, we added four more pure virtual functions that will be implemented in our COM object. Below is the exact code from ILevelGetter.h.
#ifndef ILEVELGETTER_H
#define ILEVELGETTER_H
// {BCB53641-F630-11d0-A25F-000000000000}
static const IID IID_ILevelGetter =
{ 0xbcb53641, 0xf630, 0x11d0, { 0xa2, 0x5f, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 } };
interface ILevelGetter : public IUnknown
{
//first add the three always required methods
virtual HRESULT STDMETHODCALLTYPE
QueryInterface(REFIID riid, LPVOID* ppvObj) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
virtual ULONG STDMETHODCALLTYPE Release() = 0;
//now add methods for this custom interface
virtual HRESULT STDMETHODCALLTYPE
GetCurrentLevel(long* plCurrentLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetHighestPossibleSafeLevel(long* plHighestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetLowestPossibleSafeLevel(long* plLowestSafeLevel) = 0;
virtual HRESULT STDMETHODCALLTYPE
GetTextMessage(BSTR* ppbstrMessage) = 0;
};
The next step is to declare our interface methods in BaseLevelGetter.h. At the top of BaseLevelGetter.h, add an include directive for our interface declaration as follows:
#include "ILevelGetter.h"
Once we include ILevelGetter.h, we can add our interface methods using the BEGIN_INTERFACE_PART macro. In summary, the BEGIN_INTERFACE_MACRO creates a nested class of type XLevelGetter and a class member m_xLevelGetter in BaseLevelGetter. Each method in the interface is declared in the macro in the same manner as if there were no macro wrapper. A quick review shows that the method declarations in ILevelGetter.h are exactly the same as the method declarations declared in the ATL version.
BEGIN_INTERFACE_PART(LevelGetter, ILevelGetter)
STDMETHOD(GetCurrentLevel) (long* plCurrentLevel);
STDMETHOD(GetHighestPossibleSafeLevel) (long* plHighestSafeLevel);
STDMETHOD(GetLowestPossibleSafeLevel) (long* plLowestSafeLevel);
STDMETHOD(GetTextMessage) (BSTR* ppbstrMessage);
END_INTERFACE_PART(LevelGetter)
Because our goal is to inherit interfaces from one binary to another without having to reimplement all the methods, we are going to add four virtual functions to our class. Each virtual function will correspond to a method in the ILevelGetter interface. In the sample code, these methods are defined at the bottom of the class declaration, immediately after the BEGIN_INTERFACE_PART macro, as follows.
//since the class can be dynamically created
//these virtual functions cannot be pure
virtual long GetCurrentLevel();
virtual long GetHighestSafeLevel();
virtual long GetLowestSafeLevel();
virtual CString GetMessage();
One thing to note is that because our CCmdTarget-derived class uses DECLARE_DYNCREATE, these functions cannot be pure virtual functions.
The last task is to declare the class as "exportable." To do this, we need only to include our export definition in the class definition. The lines appear as:
#include "BaseLevelGetterExport.h"
class BASE_LEVEL_GETTER_EXPORT BaseLevelGetter : public CCmdTarget
{
Implementation of our interface is equally easy. The first thing we need to do is to add support for the new ILevelGetter interface. The general rule is to add an INTERFACE_PART macro between BEGIN_INTERFACE_PART and END_INTERFACE_PART for every interface supported. In BaseLevelGetter.cpp, this is accomplished by adding the following line:
INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter)
So that the entire INTERFACE_PART appears as:
BEGIN_INTERFACE_MAP(BaseLevelGetter, CCmdTarget)
INTERFACE_PART(BaseLevelGetter, IID_IBaseLevelGetter, Dispatch)
INTERFACE_PART(BaseLevelGetter, IID_ILevelGetter, LevelGetter)
END_INTERFACE_MAP()
The next step is to implement the ILevelGetter methods. The first three methods that should be implemented are from IUnknown: QueryInterface, AddRef, and Release. These methods are shown below:
//------------------------------------------------------------------------
HRESULT FAR EXPORT BaseLevelGetter::XLevelGetter::QueryInterface
(
REFIID iid,
LPVOID* ppvObj
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (HRESULT) pThis->ExternalQueryInterface(&iid, ppvObj);
}
//-------------------------------------------------------------------------
ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::AddRef()
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (ULONG) pThis->ExternalAddRef();
}
//-------------------------------------------------------------------------
ULONG FAR EXPORT BaseLevelGetter::XLevelGetter::Release()
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
return (ULONG) pThis->ExternalRelease();
}
The four ILevelGetter methods are implemented in a very similar fashion. Rather than performing actual processing, each method calls its associated function through the pointer pThis. This actually requires some additional explanation. If you look at the definition of the BEGIN_INTERFACE_PART(. . .) macro (file located at …\MFC\include\afxdisp.h), you will notice that this macro is a nested class declaration. The macro forces the nested class (in our case, XLevelGetter) to be derived from the interface (ILevelGetter, in our example) and to be declared within the existing class (BaseLevelGetter).
The END_INTERFACE_PART(. . .) macro completes the "inner" XLevelGetter class definition and declares a member variable of that class called m_xLevelGetter. Because m_xLevelGetter is a member of the BaseLevelGetter class, we could implement some complicated pointer arithmetic to get from X of the XLevelGetter object to Y of the containing BaseLevelGetter object. However, MFC provides another macro that does just that. This macro is called METHOD_PROLOGUE_EX_, and in our particular case it will generate the variable BaseLevelGetter* pThis. You can use pThis to access public members and methods of the "outer" BaseLevelGetter class, including virtual (polymorphic) functions, as we do in the samples. Calling virtual functions in the "outer" class will, in effect, cause interface inheritance. Notice that the BaseLevelGetter virtual functions return meaningless values and have comments to remind developers creating derived classes to override these functions.
Another way to show the virtual relationship, and perhaps much easier to read, is to "set an owner object" in the XLevelGetter class (the class created by the BEGIN_INTERFACE_PART macro). Inside the BaseLevelGetter.h BEGIN_INTERFACE_PART macro, we add two functions and a class member as follows:
XLevelGetter() { m_pOwner = NULL; } //constructor sets member to NULL
void SetOwner( BaseLevelGetter* pOwner ) { m_pOwner = pOwner; } //set the member
BaseLevelGetter* m_pOwner; //class member
Inside the BaseLevelGetter constructor, we call XLevelGetter::SetOwner. As noted earlier, the BEGIN_INTERFACE_PART macro adds a class member to BaseLevelGetter, called m_xLevelGetter, that represents the LevelGetter. In the BaseLevelGetter constructor we call:
m_xLevelGetter.SetOwner( this );
which sets m_pOnwer to a valid object.
Below is the implementation of the four ILevelGetter methods and the four BaseLevelGetter associated virtual functions. The latter two methods, GetLowestPossibleSafeLevel and GetTextMessage use the "owner object" implementation style.
//----------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetCurrentLevel
(
long* plCurrentLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetCurrentLevel
//whether this class or a derived class
*plCurrentLevel = pThis->GetCurrentLevel();
return S_OK;
}
//-----------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetHighestPossibleSafeLevel
(
long* plHighestSafeLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetHighestSafeLevel
//whether this class or a derived class
*plHighestSafeLevel = pThis->GetHighestSafeLevel();
return S_OK;
}
//-----------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetLowestPossibleSafeLevel
(
long* plLowestSafeLevel
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetLowestSafeLevel
//whether this class or a derived class
if( m_pOnwer != NULL)
{
*plLowestSafeLevel = m_pOwner->GetHighestSafeLevel();
}
else
{
ASSERT(FALSE);
}
return S_OK;
}
//-----------------------------------------------------------
STDMETHODIMP BaseLevelGetter::XLevelGetter::GetTextMessage
(
BSTR* ppbstrMessage
)
{
METHOD_PROLOGUE_EX_(BaseLevelGetter, LevelGetter)
//call outer object's GetMessage
//whether this class or a derived class
CString sMessage;
If( m_pOwner != NULL )
{
sMessage = m_pOwner->GetMessage();
}
else
{
ASSERT(FALSE);
}
*ppbstrMessage = sMessage.AllocSysString();
return S_OK;
}
//-----------------------------------------------------------
long BaseLevelGetter::GetCurrentLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//-----------------------------------------------------------
long BaseLevelGetter::GetHighestSafeLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//-----------------------------------------------------------
long BaseLevelGetter::GetLowestSafeLevel()
{
TRACE("Derived classes should override!");
return -1;
}
//-----------------------------------------------------------
CString BaseLevelGetter::GetMessage()
{
TRACE("Derived classes should override!");
return "BaseLevelGetter";
}
Compile and link the application. Once the DLL is built, copy it to the Windows\System directory (\WINNT\System32 on Windows NT).
Important Because we will be using the ILevelGetter interface from BaseLevelGetter, remember to register it with RegSvr32 once it is copied to the appropriate system directory. If we were using BaseLevelGetter as an abstract base class (for example, if the BaseLevelGetter virtual functions had to be overridden) and their implementation perhaps threw assertion errors, there would be no need to register the COM object with RegSvr32.
To build a COM object that implements the ILevelGetter interface but does not have to re-implement all the methods, we create a COM DLL in the same manner as we created BaseLevelGetterDLL: We create an MFC AppWizard DLL that supports automation, and we add a class derived from CCmdTarget. The samples contain a project called HotTubLevelGetterDLL with a CmdTarget-derived class, HotTubLevelGetter, which was created in the Class Wizard New Class dialog box, as shown in Figure 10.
Figure 10. Creating the HotTubLevelGetter class
The next step is to add BaseLevelGetterDLL to the include path by setting it as an Additional include directory in the C/C++ tab of the Project Settings dialog box, as shown in Figure 11.
Figure 11.Adding BaseLevelGetterDLL to the include path
And link BaseLevelGetterDLL.lib by adding it as a Library Module in the Link tab of the Project Settings dialog box, as shown in Figure 12.
Figure 12. Linking BaseLevelGetterDLL.lib
Once our project settings are complete, we follow five simple steps to complete the plug-in COM DLL.
#include <BaseLevelGetter.h>
class HotTubLevelGetter : public BaseLevelGetter
{
virtual CString GetMessage( ) { return "HotTubLevelGetter"; }
virtual long GetCurrentLevel( ) { return -2; }
Before we demonstrate the COM plug-in in a client, let's take a look at what we have built. Classes BaseLevelGetter and HotTubLevelGetter both derive from CCmdTarget. When we created HotTubLevelGetter, we told Class Wizard to derive it from CCmdTarget. Recall that every class created with Class Wizard as a direct descendant of CCmdTarget is supplied with its own CLSID and IDispatch interface. When we changed the base class of HotTubLevelGetter from CCmdTarget to BaseLevelGetter, HotTubLevelGetter inherited the BaseLevelGetter virtual methods.
When a client needs to access HotTubLevelGetter, it does the typical CoCreateInstance(. . .)—passing the CLSID of HotTubLevelGetter and IID_ILevelGetter, and calling the LevelGetter methods. When a method is executed such as GetCurrentLevel, METHOD_PROLOGUE_EX_ sets pThis from an offset table, and pThis actually points to an instance of HotTubLevelGetter. The same thing occurs when we use m_pOwner (it also points to an instance of HotTubLevelGetter); but it is a little easier to understand, because we can watch the m_xLevelGetter.SetOwner(this) method execute. Let's look at the client and set some break points.
In the sample code, open LevelViewer in the LevelViewer2 folder. This project is almost identical to the first LevelViewer. OnFish is mapped to BaseLevelGetter, and OnGas is mapped to HotTubLevelGetter, as shown in the following code.
//-----------------------------------------------------------
void CLevelViewerDlg::OnFish() //mapped to BaseLevelGetter
{
m_sLastCalled = _T("CheckedFish");
CLSID clsid;
HRESULT hRes = AfxGetClassIDFromString("BaseLevelGetterDLL.BaseLevelGetter", &clsid);
if(SUCCEEDED(hRes))
SetNewData(clsid, IID_ILevelGetter);
}
//------------------------------------------------------------
void CLevelViewerDlg::OnGas() //mapped to HotTubLevelGetter
{
m_sLastCalled = _T("CheckedGas");
CLSID clsid;
HRESULT hRes = AfxGetClassIDFromString("HotTubLevelGetterDLL.HotTubLevelGetter", &clsid);
if(SUCCEEDED(hRes))
SetNewData(clsid, IID_ILevelGetter);
}
Both functions call SetNewData, passing the CLSID created by Class Wizard and IID_ILevelGetter declared in ILevelGetter.h and included in LevelViewerDlg.h.
Note Under the C++ tab, add \BaseLevelGetterDLL as an additional include directory.
SetNewData works in the same manner as before. Build and link the code—but before running the application, set breakpoints at any or all of the interface methods, as shown in Figure 13.
Figure 13. Setting breakpoints of interface methods
When execution stops at the breakpoint, step into the method (using either the F8 or F11 function key—depending on how your system is set up) and single step (F10 key) until you are on a line with either the pThis or the m_pOwner object. Examine the value. Depending on whether the timer has fired for HotTubLevelGetter or BaseLevelGetter, pThis (or m_pOnwer) will point to the correct object (Figure 14).
Figure 14. Stepping through a method
As you have seen, COM plug-ins are an extremely powerful software architecture technique that can be used in real situations.
For example, a health insurance company, whose business is developing and maintaining custom insurance plans for large companies, comes to you for design advice regarding a new Windows-based system. Every time they add a new plan, they have to develop new processing logic or add a slight twist to the processing logic for an existing plan, but do not have to reinvent every piece of functionality. But, as they add more plans to their system, modifying, reimplementing, or copying and pasting proven source code is impractical, because it risks integrity of proven code no matter how careful the programmers are.
As a C++ COM programmer, you recognize the need for interchangeable components that support polymorphism. Consider the following: the IBasePlan interface nested in the BasePlan class implements 100 interface methods. Plan ABC's requirements involve implementation modifications to 50 methods in the IBasePlan interface. Plan XYZ's requirements involve implementation modifications to 51 methods in the IBasePlan interface, but 50 of the implementation modifications are exactly the same as those for plan ABC. Rather than implementing complete interfaces for every COM object, you associate 100 C++ class member virtual functions in BasePlan, one for every method in interface IBasePlan , as in the previous example.
Because you have associated virtual functions in the BasePlan class, the class hierarchy for plan XYZ appears as follows:
Implements IBasePlan, 100 interface methods and 100 associated C++ class member virtual functions.
Derives from BasePlan, uses 50 C++ class member virtual functions in BasePlan and overrides 50 virtual functions of BasePlan.
Derives from ABCPlan, uses 49 C++ class member virtual functions in BasePlan, uses 50 C++ class member virtual functions in ABCPlan, and overrides 1 C++ class member virtual function of BasePlan.
Each component is created as a separate binary and COM object. Each has its own distinct CLSID and, because of the inheritance structure, implements IBasePlan. Using AppWizard and Class Wizard, you can complete the implementation of plan XYZ in minutes, without ever touching the base class COM components. All COM DLLs reside on the same computer, and if you use component categories or another similar technique in the registry, a client application will find plan XYZ as soon as it is registered with RegSvr32.
WELCOME TO OOP (object-oriented programming) NIRVANA AND THE WORLD OF COMPONENTS!
Panther Software is a software development consulting company based in Hermosa Beach, California. The company specializes in developing software for other software companies, using technologies such as DCOM, COM, and ActiveX control development on Win32 platforms, and can be reached at http://www.panthersoft.com.