Полезная информация

Chapter 12
Creating ActiveX COM Objects and Custom Interfaces Using MFC


Creating ActiveX COM Objects and Custom Interfaces Using MFC

ActiveX is a strategic technology base for Internet programming and distributed computing. While ActiveX is the successor for OLE (Object Linking and Embedding), OLE still forms the foundation of ActiveX programming. The basis for ActiveX is to provide an object-oriented solution for solving problems encountered in developing operating systems and applications. ActiveX provides the specifications necessary to create component software that ultimately benefits the computing industry.

At the core of ActiveX is an extremely powerful and extensible architecture called the Component Object Model (COM). COM provides a simple yet elegant solution for solving complex software problems such as accessing objects and services outside of application boundaries and version control. COM solves these problems through the use of binary components that are running in the system rather than by developing source code components within an application.

If you are using the Visual C++ compiler from Microsoft, chances are very high that you are also using the Microsoft Foundation Classes (MFC) as the building blocks for your applications and components. MFC is a powerful set of base classes that provide a framework of abstractions into the Windows SDK for developing Win32 applications and components.

Classes within the MFC framework are not directly derived from COM interfaces. However, the architects of MFC have provided direct support for adding COM to any MFC-based component or application. The roots for supporting COM within MFC lie in wrappers called Interface maps. Interface maps are similar to message maps (which are used for distributing Windows messages to MFC classes) in both concept and execution.

Anatomy of a COM Object

COM Objects give software developers the ability to leverage object-oriented software techniques for solving application and operating system development issues. The COM specification is not geared toward a specific language, although C++ is a natural choice when developing COM Objects. Four basic components compose a COM Object:

The class ID (CLSID) is an identifier for the COM class. This key is registered in the Windows Registry and contains a pointer (path) to where the DLL (Dynamic Link Library) or EXE containing the class can be located. The CLSID can be found in the Windows Registry under the path HKEY_CLASSES_ROOT\CLSID.

The Interface ID (IID) is an identifier for the interface to the class. The IID is used by applications to query and invoke the methods into the class. The IID is also contained in the Windows Registry and can be found in the path HKEY_CLASSES_ROOT\Interface. Figure 12.1 illustrates the relationship among class, interfaces, and IID.

FIG. 12.1
Relationship of COM Classes and Interfaces.

What COM provides to software developers is an object-oriented solution for building and maintaining software solutions. Programmers using non-object-oriented languages such as Visual Basic can develop and use COM components to build software solutions.

COM also provides a unique solution to the version control problems present in many of today's software solutions. Since COM Objects are binary components, developers do not have to worry about new or updated versions of a component being placed on a computer where their application is running. The reason for this is that COM deals with interfaces. If an interface is enhanced, new methods can be added to the interface, or additional interfaces can be obtained without breaking an existing application. COM's solution to version control provides a great method for upgrading applications while preserving legacy systems.

Tools Needed for Building COM Objects

When creating your COM Objects, a few tools must be installed on your computer. Most of these tools are automatically installed as part of the Visual C++ development environment.

MIDL Compiler

The Microsoft MIDL compiler is now a standard component of the Microsoft Visual C++ environment. The MIDL compiler compiles COM interface definitions into C code, which is then compiled into the project by the Visual C++ compiler. Figure 12.2 illustrates the purpose of the MIDL compiler.

FIG. 12.2
Inputs and outputs of the MIDL compiler.

The MIDL compiler also provides support for marshaling interfaces across process boundaries. Starting with Visual C++ 4.0, the MIDL compiler was shipped as a standard component of Visual C++. The MIDL compiler is also available with the Win32 SDK from Microsoft.

GUIDGEN

GUIDGEN is a tool used to generate Global Unique Identifiers (GUID), which can be used for Interface IDs, Class IDs, or any other 128-bit UUID, such as an RPC interface. GUIDGEN is installed when the OLE development option is selected during the Visual C++ installation. When GUIDGEN is run, you must select the proper format for the UUID and then press the New GUID button to copy the UUID to the Windows Clipboard. After running the GUIDGEN application, the resulting GUID is pasted from the Clipboard into the code that needs a GUID.

NOTE: The tool GUIDGEN is also installed by default if the option Typical is selected during the Visual C++ 5.0 installation.

RegEdit

RegEdit or the registration editor is a standard component of both the Windows 95 and Windows NT operating systems. The registration editor is used for browsing and altering operating system and application settings. The registration editor can also be used for installing and registering your COM Objects.

CAUTION:
RegEdit is a powerful tool and must be used with extreme caution by experienced users. If used improperly, systems can be damaged, resulting in a loss of data or a malfunctioning computer.



In Windows 95, this program is called regedit.exe. In Windows NT, this program is called regedt32.exe.

Registration Server

The registration server is an application that can be used to register the settings of a COM Object in the Windows registry without the need to create a separate registration file. The application is called regsvr32.exe and is automatically installed if the OLE development option is selected during Visual C++ installation or if the ActiveX SDK is installed.

Adding the Tools to the Visual C++ Development Environment

In order to maximize development productivity, the tools needed for COM programming should be integrated into the Visual C++ environment. Each of the tools needed can be added to the IDEs (Integrated Development Environment) Tools menu. The following sections illustrate how to incorporate the tools into the IDE.

Adding the MIDL Compiler to the IDE
Adding the MIDL compiler to the IDE allows for easy compilation of an IDL (Interface Definition Language) file. After adding this command, an IDL file can be compiled, and the MIDL compiler will generate a C source file with all appropriate parameter marshaling code. To add the MIDL compiler to the Visual C++ environment:

  1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog.

  2. In the Command edit box, type MIDL.EXE.

  3. In the Menu contents edit box, type Compile &IDL File.

  4. In the Arguments edit box, type /ms_ext /char unsigned /c_ext $FileName.

  5. In the Initial directory edit box, type $FileDir.

  6. Click the check box Use Output Window (the box should already be checked).

  7. In the completed Customize dialog, click the Close button to add the entry to the Tools menu (see fig. 12.3).

FIG. 12.3
Add your tools settings for the MIDL compiler in the Customize dialog.

Adding GUIDGEN Adding GUIDGEN to the Visual C++ environment enables the generation of a UUID from a single menu command. As stated earlier, the generated UUID is placed in the Windows Clipboard and must be pasted into the project code. To add GUIDGEN to the Visual C++ development environment:

  1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog.

  2. In the Command edit box, type GUIDGEN.EXE.

  3. In the Menu contents edit box, type &Generate New UUID.

  4. Clear all text from the Arguments edit box.

  5. Clear all text from the Initial Directory edit box.

  6. Click the Close button to add the entry to the Tools menu.

Adding the Registry Editor The Registry Editor serves two purposes: to add registration information to the Windows registry and to browse the registry to view information. To add the Registry Editor to the Visual C++ environment, follow these steps:

  1. Select the Customize command from the Tools menu. Select the Tools tab from the Customize dialog.

  2. In the Command edit box, type REGEDIT.EXE if the development platform is Windows 95. Type REGEDT32.EXE if the development platform is Windows NT.

  3. In the Menu contents edit box, type &Registry Editor.

  4. Clear all text from the Arguments edit box.

  5. In the Initial directory edit box, type $FileDir.

  6. Click the Close button to add the entry to the Tools menu.

Adding the Registration Server If the ActiveX Control option was selected during the Visual C++ installation, the registration server is already installed as the Register Control command from the Tools menu. If you have not installed the ActiveX Control option, installing it now adds the appropriate files and menu items to the Visual C++ development environment.

Defining COM Interfaces Using IDL

A COM interface is a group of functions used to manipulate the data of the class that is implementing the interface. Interfaces only define the functions that belong to the group. An interface does not implement the function or contain data. The function implementation and data belong to the class that implements the interface.

ActiveX is based entirely on a set of COM interfaces. These COM interfaces are a standard part of the operating system. In other words, Windows 95 and Windows NT contain all of the code that implement the ActiveX COM interfaces.

When building new components based on COM, these components define custom interfaces. A custom interface is an interface that is not already supported by the operating system. A custom interface contains a set of functions that are specific to the new component being built. For example, a spell-checker component may contain a custom interface that contains a set of functions used by a program that uses the spell-checker component. Once an interface is defined, multiple components may be built that support and implement the interface.

Going back to the spell-checker component, a defined spell-checker interface may be implemented by multiple companies. Having multiple companies provide a component with the same interface gives application developers the flexibility to have all of the components exist on a system, yet provide the user with the ability to load and use a specific company's spelling checker.

When creating a custom interface, the interface definitions need to be shared among multiple applications, such as the server that implements the interface and the client that uses the interface. For this reason, it makes sense to define the interfaces in a project separate from the server or client projects. Multiple interfaces can be defined within a single project.

In this chapter, we develop three projects to implement and use COM Objects. Table 12.1 shows the project names and the purpose of each project.

Table 12.1 Chapter 12 Project Descriptions
Project Name Purpose
IFISH (IFISH.DLL) Implements a COM interface definition.
BASS (BASS.DLL) Contains an MFC class that implements the COM interfaces in IFISH.
COMTEST (COMTEST.EXE) Sample Test application that uses the BASS COM Object.


The project IFISH defines two COM interfaces, the IFish interface and the IBass interface. The IFish interface is a base class for all of the different species of fish. The IBass interface is an interface specific to a particular type of fish. Both of these interface definitions will be implemented within the IFISH project.

Creating the IFISH Project

The IFISH project contains two COM interface definitions, IFish and IBass. The project IFISH is implemented as a DLL. The DLL does not contain any MFC code or written C\C++ code. The code contained within IFISH is produced by the MIDL compiler. The MIDL compiler takes the interface definition files (IDL) as input and produces C code for the interface as output. The C code that is produced is needed to implement parameter marshaling. Parameter marshaling is needed if the COM interface is implemented in an executable (EXE). The marshaling allows the parameters to be passed across process boundaries.

Even if the COM interface implementation is in a DLL (in-process server), the MIDL compiler should still be used. There are no penalties for implementing parameter marshaling.

NOTE: The IFISH project is built as a DLL. This is not the DLL that is implementing the COM interface. IFISH contains only the interface definitions. Projects that contain the interface definitions should be implemented as DLLs.



Perform the following steps in order to create the IFISH project:

  1. From within the Visual C++ development environment, select the command New from the File menu.

  2. Select Projects tab from the New dialog.

  3. Select Dynamic Link Library (see fig. 12.4). Enter the project name into the Project name edit box. The project is named IFISH. Select the OK button.

    FIG. 12.4
    Select the project attributes for IFISH in the New dialog.

  4. The project IFISH is now created.

Creating the Interface Definition

When creating the interface definition, you must determine whether marshaling code is needed to provide support for passing parameters between two processes. The safest method is to always assume that marshaling is needed. Providing marshaling support also allows the freedom to create either an in-process server (DLL) or an out-of-process server (EXE) to implement the interface.

Use of the Microsoft RPC MIDL compiler provides parameter marshaling support. Parameter marshaling is automatically provided by defining the COM interface with the Interface Definition Language (IDL). Once the interface is defined using IDL, the RPC MIDL compiler automatically generates the code necessary for marshaling support.

Two interface definition files are used in the IFISH project, IFISH.IDL and IBASS.IDL (see Listing 12.1).

Listing 12.1 IFISH.IDL--Interface Definition for IFish

[
object,
uuid(011BB310-5AB0-11d0-907E-00A0C91FDE82),
pointer_default(unique)
]
interface IFish : IUnknown
{
import "unknwn.idl";
HRESULT IsFreshwater([out] BOOL *pBool);
HRESULT GetFishName([out, string, size_is(255)] char *p); }

All interface definition files have the extension IDL. The first portion of the IDL file contains an object definition section. The most important part of this section is the UUID of the object. The UUID is a unique 128-bit number that is created through the tool GUIDGEN. The UUID in IFISH.IDL distinctly identifies the IFish COM interface definition. This number is used by applications that will use the IFish interface.

A unique UUID number can be generated by performing the following steps:

  1. From the Visual C++ development environment, select the command Generate New UUID, located under the Tools menu. (Note: This command was added earlier to the Tools menu, as shown in the section "Adding GUIDGEN"). The Create GUID dialog is displayed (see fig. 12.5).

    FIG. 12.5
    Use the Create GUID dialog to generate unique identifiers for COM interfaces.

  2. From the dialog, under GUID Format, select Registry Format [i.e., {XXXXXXX-XXXX...XXXX}].

  3. Select the Copy button. This option copies the new UUID into the Windows Clipboard.

  4. Select Exit to close the GUIDGEN dialog.

  5. In the interface definition file, paste the contents of the Clipboard (that is, the new UUID) into the UUID section of the IDL file. This is the unique identifier used for your interface. If another GUID is needed, you can press the New GUID button and then copy the second GUID from the Clipboard.

Following the object definition section is the actual interface definition. As shown in Listing 12.1, the object definition resembles a C++ class definition. The keyword interface specifies the start of an interface definition. The name of the interface and any inherited interfaces follows. In this case, the interface name is IFish, and this interface inherits the IUnknown interface.

Since the IUnknown interface is a standard interface within the operating system, you don't need to redefine the functions within the interface. You only need to import the IUnknown interface definition. You do this through the statement import "unknwn.idl";.

The functions implemented by the interface need to be added in order to complete the interface definition. When using IDL, all portions of a function must be defined: the return value and all parameters, including direction (in, out, or both) and size of the parameters. Specifying all portions of a function allows the MIDL compiler to generate the correct marshaling code for the interface.

NOTE: All return values must be of type HRESULT, which is standard OLE return value. If the return value is not an HRESULT, the MIDL compiler will not provide marshaling information to marshal across process boundaries. The return value HRESULT is needed for network support. In case a network error occurs, a valid error code can be returned without having to generate an exception.



The interface for IBASS is shown in Listing 12.2. Note that IBass is an aggregate interface, not an inherited interface. Aggregate interfaces will be explained in the section "Implementing the Interface."

Listing 12.2 IBASS.IDL--Interface Definition for IBass

[
object,
uuid(F60D7C40-5B4E-11d0-ABE6-D07900C10000),
pointer_default(unique)
]
interface IBass : IUnknown
{
import "unknwn.idl";
HRESULT GetLocation([out, string, size_is(255)] char *p);
HRESULT SetLocation([in, string] char *p);
HRESULT EatsOtherFish([out] BOOL *pBool); }

Compiling the Interface Definition Files

After the respective IDL files have been created, they must be compiled in order for the interface code to be generated. Since the MIDL compiler was added to the project in the section "Adding the MIDL Compiler to the IDE," this task is an easy one.

To compile the IDL files, perform the following steps:

  1. Load the file IFish.IDL into the Visual C++ environment.

  2. Select the command Compile IDL from the Tools menu in the Visual C++ development environment.

  3. Load the file IBass.IDL into the Visual C++ environment.

  4. Select the command Compile IDL from the Tools menu in the Visual C++ development environment.

  5. The MIDL compiler has now compiled the interface definition files and generated the source code for supporting these interfaces.

You may be surprised to see the code that is generated by the MIDL compiler from the simple interface definitions IFish and IBass. Unlike a C++ compiler, the output from MIDL is not binary code. Instead, MIDL generates C code, which is then compiled by the C compiler as part of the interface project.

When the file IFISH.IDL was compiled, the files shown in Table 12.2 were generated.
Table 12.2 Results Produced by Compiling IFISH.IDL
File Purpose
IFISH.H Support header file for the IFish interface.
IFISH_I.C Interface definition file that is added to both the server and client projects.
IFISH_P.C Proxy code that implements the marshaling code for the interface.
DLLDATA.C Reference file used for loading the correct interface from the DLL. Shared by all IDL files compiled within this project.


The files that were created by the MIDL compiler must now be added to the IFISH project.

NOTE: When the IFISH project was created, no source files were included in the project. The entire project consisted of only a MAK file. Since this is an interface-only DLL, the entire contents of the project will consist of the files created via the MIDL compiler.



The following files must be added to the IFISH makefile in order for the IFish and IBass interface definitions to be accessible and used in COM Object implementations.
ifish.h ibass_i.c
ifish_i.c ibass_p.c
ifish_p.c Dlldata.c
ibass.h Rpchelp.c
You can add these files to the project by performing the following steps:

  1. From the Visual C++ development environment, select the Files command from the Add to Project menu item, which can be accessed from the File menu. The Insert Files into Project dialog is displayed (see fig. 12.6).

    FIG. 12.6
    The Insert Files into Project dialog is used for adding MIDL files into a project.

  2. Select the files shown in the preceding file list, and click OK. The files are now added to the interface project make file.

Creating a Definition File

One of the tedious tasks that, unfortunately, is not performed by either the Application Wizard or the MIDL compiler is the creation of a library definition file (DEF). This library definition file, a standard part of DLLs, defines which functions are exported or made accessible by the DLL. The filename is IFISH.DEF.

NOTE: Since a standard Win32 DLL was created, there were no functions to be exported because there were no source files when the file was created. This is not the case when an MFC DLL is created. In that case, MFC source code is produced, and a DEF file for the project is also created with default functions exported through the DEF file.



The contents of the IFISH.DEF file were created manually and can be viewed in Listing 12.3.

Listing 12.3 DLL LIBRARY--Definition File for IFISH.DLL

LIBRARY IFISH
DESCRIPTION `IFISH Interface Marshaling'
EXPORTS
DllGetClassObject
DllCanUnloadNow

The DLL entry points must be defined because of the parameter-marshaling code generated by the MIDL compiler. The MIDL compiler generates code that uses the IMarshall interface. The IMarshall interface requires the DLL entry point's DllGetClassObject and DllCanUnloadNow. The IMarshall interface is a COM interface that implements parameter marshaling for all COM Objects.

These two entry points are explained in greater detail in the section "Accessing In-Process COM Objects."

Adding the RPC Libraries to the Interface Project

Parameter marshaling is implemented through RPC (Remote Procedure Calls) libraries. When creating interface libraries that use RPC for parameter marshaling, you must link a number of RPC libraries into the interface project. You can select from two methods for linking the RPC libraries into the interface project:

Four RPC libraries must be included in the interface project, rpcndr.lib, rpcdce4.lib, rpcns4.lib, and rpcrt4.lib. Creating a file with compiler pragmas is much easier than trying to remember these library filenames and including them for each project that defines a COM interface.

In the IFISH project is a file called RPCHELP.C (see Listing 12.4). This file contains the necessary compiler pragmas for RPC support.

Listing 12.4RPCHELP.C--Compiler Pragmas Used for Referencing RPC Libraries

#pragma comment(lib, "rpcndr.lib")
#pragma comment(lib, "rpcns4.lib")
#pragma comment(lib, "rpcrt4.lib")

The file RPCHELP.C must be added to the IFISH project in order for the project to link properly. The file can be added to the project by performing the following steps:

  1. From the Visual C++ development environment, select the Files command from the Add to Project menu item, which can be accessed from the File menu.

  2. Select the files shown in the file list provided earlier in this chapter, and click OK. The files are now added to the interface project make file.

The interface definitions for IFISH are now complete, and the project is ready to be built. Building the project generates a DLL called IFISH.DLL.

Registering the Interfaces

Only one task remains before the IFish and IBass interfaces can be used. The interfaces must be registered in the Windows registry. The Windows registry is the holding ground for all class and interface IDs.

For the IFISH project, a registration file named IFISH.REG must be manually created. The contents of IFISH.REG are shown in code Listing 12.5.

Listing 12.5 IFISH.REG--Contexts of IFISH.REG File Used to Register the Interfaces Supported by the IFISH.DLL

HKEY_CLASSES_ROOT\Interface\{011BB310-5AB0-11d0-907E-00A0C91FDE82}
HKEY_CLASSES_ROOT\Interface\{011BB310-5AB0-11d0-907E-00A0C91FDE82} \ProxyStubClsid32
HKEY_CLASSES_ROOT\CLSID\{011BB310-5AB0-11d0-907E-00A0C91FDE82} = IFish_PSFactory
HKEY_CLASSES_ROOT\CLSID\{011BB310-5AB0-11d0-907E-00A0C91FDE82}\InprocServer32 = d:\dev\ifish\debug\ifish.dll
HKEY_CLASSES_ROOT\Interface\{F60D7C40-5B4E-11d0-ABE6-D07900C10000}
HKEY_CLASSES_ROOT\Interface\{F60D7C40-5B4E-11d0-ABE6-D07900C10000} \ProxyStubClsid32
HKEY_CLASSES_ROOT\CLSID\{F60D7C40-5B4E-11d0-ABE6-D07900C10000} = IBass_PSFactory
HKEY_CLASSES_ROOT\CLSID\{F60D7C40-5B4E-11d0-ABE6-D07900C10000}\InprocServer32 = d:\dev\ifish\debug\ifish.dll

To add the interface keys to the Windows registry, do the following:

  1. Select the command Registry Editor from the Tools menu of the Visual C++ development environment.

  2. Select the command Import Registry File from the Registry Editor File menu.

  3. Select the file IFISH.REG, and then select OK.

Implementing the Interface

Now that the IFish interface definitions are defined, the object that implements the interfaces must be created. Remember that the IFISH interface DLL contains only the interface definitions and RPC proxy code for parameter marshaling. There is no code for implementing the interface within IFISH.DLL. The COM Object developed in this section in the project BASS.DLL will contain an MFC class called CBass that will implement both the IFish and IBass interfaces.

COM Objects can be implemented as either a DLL or an EXE. COM Objects that reside within a DLL are called in-process servers. COM Objects that reside in an EXE are called out-of-process servers. The application that will use the COM Objects does not see a difference between the two types of servers. However, internally, there are a few differences between in-process servers and out-of-process servers.

CAUTION:
If DCOM is in your development plans, you have one detriment to in-process servers: DCOM or the Distributed Component Object Model allows components to reside on other computers in your network environment. This feature allows the components to utilize the processing power of other workstations. If you are planning for DCOM, you must implement your COM Objects as out-of-process servers because DLLs cannot be used across machine boundaries.

Using the Visual C++ AppWizard to Create the COM Object

Nothing special is needed to create a basic application for containing your COM Objects. The application is where the COM interface definitions are implemented. With this in mind, you can create a basic COM Object application. In this section, you will create a COM Object using an MFC DLL (in-process server) as the containing application. During creation of the application, the differences between creating an in-process and out-of-process server will be pointed out.

NOTE: The differences between an in-process and out-of-process server are trivial at this point because the interface DLL already contains the proxy code used for parameter marshaling. The determination to have an in-process server versus an out-of-process server depends on the use and needs of the COM Object.



To create the basic application, perform the following steps:

  1. In the Visual C++ development environment, select the New command from the File menu.

  2. From the Projects tab on the New dialog, select MFC AppWizard (dll). (See fig. 12.7.) Enter the project name into the Project name edit box. This project is called BASS. Select the OK button.

    FIG. 12.7
    Name the COM Object in the New Project Workspace dialog.


    NOTE:
    When creating an out-of-process server, select the MFC AppWizard (exe) item in the New Project tab.

  3. From the MFC AppWizard dialog, select Regular DLL with MFC statically linked or select Regular DLL using shared MFC DLL (see fig. 12.8), depending on your needs. Also select the Automation option in the MFC AppWizard dialog. The Automation option will cause the AppWizard to insert start-up and exit code used for both OLE Automation Servers and COM Objects. Select the Finish button when completed. The New Project Information dialog will confirm your choices (see fig. 12.9).

FIG. 12.8
Choose your project build options.

FIG. 12.9
Recap the project selections in the New Project Information dialog.

TIP: Your COM Objects will be easier to distribute if you use the static-linked version of MFC. If your COM Objects are part of a project that contains other MFC components and applications, you will get better performance by using the DLL version of MFC.



At this point, you have a basic shell application that can be used for your COM Objects.

Accessing In-Process COM Objects

In-process servers contain two functions that serve as entry points for clients accessing COM Objects. These functions are DllGetClassObject and DllCanUnloadNow. These functions are not needed for EXE or out-of-process servers.

In order for the COM support functions to be accessed, the functions must be exported from the DLL. An exported function can be called from any Windows application. The COM support functions are defined in MFC but are implemented in the server DLL. These support functions are also exported through the definition file (.DEF) of the DLL that uses the functions--in this case, BASS DLL. The AppWizard has already created a DEF file entitled BASS.DEF (see Listing 12.6).

Listing 12.6 BASS.DEF--BASS Definition File with the COM Support Functions Explicitly Exported

; BASS.def : Declares the module parameters for the DLL.
LIBRARY "BASS"
DESCRIPTION `FISH Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
DllCanUnloadNow PRIVATE
DllGetClassObject PRIVATE
DllRegisterServer PRIVATE

NOTE: The BASS definition file and the three support functions needed for accessing COM Objects were automatically inserted by the MFC Application Wizard as a result of selecting the OLE Automation option for the project. Automation and COM Objects are accessed in in-process servers through the same support functions. If OLE Automation was not selected, these functions will have to be implemented manually.


Each of the COM support functions are explained in the following three sections.

HRESULT DllGetClassObject (REFCLSID rclsid
, REFIID riid, LPVOID *ppv)
When a user requests a given COM Object, the Component Object Library looks into the Windows registry for the InProcServer of the given CLSID. The DLL that implements the COM Object is then loaded into memory, and the function DllGetClassObject is called. The CLSID of the COM Object implementing the interface and IID of the interface the user is requesting are passed into the function. The DLL containing the COM Object then creates the appropriate class factory for the CLSID and returns the corresponding interface pointer for the IID. Since a CLSID is passed into DllGetClassObject, a DLL can contain many different COM Objects. The interface pointer is returned to the caller through the parameter ppv.

The MFC AppWizard inserts all of the code needed for accessing COM Objects in an MFC Application DLL. The code for DllGetClassObjects is shown in Listing 12.7.

Listing 12.7 BASS.CPP--DLlGetClassObject Implementation Code Inserted by the MFC AppWizard

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllGetClassObject(rclsid, riid, ppv);
}

STDAPI DllCanUnloadNow(void) Since the client application using the COM Object does not link directly with the server creating the COM Object, an unload mechanism must be in place so that unneeded COM Objects are removed from memory. With in-process servers, this mechanism is through the function DllCanUnloadNow. The Component Object Library periodically asks each COM Object server if it can be unloaded from memory. If the server returns S_OK, the server DLL is removed from memory. A COM Object server returns S_OK when clients have finished using the COM Objects. The function DllCanUnloadNow is shown in Listing 12.8.

An EXE server does not need to implement the function DllCanUnloadNow because an EXE can keep track of how many users it has. When the usage count reaches 0, the EXE can unload itself from memory.

Listing 12.8 DLLCanUnloadNow--Implementation Code Inserted by the MFC AppWizard

STDAPI DllCanUnloadNow(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
return AfxDllCanUnloadNow();
}

BOOL DllRegisterServer(void) The function DllRegisterServer is a useful method for having the COM Object server correctly register with the Windows registry. When this function is called, the COM Object server updates the Windows registry with the settings necessary for client applications to use the server.

The ActiveX development kit has a tool called regsvr32.exe. This program can be run with two parameters. The first parameter is the DLL to register. The second parameter is TRUE (for registering a server) or FALSE for unregistering a server. To register the BASS COM server, run regsvr32 with the following parameters:

regsvr32 BASS.DLL TRUE

Listing 12.9 DllRegisterServer--Implementation Code Inserted by the MFC AppWizard

// by exporting DllRegisterServer, you can use regsvr.exe
STDAPI DllRegisterServer(void)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState());
COleObjectFactory::UpdateRegistryAll();
return S_OK;
}

Creating a Class that Implements COM Interfaces

Now that the application that will contain the COM Class has been created, the COM Class needs to be added to the project. The Visual C++ ClassWizard will be used to implement this class. The class name being created is CBass. To create the CBass class, perform the following steps:

  1. Select the ClassWizard command from the View menu of the Visual C++ development environment. The ClassWizard dialog will appear as shown in Figure 12.10.

  2. Select the Add Class button, and then select the new option. The New Class dialog appears (see fig. 12.11).

FIG. 12.11
Set the class options with the New Class dialog.

  1. Enter the class name CBass in the Name edit box.

  2. Select the base class CCmdTarget from the Base class drop-down list.

  3. Select the Create button. The Class CBass is now part of the BASS project.

The class CCmdTarget is chosen as the base class because MFC provides a standard implementation of the IUnknown interface in this class. As you have seen, the IUnknown interface provides the three basic methods that must be supported by all COM Objects.

Deriving from the CCmdTarget base class also allows the object to be created through the MFC class COleObjectFactory. The class COleObjectFactory is called through the DLL entry point, DllGetClassObject. If the COM class was not derived from a class with CCmdTarget as its base, special object creation code must be written in the function DllGetClassObject to create an instance of the object.

NOTE: Even though CCmdTarget was selected as the base class for the CBass class, any of the other classes derived from CCmdTarget can be used. If a class not derived from CCmdTarget is used, you must manually provide support for the IUnknown interface.



Figure 12.12 illustrates the CBass class and the interfaces that will be encapsulated within this class.

FIG. 12.12
Class hierarchy and supported interfaces of the CBass class.

Supporting the IUnknown Interface While the MFC class CCmdTarget provides built-in support for the IUnknown interface, the COM class derived from CCmdTarget must still provide methods that enable the MFC Interface maps to call these routines. Since IUnknown is a standard COM interface, the functions that must be called from the COM class have been implemented through a series of support macros. The advantage of the set of macros is that they can be used for the IUnknown implementation of any MFC derived class. The file commacros.h implements a set of macros that are used for calling the IUnknown implementation within the CCmdTarget class. Listing 12.10 shows the COM support macros for MFC.

NOTE: The file commacros.h is not a part of MFC and must be added to the project manually.

Listing 12.10 COMMACROS.H--COM Macros Used for Accessing the IUnknown Implementation of CCmdTarget

#ifndef _COMMACROS_H
#define _COMMACROS_H
#ifndef IMPLEMENT_IUNKNOWN
#define IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass)\
STDMETHODIMP_(ULONG)ObjectClass::X##InterfaceClass::AddRef(void)\
{ \
METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
return pThis->ExternalAddRef(); \
}
#define IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass)\
STDMETHODIMP_(ULONG)ObjectClass::X##InterfaceClass::Release(void)\
{ \
METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
return pThis->ExternalRelease(); \
}
#define IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass)\
STDMETHODIMP ObjectClass::X##InterfaceClass::QueryInterface(REFIID riid, LPVOID *pVoid)\
{ \
METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
return (HRESULT)pThis->ExternalQueryInterface(&riid ,ppVoid); \
}
#define IMPLEMENT_IUNKNOWN(ObjectClass, InterfaceClass)\
IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass)\
IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass)\
IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass)
#endif #endif

Adding a Class IDNow that the class that will support the COM interfaces is defined, a unique class ID (CLSID) must be created for the class. The CLSID allows the operating system to distinguish which application to load when a client program wants to invoke an instance of the object. To create a unique CLSID, the program GUIDGEN must once again be run.

NOTE: To create a unique ID using GUIDGEN, refer to the steps outlined in the section " Creating the Interface Definition."



After the CLSID is created, it must be placed in a header file that acts as a define for the class implementation. For the CBass object, the file bassid.h is created. The CLSID is then pasted into the file and added to the macro DEFINE_GUID (see Listing 12.11).

Listing 12.11 BASSID.H--Header File bassid.h that Contains the Implementation of CLSID for the CBass Class

#ifndef _CLSID_Bass
#define _CLSID_Bass
//{AFA853E0-5B50-11d0-ABE6-D07900C10000}
DEFINE_GUID(CLSID_Bass,0xAFA853E0,0x5B50,0x11d0,
0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00); #endif

The macro DEFINE_GUID assigns the name CLSID_Bass to the class ID that was created via GUIDGEN. This macro is placed in a header file that is used by all clients that need to invoke an instance of CLSID_Bass. This file is not used by the server that implements the COM Object.

Using Interface Maps to Support COM Interfaces
MFC supports COM interfaces through a technique known as interface maps. This technique is similar to the message maps used to route Windows messages to message handlers (functions) within the target class. Interface maps are basically a set of macros that provide support for the COM interfaces embedded within the MFC-derived class. Interface maps determine which interface methods will be handled by the particular MFC derived class. Only methods placed in the interface maps are supported by the COM Object. Only interface functions declared within the interface map are supported by the COM Object.

NOTE: Even though the IUnknown interface functions are not included within the interface map for the class, they must be supported by the class. All derived methods of the interface must be supported, although not explicitly defined.



Adding Interface maps to a class is an easy procedure. To add interface maps to a class, perform the following steps:

  1. Add the header files that define the interfaces to your class. In the BASS project, includes for the interface files Ifish.h and Ibass.h were added to the header file bass.h. The interface files ifish.h and ibass.h were generated by the MIDL compiler when the IFISH.DLL project was built. These files define the respective interface classes.

  2. Add the macro DECLARE_OLECREATE() to the class. This macro adds public data members to the class including COleObjectFactory, which is the primary interface needed for creating an object of the specified class.

  3. The COM specification dictates that interfaces simply define the interface and that they do not implement the methods for the interface or contain data members. This concept means that the class implementing the interface must contain the methods for the interface and contain any data variables needed to keep track of information about the interface. For the class CBass, Table 12.3 illustrates the data members needed for the interface implementation and the interface method that retrieves or sets the data member.

  4. Now that the data members are added to the class, some MFC macros must be added to the class in order for MFC to support the interfaces. The first macro is DECLARE_INTERFACE_MAP, which is added to a protected section of the class. This define is analogous to the DECLARE_MESSAGE_MAP macro that is needed for support of Windows message maps. DECLARE_INTERFACE_MAP is a macro that adds member variables needed for the support of COM interfaces.

  5. For each of the supported primary interfaces, there must be an interface map. An interface map is added to a class through the macros BEGIN_INTERFACE_PART (Class Name, Interface Name) and END_INTERFACE_PART (Class Name). Between these sections are the methods that implement the interface in the class. Again, the IUnknown interface methods do not need to be added.

NOTE: Upon inspection of the macro BEGIN_INTERFACE_PART, you can see that the methods for the IUnknown interface are automatically added to the class. This eliminates the need to manually add them to the class.



Table 12.3 Data Members Needed in CBass Class to Implement Supported Interfaces
Data Member Interface Method
BOOL m_bFreshwater IFish::IsFreshwater
CString m_zFishName IFish::GetFishName
CString m_zLocation IBass::GetLocation IBass::SetLocation
BOOL m_bEatsOtherFish IBass::EatsOtherFish


The header file for the CBass class (BASS.H) has been modified to include all of the changes listed in Table 12.3. The resultant file is shown in Listing 12.12.

Listing 12.12 BASS.H--Header File for the CBass Class (bass.h)

#ifndef __AFXWIN_H__
#error include `stdafx.h' before including this file for PCH
#endif
#include "resource.h" // main symbols
#include "..\ifish\ifish.h"
#include "..\ifish\ibass.h" //////////////////////////////////////////////////// /////////////////////////// CBass command target
class CBass : public CCmdTarget
{
DECLARE_DYNCREATE(CBass)
CBass(); // protected constructor used by dynamic creation
// Attributes
public:
CString m_zFishName;
CString m_zLocation;
BOOL m_bEatsOtherFish;
BOOL m_bFreshwater;
// Operations
public:
// Overrides
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CBass)
//}}AFX_VIRTUAL
// Implementation
protected:
virtual ~CBass();
// Generated message map functions
//{{AFX_MSG(CBass)
// NOTE - the ClassWizard will add and remove member functions here.
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
DECLARE_OLECREATE(CBass)
DECLARE_INTERFACE_MAP()
BEGIN_INTERFACE_PART(Fish, IFish)
STDMETHOD(GetFishName)(char *pStr);
STDMETHOD(IsFreshWater)(BOOL *pBool);
END_INTERFACE_PART(Fish)
BEGIN_INTERFACE_PART(Bass, IBass)
STDMETHOD(GetLocation)(char *pStr);
STDMETHOD(SetLocation)(char *pStr);
STDMETHOD(EatsOtherFish)(BOOL *pBool);
END_INTERFACE_PART(Bass) };

Implementing MFC Interface Maps In the previous section, interface maps were added to the class header file. The actual interface maps must now be implemented in order for the interfaces to be supported. These changes are made to the source implementation file for the class. For the CBass class, this file is bass.cpp.

  1. Add the COM support macros to the implementation file. Add the line #include "commacros.h" to your class implementation file. Commacros.h is the common include file that implements the IUnknown interface for your MFC class.

  2. Add the macro IMPLEMENT_OLECREATE() to the source file as shown here. This macro links the MFC class, in this case CBass, with the friendly or callable name for the class (Bass). The CLSID for this class is also passed into the macro. The IMPLEMENT_OLECREATE is needed for implementation of the COleClassFactory object that instantiates instances of the CBass object.

    //{AFA853E0-5B50-11d0-ABE6-D07900C10000}
    IMPLEMENT_OLECREATE(CBass, "Bass",
    0xAFA853E0,0x5B50,0x11d0,0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00)

  3. Implement the interface map for the class. This process is very similar to implementing the message map. You implement an interface map by using three macros: BEGIN_INTERFACE_MAP, INTERFACE_PART, and END_INTERFACE_MAP.


    The macro BEGIN_INTERFACE_MAP takes two parameters: the runtime class (Cbass) and the derived class (CCmdTarget). This macro provides a series of methods used for retrieving entries into the interface map structure.

    The macro INTERFACE_PART is needed for each COM interface that is to be supported by the class. This macro links the runtime class with the UUID for the interface and the friendly interface name.

    The macro END_INTERFACE_MAP ends the interface map implementation.

    BEGIN_INTERFACE_MAP(CBass, CCmdTarget)
    INTERFACE_PART(CBass, IID_IFish, Fish)
    INTERFACE_PART(CBass, IID_IBass, Bass)
    END_INTERFACE_MAP();

  4. Now you implement the interfaces within the class. This step is where the methods for the interface are implemented. Adding methods to a class is a straightforward process, with one exception. The method definition is as follows:

    runtime_class::interface_class::method.

    Listing 12.13 illustrates the implementation file, bass.cpp.
    The macro METHOD_PROLOGUE is used to establish a local variable named pThis, which is a pointer to the interface function table. The arguments to the METHOD_PROLOGUE are the runtime class and the interface name. This macro must precede every interface implementation.

  5. So far you have not implemented the IUnknown interfaces for the class. To implement the IUnknown interfaces, use the macro that was added in the commacros.h file. In bass.cpp, the following lines are added to implement the IUnknown interface for each interface.

    // implement the Iunknown interface
    IMPLEMENT_IUNKNOWN(CBass, Fish)
    IMPLEMENT_IUNKNOWN(CBass, Bass)


Listing 12.13 BASS.CPP--Complete Implementation File for CBass object (Bass.cpp)

/////////////////////////////////////////////////////////////////////////////
// CBass
IMPLEMENT_DYNCREATE(CBass, CCmdTarget)
CBass::CBass()
{
m_zFishName = "Large Mouth Bass";
m_zLocation = "Under Lily Pads";
m_bEatsOtherFish = TRUE;
m_bFreshwater = TRUE;
}
CBass::~CBass()
{
} BEGIN_MESSAGE_MAP(CBass, CCmdTarget)
//{{AFX_MSG_MAP(CBass)
// NOTE - the ClassWizard will add and remove mapping macros here.
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
//{AFA853E0-5B50-11d0-ABE6-D07900C10000}
IMPLEMENT_OLECREATE(CBass, "Bass",
0xAFA853E0,0x5B50,0x11d0,0xAB,0xE6,0xD0,0x79,0x00,0xC1,0x00,0x00)
BEGIN_INTERFACE_MAP(CBass, CCmdTarget)
INTERFACE_PART(CBass, IID_IFish, Fish)
INTERFACE_PART(CBass, IID_IBass, Bass)
END_INTERFACE_MAP();
/////////////////////////////////////////////////////////////////////////////
// CBass message handlers
// CBass:Fish implementation of IFish
// implement the Iunknown interface
IMPLEMENT_IUNKNOWN(CBass, Fish)
STDMETHODIMP CBass::XFish::GetFishName( char *pStr)
{
METHOD_PROLOGUE(CBass, Fish);
TRACE("CBass::XFish::GetFishName\n");
if (pStr)
strcpy((char *)pStr, (LPCTSTR)pThis->m_zFishName);
return (HRESULT)NOERROR;
}
STDMETHODIMP CBass::XFish::IsFreshwater( BOOL *pBool )
{
METHOD_PROLOGUE(CBass, Fish);
TRACE("CBass::XFish::IsFreswWater\n");
if (pBool)
{
*pBool = pThis->m_bFreshwater;
return S_OK;
}
return (HRESULT)NOERROR;
}
// CBass:Fish implementation of IFish
// implement the Iunknown interface
IMPLEMENT_IUNKNOWN(CBass, Bass)
STDMETHODIMP CBass::XBass::GetLocation( char *pStr)
{
METHOD_PROLOGUE(CBass, Bass);
TRACE("CBass::XBass::GetLocation\n");
if (pStr)
strcpy((char *)pStr, (LPCTSTR)pThis->m_zLocation);
return (HRESULT)NOERROR;
}
STDMETHODIMP CBass::XBass::SetLocation( char *pStr)
{
METHOD_PROLOGUE(CBass, Bass);
TRACE("CBass::XBass::SetLocation\n");
if (pStr)
pThis->m_zLocation = pStr;
return (HRESULT)NOERROR;
}
STDMETHODIMP CBass::XBass::EatsOtherFish( BOOL *pBool )
{
METHOD_PROLOGUE(CBass, Bass);
TRACE("CBass::XBass::EatsOtherFish\n");
if (pBool)
{
*pBool = pThis->m_bEatsOtherFish;
return S_OK;
}
// return E_BADPOINTER;
return (HRESULT)NOERROR;
}

Building the COM Object The good news is that all of the code has been written for the COM Object. However, here are a couple of items that still need to be done before the COM Object can be built:

Using the Interface

Use of COM Objects within an application is an easy task. A test application called ComTest has been developed to aid in the testing and use of the IFish and IBass interfaces.

Only a handful of functions are necessary to access and utilize COM interfaces within an application. These functions can be broken into two categories:

OLE Initialization and Shutdown Functions

When building MFC applications that will utilize COM interfaces, two functions must be called to properly initialize the MFC framework. These functions are AfxOleInit() and CoFreeUnusedLibraries(). These functions must be called during the application initialization and removal. Listing 12.14 shows how the OLE libraries are initialized for use during application creation and removed during exit.

NOTE: If the COMTEST app was created with OLE support, the AppWizard would have automatically inserted the function AfxOleInit() within the InitInstance() method of CComTestApp. Likewise, OLE termination code would be automatically called on the program's exit.

Listing 12.14 COMTEST.CPP--Initialization and Removal of OLE Libraries within an MFC Application

CComTestApp::CComTestApp()
{
// TODO: add construction code here,
// Place all significant initialization in InitInstance
AfxOleInit();
}
int CComTestApp::ExitInstance()
{
::CoFreeUnusedLibraries();

return CWinApp::ExitInstance();
}

COM Object Access Functions

Using COM interfaces within an application is similar to using any ordinary C++ class, the exception being that instances of a class are created through the function CoCreateInstance() rather than the new operator. After an interface pointer is returned, it can be used as though it is a C++ class, to call any of the functions in the interface. Listing 12.15 provides an example of how to access COM interfaces and call their functions.

Listing 12.15 Comtestview.cpp--Test Function Used for Accessing the IFish and IBass Interfaces

void CComTestView::OnEditCreatebassinterfaces()
{
char lo_Location[255];
char lo_FishName[255];
IFish *pIfish;
IBass *pIBass;
::CoCreateInstance( CLSID_Bass, NULL,
CLSCTX_INPROC_SERVER,
IID_IFish,
(LPVOID *)&pIfish);
if ( pIfish )
{
TRACE0("Success ... Got an interface to Ifish\n");
pIfish->GetFishName(lo_FishName);
TRACE1("The Fish Name is %s\n", lo_FishName);
if (pIfish->QueryInterface(IID_IBass, (LPVOID *)&pIBass)== S_OK)
{
pIBass->GetLocation(lo_Location);
TRACE1(" The Fish is a bass and it is located %s\n", lo_Location);
pIBass->Release();
}
pIfish->Release();
} }

From Here...

This chapter discussed creating COM Objects based on the MFC application framework. While MFC does not directly utilize COM, provisions have been made so that MFC supports COM and reduces the amount of work necessary in creating COM Objects. Other techniques can be used for creating COM Objects. Chapter 13 examines the creation of COM Objects through the ActiveX Template Library (ATL). ATL provides a lightweight COM framework for building COM Objects.