Creating and Using COM Interfaces

   

As identified in Table 17.2, COM takes on many different forms, including ActiveX controls, Automation, ActiveForms, COM+, and DCOM just to name a few. The one common capability provided by these various COM technologies is that COM facilitates communication between components , applications, clients , and servers through clearly defined interfaces . These interfaces enable the reuse of software components and services provided by COM objects.

Therefore, the most important thing about developing and using COM-based software is to understand interfaces. As stated earlier, an interface defines a set of public methods for accessing a COM object, which is contained within a COM server. From a C++ perspective, an interface is comparable to an abstract class containing pure virtual functions.

IUnknown

All COM interfaces descend from a base class called IUnknown . The primary purpose of IUnknown is to expose an interface so that it can be utilized by other applications. IUnknown is comprised of three virtual methods.

  • QueryInterface()

  • AddRef()

  • Release()

QueryInterface() is used to query and retrieve a reference to a specified interface. The C++ declaration for QueryInterface() is defined as follows :

 virtual HRESULT QueryInterface(REFIID riid, void ** ppvObject); 

Notice that there are two parameters associated to QueryInterface() : riid and ppvObject . The riid parameter is used to identify the interface being requested . If the interface exists, QueryInterface() will assign a pointer representing the interface to the ppvObject parameter. If the object doesn't support the interface, QueryInterface() sets ppvObject to NULL and returns a nonzero error code.

AddRef() is used to increment the reference count for the interface and returns the reference count value. When the caller is finished with the interface, it should call the Release() method. Release() will decrement the reference count. If the reference count drops to zero, the object is automatically freed.

CAUTION

Each of the IUnknown methods are virtual and are redefined by the class that inherits IUnknown . Therefore, it's quite possible that a COM object has redefined AddRef() and Release() so that they do not perform the anticipated reference counting. In this case, the reference count might never drop to zero. If this occurs, the object will not be automatically freed, and it will be the responsibility of the application to then free the object.


NOTE

In addition to IUnknown , a custom interface can inherit several other interfaces, which are identified in Table 17.3

Table 17.3. COM Common Interfaces

Interface

Description

IDataBroker

Design time interface for remote data modules.

IDispatch

Interface used for providing Automation. (This is used in some of the following examples.)

IEnumVARIANT

Interface used for enumerating a collection of variant objects.

IFont

Interface to a COM font object, which is actually a wrapper around a Windows font object.

IPicture

Interface to a picture object, which is a language-neutral abstraction for bitmaps, icons, and metafiles, and its properties.

IProvider

Provider interface for TClientDataSet .

IStrings

Collection Interface for TStrings .

IUnknown

The base interface for all other interfaces. Introduces the QueryInterface() method, which is useful for discovering and using other interfaces implemented by the same object.

The two most popular interfaces are IUnknown and IDispatch . We will use IDispatch later in discussing Automation.


The first step when defining a custom interface is to establish a physical name for the interface. Let's take a look at a simple C++ example that declares a new COM interface.

 interface DECLSPEC_UUID("{62648A4D-E9B4-4D92-A3AF-56AB782E233A}")          IMetricConversion:          public IUnknown  {          virtual HRESULT feet_to_meters(double feet/*[in]*/,                double* meters/*[out]*/) = 0;          virtual HRESULT meters_to_feet(double meters/*[in]*/,                double* feet/*[out]*/) = 0;  } 

Notice how our interface inherits IUnknown . Also, an I prefix is used in our physical name to identify the type as an interface. This particular example includes five virtual methods that will need to be implemented as a COM class. In this example, each one of them returns an HRESULT return type, which is the common return type for COM methods. It is equivalent to a LongWord type. Possible HRESULT values are listed in the winerror.h file included with C++Builder. A return value of S_OK indicates success.

Interface ID

For our interface to work in COM, IMetricConversion is identified (keyed) by a Globally Unique Identifier (GUID), which is a 128-bit number. It is this GUID, rather than the C++ type name, that a client uses to reference our interface. When a GUID is associated with an interface, it is known as an Interface ID (IID). In the previous example, the IID for the IMetricConversion interface would be IID_IMetricConversion . In the system registry you'll find IIDs keyed by the GUID. The value data for each GUID in the registry identifies the physical name (for example, IMetricConversion ), but programmatically you reference the interface using the IID (for example, IID_IMetricConversion ).

If you do choose to utilize inline code that identifies a GUID for a custom interface as shown in the previous sample, you can create your own GUIDs as described in the following tip.

TIP

To generate a GUID at design time within the C++Builder use the Ctrl+Shift+G keystroke in the Code Editor. To create a GUID programmatically, you should first initialize COM by calling the Windows API function CoInitialize() , and then call CoCreateGuid() to generate a unique GUID. C++Builder also generates a GUID when using the Type Library Editor to create interfaces, which will be discussed later.


For a client to latch on to an interface, the QueryInterface() method is used with the IID, as shown in the following psuedocode example.

 extern GUID IID_IMetricConversion;  IUnknown *pServerUnkn;  // some code is left out here  // use OLE to get IUnknown of Server  IMetricConv *pMetricConversion;  pServerUnkn->QueryInterface(IID_MetricConv, (void **)&pMetricConversion);  // use MetricConversion methods  ServerUnkn->Release();  // release server  pMetricConversion->Release();  // release pointer to pMetricConversion object 

Keep in mind that this is a pseudocode example. The actual process of getting an IUnknown pointer to a server that supports the IMetricConversion interface is more complex than has been shown, and in-depth discussion is beyond our scope at this time. What's being shown here is how an instance of a COM object is created by passing the GUID-oriented class identifier (often called a CLSID). The CLSID is associated to the COM class we want to create.

In a short bit, we'll see how C++Builder alleviates much of the complexities through Microsoft's Active Template Library (ATL) with a class factory, specifically the TCoClassCreatorT template and CoCreateInstance() call. What's important to understand is that the GUID is utilized within IUnknown 's QueryInterface() to retrieve a pointer to pMetricConversion . After we latch onto an pMetricConversion object we can utilize the methods associated to the interface, such as meters_to_feet() or feet_to_meters() . Also, it's important for the client to release the object using the Release() method associated to the IUnknown interface after it's done.

You might be wondering why a C++ client doesn't delete a COM object as we would normally expect since an object was instantiated through the QueryInterface() call. In COM, it's not the responsibility of the client to delete an object pointer to a COM interface implementation. In fact, it should be avoided since other clients might also access the object within the server. Instead, a client issues a Release() call when it's done using the object. The reference counting provided by the IUnknown interface enables the proper lifetime management of an object. After the reference counting reaches zero when a Release() call is made, the server is free to delete the object.

Type Libraries

In C++, a header file is often created to share data types such as classes and structures that can be referenced by other C++ files. Unfortunately the use of a C++ header file is language-dependent and not practical with COM because it can't be used natively by other languages such as Delphi or Visual Basic. Therefore, when we create a COM interface in C++Builder, or in any other COM supported language, we need a mechanism to allow other languages to reference the interface we've defined. This is the capability that COM Type Libraries provide.

Type libraries provide a language-neutral mechanism for defining types such as interfaces, methods, classes, and other COM elements that are defined and used by a server and called on by a client. Type libraries are saved as binary files with a *.tlb extension or can be contained within the binary file representing the server ( *.dll , *.ocx , *.exe , *.olb ). C++Builder provides support for creating and viewing type libraries through the Type Library Editor. To view a type library, simply select File, Open from the main menu and select type library from the list of file types. Figure 17.2 provides an illustration of C++Builder's Type Library Editor.

Figure 17.2. Borland's Type Library Editor.

graphics/17fig02.jpg

We will use a Type Library to define and contain the interfaces we need for our examples. Later we'll also use a type library to create the interface implementation ( CoClass ) for a server.

NOTE

All COM interfaces conform to a Virtual Method Table (VMT), or vtable, which maps how an object's functions are laid out in memory. A vtable is COM's way of standardizing the organization and order of declared interfaces so that multiple languages can utilize it. A standardized layout of these interfaces, which contains virtual methods, is important in allowing language independence and binary compatibility.


Creating an Interface in C++Builder

The easiest way to create an interface in C++Builder is through Borland's Type Library Editor. This is accomplished by selecting File, New, Other from the IDE's main menu. When the New items dialog appears, select on the ActiveX tab. This view is shown in Figure 17.3.

Figure 17.3. New Items DialogActiveX View.

graphics/17fig03.jpg

Within this view, select the Type Library icon to activate the Type Library Editor. The Type Library Editor contains a toolbar across the top allowing you to create various COM elements, and a tree view on the left side identifying the elements of your type library. The root node of the tree view always represents the type library itself. To create an interface, you need to select the first red glyph in the top toolbar.

You'll notice in the Attributes tab sheet a GUID is automatically generated for the interface. You can rename your interface, and identify the type of interface you're inheriting, such as IUnknown , in the Parent Interface selection. To add methods to your interface, select the glyph from the toolbar that looks like a green downward right arrow. Again, you can rename the method within the Attributes tab sheet. An example of an interface created using the Type Library Editor is shown in Figure 17.4.

Figure 17.4. Using Borland's Type Library Editor to create an interface.

graphics/17fig04.jpg

To generate the implementation for the Type Library, select the Refresh Implementation glyph on the toolbar. C++Builder generates/updates a source and header file for your server project that accompanies the TLB. Select F12 to view the source code for the TLB file. The areas of interest within the header file for the TLB we created is shown in Listing 2.1.

Listing 17.1 Type Library Header File Declarations
 // *********************************************************************//  // Forward declaration of types defined in TypeLibrary  // *********************************************************************//  interface DECLSPEC_UUID("{FAA00638-C897-4689-9AFF-D5B8E53A3A72}")          IMetricConversion;  typedef TComInterface<IMetricConversion, &IID_IMetricConversion>          IMetricConversionPtr;  // *********************************************************************//  // Interface: IMetricConversion  // Flags:     (320) Dual OleAutomation  // GUID:      {FAA00638-C897-4689-9AFF-D5B8E53A3A72}  // *********************************************************************//  interface IMetricConversion  : public IUnknown  {  public:    virtual HRESULT STDMETHODCALLTYPE feet_to_meters(double feet/*[in]*/,          double* meters/*[out]*/) = 0; // [3]    virtual HRESULT STDMETHODCALLTYPE meters_to_feet(double meters/*[in]*/,          double* feet/*[out]*/) = 0; // [4]  }; 

The Type Library Editor enables you to register the Type Library Binary (TLB) for this interface by selecting the Register Type Library glyph. By registering the TLB, we can use the interface for supporting development of other applications in the future (that is, COM servers that implement the interface). This is demonstrated in the following section.

Implementing an Interface in C++Builder

To implement an interface, we need to create a COM class (called a CoClass ) within a COM server. A Server can include an out-of-process server such as an EXE application or an in-process server such as DLL.

COM SERVER TYPES

COM servers are binaries that contain the implementation of at least one COM object. Depending on where the server resides, it can be classified as in-process ( inproc ), out-of-process ( outproc ), or remote . It's important to select the type of COM Server you wish to implement.

An inproc server is always a DLL (OCX files, where ActiveX controls usually reside, are actually DLLs). This kind of server makes its components reside in the client's address space. The main advantage is speed. The main disadvantage is that if the server crashes, the client will probably crash, too, because they share the same memory segment.

An outproc server is an executable. The main advantage with an EXE is that if the server crashes, the client can potentially recover without much of an incident. The disadvantage is that these servers typically respond slower.

A remote server resides in a different machine than the client. It can be implemented as a DLL (using a surrogate process that wraps the DLL) or an EXE file.

COM+/MTScompatible objects must be developed in DLLs to take full advantage of these technologies. Therefore, they tend to be the choice when there is no reason to prefer an EXE file.

Start by creating either an ActiveX Library (DLL) or a standard EXE application, which will represent the COM server. To create an ActiveX Library, select the ActiveX library icon in the ActiveX tab of the New items dialog (see Figure 17.3 for reference). After you have you started the DLL or EXE project, you need to select the COM Object icon in the ActiveX tab of the New items dialog (again, see Figure 17.3 for reference). This will open the New COM Object dialog as illustrated in Figure 17.5.

Figure 17.5. New COM Object dialog.

graphics/17fig05.jpg

In this illustration, we entered a name for our class. Again, the type that identifies a COM object is often referred to as a component class or CoClass.

Next , we use the type library containing the IMetricConversion interface we created earlier through the Interface Selection Wizard. This is shown in Figure 17.6.

Figure 17.6. Interface Selection Wizard.

graphics/17fig06.jpg

After a class has been created using the Type Library Editor, an icon will appear in the tree view representing our CoClass . This is shown in Figure 17.7.

Figure 17.7. New CoClass created represented within the Type Library Editor.

graphics/17fig07.jpg

NOTE

The node in the tree view illustrated in Figure 17.7 represents a new CoClass , which we expected. If we had checked the Generate Event support code check box in the New COM Object dialog (see Figure 17.5) the Type Library Editor would have generated what's called a dispatch interface (or dispinterface ). This combination of a CoClass and a dispinterface is sometimes referred to as a dual interface, which allows binding on an object to occur two different ways: late binding and early binding. The dispinterface is an interface used to support binding at runtime (called late binding), whereas early binding occurs at compile time, by directly linking to a member function of a vtable representing the object (the custom interface). Basically, the dispinterface provides runtime access for an object so that it can issue event callbacks to the client. This is part of a two-way automation, which we'll discuss in more detail later in the chapter.


When Refresh is selected, C++Builder's Type Library Editor will autogenerate the code needed for our class. Actually several file pairs are created as identified in Table 17.4.

Table 17.4. Type Library Editor Autogenerated Files When Creating a New Object

File Pair

Description

Type Library

The C++ TLB code for the COM class. Represented by *_TLB.* .

Active Template Library

The C++ ATL code for the COM class. Represented by *_ATL.* .

Implementation

The C++ implementation boilerplate . Represented by *Impl.* .

The most useful header/source file pair generated from a developer's standpoint, is the implementation boilerplate. Here's where we can fill in the code needed to process a method defined by the original interface, as shown in Listing 17.2.

Listing 17.2 TMetricConversion CoClass C++ Source Code
 // TMETRICCONVERSIONIMPL : Implementation of TTMetricConversionImpl  // (CoClass: TMetricConversion, Interface: IMetricConversion)  #include <vcl.h>  #pragma hdrstop  #include "TMETRICCONVERSIONIMPL.H"  #pragma link "MetricConversion_OCX"  /////////////////////////////////////////////////////////////////////////////  // TTMetricConversionImpl  STDMETHODIMP TTMetricConversionImpl::feet_to_meters(double feet,    double* meters)  {       *meters =  feet * 0.3048;       return S_OK;  }  STDMETHODIMP TTMetricConversionImpl::meters_to_feet(double meters,    double* feet)  {       *feet = meters * 3.2808;       return S_OK;  } 

As you might except from Borland RAD Tools, the Type Library Editor generates the method calls within the source code, leaving the implementation up to you, the developer. Unfortunately, if any modifications are made to the declarative elements of either the source or header implementation boilerplate, they will not be reflected in the Type Library Editorit's not quite two-way. In this example, after the boilerplate code was generated, the code representing the behavior for each method was added manually.

NOTE

If you have created an inproc ActiveX Library, be sure to register the server after you're satisfied with the implementation. This is accomplished by selecting Run, Register ActiveX Server from the IDE's main menu. When successfully registered, your COM server will be accessible to client applications. Note that you can unregister a COM server by selecting Run, Unregister ActiveX Server.

To register an outproc Server, simply run the application after each time it is built.


As you can see, the same basic steps apply to creating a class in C++Builder as they do in creating an interface. It's just that a class must be implemented as part of a server. The accompanying CD-ROM contains an example for both an inproc DLL server and an outproc EXE server within the SimpleCOM folder for this chapter. Just look for the projects titled MetricConversionServerEXE.bpr and MetricConversionServerDLL.bpr , respectively.

Accessing a COM Object

Now that we have both an inproc and outproc Server, we need to build a simple application known as a COM client that utilizes the MetricConversion object contained within these servers.

The ClientExample program for this chapter, which can be found in the SimpleCom folder for this chapter on the companion CD-ROM, contains two check boxes for selecting the server type at runtime, two edit fields for entering measurement values, and two buttons to access the methods of the object to convert these measurements. Let's take a look at the code for accessing these two different types of servers, as shown in Listing 17.3.

Listing 17.3 ClientExample C++ Source Code
 #include <vcl.h>  #pragma hdrstop  #include "ClientForm.h"  #include "MetricConversionServerDLL_TLB.cpp"  #include "MetricConversionServerEXE_TLB.cpp"  //--------------------------------------------------------------------------- #pragma package(smart_init)  #pragma resource "*.dfm"  TForm1 *Form1;  //--------------------------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner)          : TForm(Owner)  {  //  }  //--------------------------------------------------------------------------  void __fastcall TForm1::ButtonConvertToFeetClick(TObject *Sender)  {    TCOMIMetricConversion MetricConversion; //    if (RadioButtonEXE->Checked)          MetricConversion =                  Metricconversionserverexe_tlb::CoMetricConversion2::Create();    else          MetricConversion =                  Metricconversionserverdll_tlb::CoMetricConversion::Create();    double meters = EditMeters->Text.ToDouble();    double feet;    MetricConversion->meters_to_feet(meters,&feet);    EditFeet->Text = AnsiString(feet);  }  //--------------------------------------------------------------------------- void __fastcall TForm1::ButtonConvertToMetersClick(TObject *Sender)  {    TCOMIMetricConversion MetricConversion; //    if (RadioButtonEXE->Checked)          MetricConversion =                  Metricconversionserverexe_tlb::CoMetricConversion2::Create();    else          MetricConversion =                  Metricconversionserverdll_tlb::CoMetricConversion::Create();    double feet = EditFeet->Text.ToDouble();    double meters;    MetricConversion->feet_to_meters(feet,&meters);    EditMeters->Text = AnsiString(meters);  } 

For accessing two different servers, there's really not a lot of code required. We only need to include the server's TLB file within our include section. This approach is unlike the approach required for an application that leverages capabilities provided by a standard DLL. With a standard DLL, the application needs to either link with an equivalent LIB at build time or use the Windows LoadLibrary() call at runtime. In COM, C++Builder provides a utility file called utilcls.h , which contains a class template called TcoClassCreatorT , which is used to expose Create() and CreateRemote() routines for clients. This class template is used within our client by CoMetricConversion within the MetricConversionDLL_TLB.h file as shown here.

 typedef TCoClassCreatorT<TCOMIMetricConversion, IMetricConversion,                &CLSID_MetricConversion,                &IID_IMetricConversion> CoMetricConversion; 

When we make the CoMetricConversion::Create() call as shown in Listing 17.3, the class template actually calls CoCreateInstance() , which is what's used in COM to instantiate an object of the class associated with a specified class identifier (CLSID). In our case the class identifier is CLSID_MetricConversion , which was provided through the typedef declaration that applied the TCoClassCreateorT template.

NOTE

According to the Borland Help:

" CoCreateInstance() first ensures that COM is initialized before attempting to create the specified object. It then connects to the class object specified by [the class identifier], uses its IClassFactory interface to create an instance, releases the class factory, and returns the requested interface."

If the object is successfully created, CoCreateInstance() returns S_OK . Borland's implementation of CoCreateInstance() protects against errors that might occur if the object is not available locally.


Figure 17.8 illustrates this example during execution.

Figure 17.8. Snapshot of the COMClient example.

graphics/17fig08.gif

This example also demonstrates how COM servers, which offer varying levels of fidelity while still being based on the very same interface, can be developed and deployed. In this example, the inproc DLL server doesn't provide nearly the same amount of fidelity as the outproc EXE application. However, they both support the same interface.

We can also build clients that utilize a COM server with application development environments such as Delphi, Visual Basic, Visual C++, or even Java. This is one of the benefits over a standard DLL approach because a DLL, to be effective and useful with other languages, needs to be written with C structure function calls and wrappers to any embedded class methods. This isn't uncommon because many of the Win32 API calls, which are contained within DLLs, provide straight C functions. But COM enables us to maintain a more object-oriented design and enforces a guaranteed interface after it's been registered. The same can't be said for a DLL because there's no guarantee it will maintain its interface (and backward compatibility) as it changes and evolves. Additionally, in COM, an interface implementation can be accessed and used as objects externally by clients. Furthermore, with the capabilities of COM such as Distributed COM (DCOM), which will be discussed in the next chapter, clients can leverage and access objects provided by remote servers, not just inproc our outproc servers on a single platform.

OPERATING AS A COM CLIENT

COM Clients are applications that access COM objects implemented by a server application (EXE) or library (DLL, OCX). Examples of COM Clients include applications that visually reflect an ActiveX control (called an ActiveX container), utilize the services and capabilities provided by an external application (called an Automation controller), or simply access objects and associated data provided through a server application. Although COM Clients vary, the steps in functioning as a COM client are very similar.

COM Clients first must call either the CoInitialize() or CoInitializeEx() function provided by the objbase.h Win32 API file. In our example, this call was made for us within the CoCreateInstance() method, which we described earlier. Initialization needs to be performed before utilizing any other COM function. CoInitializeEx() provides an additional parameter over CoInitializeEx() enabling you to specify the type of threading model. Although there are many types of threading models, the two valid choices for this function are either apartment-threaded or free-threaded. Threading models are discussed later in this chapter and are further defined in Table 17.5. The use of CoInitialize() defaults the threading model to an apartment-threaded.

When initialized, the client can use an interface (or set of interfaces) to a server object and begin to use its properties and methods. However, the client must have access to the interface, which is often provided by a type library.

Importing a Type Library

Because we created the server for our last example, we were fortunate to have the server's TLB header file that we included in our client.

 #include "MetricConversionServerDLL_TLB.cpp" 

In many instances, however, the C++ TLB source code for a server will not be provided, only its type library ( *.TLB ) or within the actual server ( *.DLL ). Fortunately, there's a mechanism to import a type library within C++Builder.

For a client to digest the COM-type elements a server exposes (including classes and interfaces), follow these steps:

  1. Click Project, Import Type Library in the BCB IDE. The Import Type Library dialog appears (see Figure 17.9 later in this chapter).

    Figure 17.9. Importing the MetricConversionServerDLL Type Library.

    graphics/17fig09.jpg

  2. In this example, uncheck the Generate Component Wrapper check box. Having this checked indicates that you want component wrappers to be generated for all CoClasses that are not flagged as Hidden, Restricted, or PreDeclID. An unchecked control will generate the type library definitions, but not the component wrappers.

  3. In the Import Type Library list box, select the name of the Type Library of your server. In our example, it is the MetricConversionServerDLL Library (Version 1.0). Press the Create Unit button. The file MetricConversionServerDLL_TLB.cpp will be added to your project.

NOTE

If you compare the server sources TLB files MetricConversionServerDLL_TLB.cpp and MetricConversionServerDLL_TLB with the C++ files generated using the Import Type Library wizard, you will notice that they are equivalent. However, because COM is a binary standard, there is no need to distribute or locate the TLB header file of a server. Also, remember that a server can be written in any COM-supported programming language, not just C++.

The main advantage of distributing the Type Library is that it can be used to generate specific language declarations of all the components and types it describes. That will guarantee that your components (interfaces, dispinterfaces, CoClasses, and other elements) are capable of being used by any development platform.



   
Top


C++ Builder Developers Guide
C++Builder 5 Developers Guide
ISBN: 0672319721
EAN: 2147483647
Year: 2002
Pages: 253

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