Developers Workshop to COM and ATL 3.0

 < Free Open Study > 


Reading this section is completely optional, as COM development has now become easy and intuitive when using ATL. OK that's a lie. The truth is, ATL can add complexity to an already challenging architecture. To help ease our pain even further, ATL does provide a number of helpful debugging tricks, which we will now examine.

First of all, ATL does provide a macro for tracing the flow of execution in your code. When you wish to send diagnostic strings into the Visual C++ Debug window, use ATLTRACE. This macro will expand to either a call to AtlTrace() (for debug builds) or nothing (in non-debug builds). ATLTRACE is much like the MFC tracing macros, in that the calling syntax mimics the C printf() function. Assume you wish to display a string that reveals the FILLTYPE sent from a client:

// Making use of ATLTRACE. STDMETHODIMP CCoHexagon::Fill(FILLTYPE fType) { ATLTRACE("Client is using fill type number: %d\n", fType); ... return S_OK; }

When you are in a debug session, you will see this formatted string appear in your Debug window:

Figure 6-26: ATLTRACE output.

Another helpful debugging aid is the _ATL_DEBUG_QI preprocessor symbol. When this constant is defined in your project, you will be able to see the QueryInterface() calls sent to your coclasses during a debug session. To enable this functionality, you may add a #define entry in your stdafx.h file:

// stdafx,h #define STRICT #ifndef _WIN32_WINNT #define _WIN32_WINNT 0x0400 #endif #define _ATL_APARTMENT_THREADED // Enable QI debugging support #define _ATL_DEBUG_QI

In order to debug a COM DLL, you will have to select an EXE used to host the server at debug time. Assume a VB client project has been compiled and has been specified as the EXE to host ATLShapesServer.dll during a debug session (simply start a debug session for the DLL and navigate to the compiled EXE server from the resulting dialog box). Here is a possible output for CoHexagon's QI debug session:

Figure 6-27: _ATL_DEBUG_QI output.

The GUIDs you see in Figure 6-27 specify IID_IDraw and IID_IErase. If you would rather see the friendly text names appear in the QI debug session (such as IUnknown), you will need to ensure your interfaces are registered under HKCR\Interface. Recall that if your COM interfaces are all VARIANT compliant, you may specify the [oleautomation] attribute to leverage the universal marshaler.

Also recall (from Chapter 5) that when you register your type information using the COM library, any interface marked with this attribute is automatically merged into the registry. If we were to add the [oleautomation] attribute to IDraw, IShapeEdit, IErase, and IShapeID and rebuild the project (which will also reregister the server), we would find a more intuitive QI dump:

Figure 6-28: _ATL_DEBUG_QI only uses friendly names for registered interfaces.

The final bit of ATL debugging that can prove very helpful is the _ATL_DEBUG_INTER- FACES. This preprocessor flag supercedes _ATL_DEBUG_QI in that you see not only the interface requested by a client, but the current reference count to the interfaces. If we were to edit stdafx.h to use _ATL_DEBUG_INTERFACES and recompile, here is a new debugging session output:

Figure 6-29: _ATL_DEBUG_INTERFACES also displays reference counts.

The best thing about using _ATL_DEBUG_INTERFACES is that it is smart enough to detect mismanaged reference counting! Visual Basic will always ultimately call Release() on all acquired interfaces, so let's assume we have a C++ client that created CoHexagon and forgets to Release() the obtained IDraw interface. In a debug session, we would find the following listing:

INTERFACE LEAK: RefCount = 1, MaxRefCount = 1, {Allocation = 4} CCoHexagon - IDraw

So then, that wraps up our initial look at the Active Template Library. This chapter has you at a point where you can move around intelligently in the ATL environment, but there is a ton more to see. Move onto the next lab, and then to Chapter 7 to drill down deeper into the framework's support for COM classes.

Lab 6-1: ATL Car Server

This lab will allow you to work with the various tools and techniques introduced in this chapter including the ATL COM AppWizard, ATL Object Wizard, and Implement Interface Wizard. You will be creating an ATL version of the CoCar you know and love. Beyond implementing the ICreateCar, IStats, and IEngine interfaces, you will also be adding an additional interface containing COM properties.

Note 

This lab will serve as the basis of numerous other labs throughout the book. When you have completed this lab, you may wish to make a complete copy of this project to use as a starting point in the future.

 On the CD   The solution for this lab can be found on your CD-ROM under:Labs\Chapter 06\ATLCarServerLabs\Chapter 06\ATLCarServer\VB Client

Step One: Develop the Component Housing and Code Examination.

Begin your lab by accessing the ATL COM AppWizard. Create an in-process server named ATLCarServer, with no support for MFC, MTS, or merging of proxy/stub code. Once you hit the Finish button you will be presented with a confirmation dialog. Hit OK and compile your new DLL.

Figure 6-30: The initial ATL COM AppWizard exports.

Expand the Globals folder from ClassView (Figure 6-30). Remember that the AppWizard is used to generate the necessary code for COM servers, and at this point you will have no coclasses in your project. ATL implements your DLL exports by calling various methods of your project-wide instance of CComModule, _Module. Examine the source code behind DllCanUnloadNow(), DllGetClassObject(), DllMain(), DllRegisterServer(), and DllUnregisterServer(). Take the time to look up CComModule from online help and investigate the various members of this class.

As you have seen, and will see much more of in Chapter 9, CComModule uses your project's object map to get much of its work accomplished (object creation and registration). Every COM object that you wish to allow a client to activate must be listed in your object map. Currently your map is empty, as we have not yet added any ATL coclasses to the project:

// Initial COM Map. BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP()

Also recall that the ATL COM AppWizard will automatically include a DEF file into your project to export your DLL functions:

; ATLCarServer.def : Declares the module parameters. LIBRARY "ATLCarServer.DLL" EXPORTS DllCanUnloadNow @1 PRIVATE DllGetClassObject @2 PRIVATE DllRegisterServer @3 PRIVATE DllUnregisterServer @4 PRIVATE

Finally, examine your FileView subfolders. ATL projects always provide separate files for implementation code, class definitions, and project resources. MIDL-generated files are automatically placed in the External Dependencies folder. When you have finished examining the initial AppWizard generated files, move to step two.

Step Two: Insert a Simple Object

Access the ATL Object Wizard. Insert a Simple Object named ATLCoCar. Be sure to change the name of the initial interface to ICreateCar. Also be sure you select a Custom interface from the Attributes tab, leaving all other options as is.

Figure 6-31: Changing the suggested name for the [default] interface.

Once you have inserted this simple object, take a look at your class declaration. You will see that your ATL class derives from two ATL templates and your custom interface. CComObjectRootEx<> and its base class CComObjectRootBase provide helper functions used to implement the methods of IUnknown. CComCoClass<> defines a default class factory for your new coclass (via the DECLARE_CLASSFACTORY macro) as well as aggregation support (via the DECLARE_AGGREGATABLE macro).

// The initial CoCar. class ATL_NO_VTABLE CATLCoCar : public CComObjectRootEx<CComSingleThreadModel>, public CComCoClass<CATLCoCar, &CLSID_ATLCoCar>, public ICreateCar { ... };

Locate your COM map. Recall the COM map is used to provide a table driven implementation of QueryInterface(). For each interface supported in your COM class you will need to be sure to add a COM_INTERFACE_ENTRY listing, or else clients will not be able to receive vPtrs for the requested interface. The Object Wizard has already added an entry for your [default] interface, and thus your COM map should appear as so:

// Updated COM Map. BEGIN_COM_MAP(CATLCoCar) COM_INTERFACE_ENTRY(ICreateCar) END_COM_MAP()

Now examine your project's IDL file. You should see the interface definition for ICreateCar as well as support for ICreateCar in your coclass statement. You may wish to add the [oleautomation] attribute to the initial IDL code to ensure interface registration. Last but not least, check out your RGS file. Recall that ATL servers are all self registering and the script contained in a coclass's RGS file will be entered or removed from the system registry.

Step Three: Implement the ICreateCar Interface

Access the Add Method Wizard by right-clicking on the ICreateCar interface from ClassView. Add the SetPetName() method, taking a single [in] BSTR as the sole parameter. Remember to define your parameters in IDL notation:

Figure 6-32: Adding methods à la ATL.

Open the Add Method Wizard once again, and add SetMaxSpeed() to ICreateCar. When you have added these methods, your interface definition will appear as so:

// The ICreateCar interface in IDL syntax. interface ICreateCar : IUnknown { [helpstring("method SetPetName")] HRESULT SetPetName([in] BSTR petName); [helpstring("method SetMaxSpeed")] HRESULT SetMaxSpeed([in] int maxSp); };

Add a private CComBSTR to your class named m_petName, as well as a private integer named m_maxSpeed. In the constructor of your coclass, set each state variable to a default value (remember that CComBSTR objects are set to NULL, not an empty string!):

// Recall that CComBSTR does not set the underlying BSTR to an empty string! CATLCoCar() : m_maxSpeed(0) { // Set the BSTR to an empty string m_petName = ""; }

Use these two data members to implement the methods of ICreateCar. Because the Object Wizard has already added stub code to your implementation file, you can focus on the logic behind the interface. Be sure to set the car's maximum speed only if it is less than or equal to 500 MPH:

// Same as it ever was... STDMETHODIMP CATLCoCar::SetMaxSpeed(int maxSp) { // Assume MAX_SPEED is a class level const, // i.e., const int MAX_SPEED = 500; if(maxSp < MAX_SPEED) m_maxSpeed = maxSp; return S_OK; }

SetPetName can make use of the overloaded assignment operator of CComBSTR. Remember that CComBSTR is making the same system string function calls you were making by hand in previous labs:

// Using overloaded operators of CComBSTR. STDMETHODIMP CATLCoCar::SetPetName(BSTR petName) { // operator= is a good thing! m_petName = petName; return S_OK; }

Go ahead and compile the project, just to be sure you come out clean.

Step Four: Add an Interface by Hand

Wizards are good tools when you want to save yourself some typing. The problem is if you click on too many CASE tools without understanding what is being done on your behalf, you are sure to get burned (not to mention what to do if the CASE tool breaks). To avoid that pitfall, we will now add support for IStats by hand.

Begin by opening up your IDL file and define a bare bones interface definition. Use guidgen.exe to grab a new IID for IStats (be sure to select the registry format option):

// You can't escape understanding IDL when using ATL. [object, uuid(A533DA31-D372-11d2-B8CF-0020781238D4), oleautomation, helpstring("Get info about this car")] interface IStats : IUnknown { };

Next, add this new interface to your coclass definition in the project's library statement:

// Manually add support for your new interfaces. coclass ATLCoCar { [default] interface ICreateCar; interface IStats; };

Go ahead and save the IDL file. You should see IStats appear in ClassView.

Now open up your coclass's header file and derive your ATL CoCar from IStats. Next, add an entry for IStats to your COM map. This step is critical.

// The COM_INTERFACE_ENTRY macro spits out a vPtr for a given interface. BEGIN_COM_MAP(CATLCoCar) COM_INTERFACE_ENTRY(ICreateCar) COM_INTERFACE_ENTRY(IStats) END_COM_MAP()

Go ahead and save everything and recompile.

In this chapter you were told about the true "by hand" approach. After this point you would type the IDL method definitions, add prototypes to the header file (by hand), and then implement the methods in the CPP file. But here is a shortcut. Now that you have an empty IDL interface definition and a COM map entry, simply right-click on the IStats interface from ClassView and add the DisplayStats() method using the New Method Wizard (recall this method takes no parameters). When you click OK, you will see that the CASE tool has automatically added a prototype and stub code on your behalf:

// ATL Wizard starter code. STDMETHODIMP CATLCoCar::DisplayStats() { // TODO: Add your implementation code here return S_OK; }

I thought you might like that cheat. Now, finish off the IDL definition by adding the GetPetName() method:

Figure 6-33: Don't forget to specify parameter attributes!

GetPetName() will be using the Copy() method of CComBSTR rather than raw system string functions:

// Using the overloaded Copy method. STDMETHODIMP CATLCoCar::GetPetName(BSTR *petName) { *petName = m_petName.Copy(); return S_OK; }

DisplayStats() will be trimmed down a bit, as we can now make use of the ATL conversion macros. Define the USES_CONVERSION macro within the method, and transform your BSTR into a char* using W2A. Format the string into a message box and exit the function:

// Using ATL conversion macros. STDMETHODIMP CATLCoCar::DisplayStats() { // Use the ATL conversion macros. USES_CONVERSION; MessageBox(NULL, W2A(m_petName), "Pet Name", MB_OK | MB_SETFOREGROUND); char buff[MAX_NAME_LENGTH]; memset(buff, 0, sizeof(buff)); sprintf(buff, "%d", m_maxSpeed); MessageBox(NULL, buff, "Max Speed", MB_OK | MB_SETFOREGROUND); return S_OK; }

Compile the server and move on to the next step.

Step Five: Use the Implement Interface Wizard

Next, we must add support for IEngine. Begin by writing an empty IDL interface definition for IEngine. Next, add support for your new interface in the coclass definition and recompile the project. Now, right-click on CATLCoCar from ClassView, and select the Implement Interface Wizard. You will see IEngine listed from the tool, as your class does not yet support this interface. Select it and click OK.

Figure 6-34: Don't forget to recompile your type library first.

This tool will derive your class from IEngine as well as add an entry to your COM map. Now use the Add Method Wizard as usual to define the methods of IEngine. When you have finished, your IDL should appear as such:

[object, uuid(A533DA30-D372-11d2-B8CF-0020781238D4), oleautomation, helpstring("Rev your car & slow it down")] interface IEngine : IUnknown { [helpstring("method SpeedUp")] HRESULT SpeedUp(); [helpstring("method GetMaxSpeed")] HRESULT GetMaxSpeed([out, retval] int* maxSpeed); [helpstring("method GetCurSpeed")] HRESULT GetCurSpeed([out, retval] int* curSpeed); };

Finally, implement these three methods. Add a private integer to hold the current speed, and code SpeedUp(), GetMaxSpeed(), and GetCurSpeed() as usual:

// IEngine implementation. STDMETHODIMP CATLCoCar::SpeedUp() { m_currSpeed += 10; return S_OK; } STDMETHODIMP CATLCoCar::GetMaxSpeed(int *maxSpeed) { *maxSpeed = m_maxSpeed; return S_OK; } STDMETHODIMP CATLCoCar::GetCurSpeed(int *curSpeed) { *curSpeed = m_currSpeed; return S_OK; }

Compile your ATL server. You may wish to whip together a quick Visual Basic test client at this point (or simply recycle an existing VB car tester). Before you move onto the next chapter, however, we have one additional step...

Step Six: An Interface with Properties

We are going to extend the functionality of our CoCar by maintaining information about the current owner. Define an IDL interface named IOwnerInfo and use the Implement Interface Wizard to add support for this interface to ATLCoCar. Next, right-click on this new interface from ClassView and access the Add Property Wizard. Define two properties to keep track of the name and address of the car owner. Your IDL will appear as such:

// Using [propput] and [propget]. [object, uuid(530D7320-333E-11d3-B904-0020781238D4), oleautomation, helpstring("Information about the owner of this car")] interface IOwnerInfo : IUnknown { [propget, helpstring("property Name")] HRESULT Name([out, retval] BSTR *pVal); [propput, helpstring("property Name")] HRESULT Name([in] BSTR newVal); [propget, helpstring("property Address")] HRESULT Address([out, retval] BSTR *pVal); [propput, helpstring("property Address")] HRESULT Address([in] BSTR newVal); };

Add two CComBSTRs to your CoCar, and implement the four methods as necessary. Coding a property pair is identical to a traditional COM interface method. Here is the Name property to get you started:

// IDL Properties map to get_ and put_ methods. STDMETHODIMP CATLCoCar::get_Name(BSTR *pVal) { *pVal = m_ownerName.Copy(); return S_OK; } STDMETHODIMP CATLCoCar::put_Name(BSTR newVal) { m_ownerName = newVal; return S_OK; }

Again, create a simple Visual Basic (or Java or C++) tester to exercise your new interface. The point here is to understand that properties are a nicety C++ developers give to COM developers using kinder, gentler languages such as VB:

' Don't forget to include a reference to this type information. ' Private Sub Form_Load() Dim MyCar As New ATLCoCar MyCar.SetMaxSpeed 40 MyCar.SetPetName "JoJo" Dim itfOwner As IOwnerInfo Set itfOwner = MyCar itfOwner.Name = "Manu" MsgBox itfOwner.Name itfOwner.Address = "123 Happy Lane" MsgBox itfOwner.Address End Sub

There you have it! I am sure you agree that this iteration of the CoCar was substantially simpler to implement with the help of the ATL framework. In the next chapter, we will build on your current understanding of the ATL framework and investigate just how ATL provides core services to a COM class.


 < Free Open Study > 

Категории