Frank Crockett
Microsoft Corporation
March 1999
Click here to download the sample associated with this article.
Summary: Discusses the development of a simple MFC client for use in testing ATL server objects. (12 printed pages) Covers the creation and implementation of a sample COM server.
Introduction
The ATL COM Server
The QueryString Custom Method
The MFC Client
Accessing the COM Object from the Simple Client
Using Namespaces and Smart Pointers
BSTRs and String Manipulation
Further Reading
The following discussion looks at developing a simple Microsoft Foundation Classes (MFC) client for use in testing Active Template Library (ATL) server objects. This article assumes that the reader is familiar with the fundamental elements of ATL, MFC, and Microsoft® Visual C++®. Basic knowledge of COM, specifically interfaces and object creation, is also assumed.
MyObj, the ATL COM server object, is implemented by a dynamic-link library (DLL) called MySvr. It implements a custom interface, IMyObj, which exposes a property and a method.
The MFC client, MyClient, is a dialog-based application that provides a graphical user interface for manipulating and displaying this property. The application uses smart pointers to create the COM object and access its property and method.
The sample COM server was created using Visual C++ and two ATL wizards. The only code that wasn’t automatically generated is the implementation code for the custom property (ObjectString) and method (QueryString).
The COM server application was created by using the ATL COM AppWizard. The ATL COM project option was chosen and no modifications were made to the default options.
A simple COM object was then added using the ATL Object Wizard. From the wizard, the Simple Object component was chosen (located in the Objects category) and a name for the component was supplied (MyObj). To reduce the amount of interface code generated for the project, the Custom option (located in the Interface group) was chosen.
Note Because the COM object is not implementing any standard interfaces or firing events, the object only needs code for implementing a custom interface. This is why Custom is chosen, instead of Dual.
At this point, the application consisted of a simple COM object whose interface has no properties or methods.
The custom property was added, using the Add Property dialog box, with the following values:
To store the value for the new custom property, a data member (of type BSTR) was added to the object class and was initialized to NULL in the constructor of the COM object (CMyObj). Because the sample uses a BSTR data member, the memory allocated for storage of the string must be freed up when the object is dismissed. This was done by adding a destructor to the CMyObj class. The following code, taken from the MySvr sample, demonstrates these steps:
...
BSTR m_strObject;
CMyObj()
{
m_strObject= NULL;
}
~CMyObj()
{
SysFreeString(m_strObject);
}
...
Finally, the get_ObjectString and put_ObjectString functions that are generated by the ATL Object Wizard were modified to retrieve and set the ObjectString value:
STDMETHODIMP CMyObj::get_ObjectString(BSTR *pVal)
{
*pVal= SysAllocStringByteLen((char*)m_strObject,
SysStringByteLen(m_strObject));
return S_OK;
}
STDMETHODIMP CMyObj::put_ObjectString(BSTR newVal)
{
//Free the current string and then copy in the new value
SysFreeString(m_strObject);
m_strObject= SysAllocStringByteLen((char*)newVal,
SysStringByteLen(newVal));
return S_OK;
}
The sample also implements the custom method QueryString. This method displays a standard message box containing the current value of the ObjectString property.
The custom method was added, using the Add Method dialog box, with the following values:
The implementation of the method was completed by adding code to convert the value of the ObjectString property, for display in a message box:
STDMETHODIMP CMyObj::QueryString()
{
unsigned cmb;
if(m_strObject)
cmb= wcstombs(NULL, m_strObject, SysStringLen(m_strObject)) + 1;
else
cmb= 1;
USES_CONVERSION;
if(cmb >1)
MessageBox(NULL, W2CT(m_strObject), _T("Current value of ObjectString property"),
MB_OK);
else
MessageBox(NULL, _T("No value"), _T("Current value of ObjectString property"),
MB_OK);
return S_OK;
}
Because the property is stored as a BSTR, and the MessageBox function (reference available in the User Interface Services section of the Platform SDK) does not handle BSTRs, a conversion was needed. In the code just shown, the W2CT macro function (see "String Conversion Macros," available in the Reference section of the Visual C++ Documentation) provides this conversion.
Once the interface implementation was completed, the project was built. By default, the build step also registers the COM server object with the system. This registration allows the client to later locate and create an instance of the object for use.
To access a COM object, there must be a client of some kind. To be useful for developers learning COM programming, the client should have the following characteristics:
With these points in mind, a dialog-based application using AppWizard and the resource editor is the best choice for the MFC client.
Using the MFC AppWizard, a dialog-based project was chosen. The specific features of the dialog box project (About Box, 3D controls, and ActiveX controls), found on the Step 2 page of the MFC AppWizard, were unchecked because they are unnecessary for a simple MFC dialog-based application.
The graphical user interface for a client that interacts with a COM server object most often consists of a set of buttons and perhaps such other controls as an edit box or a slider. These controls are used to manipulate the various properties and methods of the COM object. Therefore, the properties and methods exposed by the COM server play a large factor in determining the layout. For instance, if a server exposes one or more methods, a button control can be added for each method. The related method can then be easily fired with the use of a button.
Another example would be to represent a property with some type of control that presents feedback. Such controls can range from a static text control that displays the current value of a property string to a slider control that displays the current value of a property with a fixed range. Some common examples:
MyClient, shown in Figure 1, demonstrates several controls that provide feedback.
Figure 1: The MyClient graphical interface
The user interface, added with the Visual C++ Dialog editor, consists of a read-only static text control (displaying the current value of the ObjectString property) and a button and edit box for modifying the ObjectString property. In addition, another button allows the user to invoke the QueryString method.
Once the user interface has been added, you will need to create and manipulate an instance of the MyObj server object within the client application. This is achieved by either adding the declaration of the interface to client application or using smart pointers—and the #import directive (reference available in the Visual C++ Programmer's Guide). This discussion focuses on the smart pointer approach.
From the client side of things, there are four basic steps in accessing any COM object:
COM objects are supported by a series of COM libraries. These libraries (a series of DLLs) are part of the operating system. As a developer, you cannot assume that the COM libraries are initialized for the operating system.
Therefore, the first item of business is to initialize the COM libraries in the client application. Because the client application is using MFC, use the AfxOleInit function, which initializes the COM libraries, if they have not been initialized previously. A good place to do this initialization is in the InitInstance function of the application class. InitInstance is called fairly early in the start-up of the client, and before an attempt is made to instantiate the COM object. The following code is one example of initialization:
//Init OLE libraries and support
if (!AfxOleInit())
{
AfxMessageBox("OLE initialization failed");
return FALSE;
}
The next step involves retrieving a unique identifier for the COM object you want to create. Commonly referred to as a CLSID (a 128-bit integer), this identifier specifies the COM object to be instantiated. The operating system uses this number to locate the component files, using the system registry.
An easy way to retrieve this ID is by passing the programmatic ID to the CLSIDFromProgID function (reference available in the Component Services section of the Platform SDK). This Win32 function takes the readable programmatic ID and searches the registry for the associated CLSID. If found, the CLSID is returned. This number is used later to create an instance of the COM object.
Note Using the programmatic ID of a COM object does not guarantee that the proper COM object will be found. Unlike CLSIDs, it is possible to have the same programmatic ID for different COM objects. However, in most cases it is quite adequate.
Because the client application is dialog-based, a good place for this step is in the OnInitDialog function. This function is called before the dialog is visible, allowing the application to exit quickly and communicate the encountered error (CLSID retrieval was unsuccessful) to your user. The CLSID is retrieved using the following code:
CLSID clsID;
HRESULT hr;
hr= CLSIDFromProgID(OLESTR("MySvr.Object1"), &clsID);
if(FAILED(hr))
{
AfxMessageBox("Retrieval of ProgID failed");
return FALSE;
}
In this code, the programmatic ID (in this case, MySvr.Object1) is passed to the CLSIDFromProgID function call and the return value is checked for a possible error. Once the CLSID is retrieved, the COM object can be instantiated.
The process for creating and using the ATL COM object can be simplified by making use of the #import directive. This directive provides a relatively simple and safe access to any COM object by using smart pointers. In terms of declaration and usage, a smart pointer is similar to a C++ pointer but with a few additional COM-specific advantages.
The following example imports the type library for the sample COM server application, MySvr, and prevents the use of namespaces (as discussed in "Using Namespaces and Smart Pointers") in the generated header files:
#import"D:\VS\MyProjects\MySvr\MySvr.tlb" no_namespace
This code creates two new files (of type .tli and .tlh), which are automatically included in the project. The main item of interest in these files is the declaration of the smart pointer class:
//
// Smart pointer typedef declarations
//
_COM_SMARTPTR_TYPEDEF(IMyObj, __uuidof(IMyObj));
This code sample uses a predefined macro (_COM_SMARTPTR_TYPEDEF) to create a smart pointer class (based on _com_ptr_t) called IMyObjPtr (the Ptr portion of the name is automatically appended by the compiler) and specialized from the _com_ptr_t class (reference available in the C++ Language Reference section of the Visual C++ Documentation). One useful function of the _com_ptr_t class is the CreateInstance function (the _com_ptr_t::CreateInstance reference is available in the C++ Language Reference section of the Visual C++ Documentation). This pointer is used extensively in the code of the client application.
In addition to the smart pointer class, an interface object is generated, containing type-safe member functions for accessing properties and invoking methods. The interface object follows:
IMyObj : IUnknown
{
// Property data
__declspec(property(get=GetObjectString,put=PutObjectString))
_bstr_t ObjectString;
// Wrapper methods for error-handling
_bstr_t GetObjectString ( );
void PutObjectString ( _bstr_t pVal );
HRESULT QueryString ( );
// Raw methods provided by interface
virtual HRESULT __stdcall get_ObjectString ( BSTR * pVal ) = 0;
virtual HRESULT __stdcall put_ObjectString ( BSTR pVal ) = 0;
virtual HRESULT __stdcall raw_QueryString ( ) = 0;
};
In the preceding code, IMyObj provides a data member (ObjectString, of type _bstr_t) and two member functions, GetObjectString and SetObjectString, that access the ObjectString property in a type-safe manner. These functions are discussed in "Using the COM Object."
Normally, a COM object is created with a call to another Win32 function, CoCreateInstance. This is a fairly complicated function with several parameters that need to be present. However, because the sample application has imported the type library of the COM server object, a much simpler method is available. Because the smart pointer class derives from _com_ptr_t, a powerful function is available that is an improvement on the CoCreateInstance function. The COM object is made by a call to the CreateInstance function of the smart pointer object, passing the newly retrieved CLSID of the desired COM object. The following code sample (taken from the OnInitDialog function) creates an instance of the COM server object MySvr (using the CLSID retrieved by a call to CLSIDFromProgID):
CLSID clsID;
HRESULT hr;
hr= CLSIDFromProgID(OLESTR("MySvr.MyObj"), &clsID);
if(FAILED(hr))
{
AfxMessageBox("Retrieval of ProgID failed");
return FALSE;
}
m_pMyObj.CreateInstance(clsID); //does the work of CoCreateInstance
Note that m_pMyObj (of type IMyObjPtr) is a data member of the dialog class. This is necessary so that properties and methods can be accessed from any function of the dialog class.
Now that a pointer to the COM object is available, the interface implementation of the dialog box can be completed. Basically, this consists of calling the proper member function of the COM wrapper class for the corresponding action.
For instance, in the client application there is an Invoke QueryString method button. When the user clicks this button, the QueryString method should be invoked.
The handler is completed by making a call to the IMyObj::QueryString function, as follows:
m_pMyObj->QueryString();
Note that the QueryString function is accessed through the m_pMyObj pointer of the dialog class.
To complete the implementation of the other custom button handler (the Update ObjectString value), the following code was added to the body of the function handler:
_bstr_t newbstr;
CString tmpCStr;
GetDlgItemText(IDC_NEWPROPVAL, tmpCStr);
newbstr= tmpCStr;
m_pObject1->PutObjectString(newbstr);
//Update static text with new value
BSTR tmpBStr;
m_pMyObj->GetObjectString(&tmpBStr);
_bstr_t tmpbstr(tmpBStr, FALSE);
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
This code runs when the user clicks the Update ObjectString value button. The code first retrieves the new string value from the IDC_NEWPROPVAL edit box control. This value is then formatted and passed to the PutObjectString wrapper method. The static text control (which displays the current ObjectString value) is then updated by calling the GetObjectString wrapper method. The return value is properly formatted and passed to the SetDlgItemText Win32 function.
Note As mentioned in "BSTRs and String Manipulation," the call to the _bstr_t constructor is necessary to avoid memory leaks associated with BSTR manipulation.
When the client application is finished with the COM object, the object must be destroyed and the COM libraries uninitialized.
However, in the case of the MFC client application (and the smart pointer implementation), this cleanup is handled automatically. Apart from the standard exit code supplied by the framework, nothing else is needed to exit cleanly from the client application.
One of the possible attributes that can be used with the #import directive is namespace. Namespaces are used to prevent name collisions between C++ objects. The MyClient sample, because of its simplicity, doesn't use the namespace attribute. However, because more complex applications often run into namespace collisions when using #import, the following introduction to namespaces is provided.
Let's say that a source file contains a declaration for a simple C++ class called CMyClass. Later in the development cycle, an external file is added to the project, containing another simple C++ class declaration. Unfortunately, the implementations are different, but both classes share the same name: CMyClass. Upon compilation, you now get compiler errors because of this "namespace collision."
One method of resolution is to create namespaces for one or both classes, by enclosing the declarative region for CMyClass with a namespace. For example, the following code sample creates a namespace, Original, for CMyClass:
namespace Original {
class CMyClass {
Int m_iData;
};
} //namespace Original
Because CMyClass now has a namespace, all references to this class must use an explicit qualification with the namespace name. For example, the following code creates a new CMyClass object on the stack:
Original::CMyClass class1;
In terms of importing type libraries, the namespace can be suppressed by using the no_namespace attribute. However, suppressing the namespace may lead to name collisions. The namespace can also be renamed by the rename_namespace attribute.
One important issue in COM programming is the manipulation of BSTR-type variables. There are some cases (mainly when passing or copying BSTR-type data) where BSTR-type objects can cause some problems. The simple client sample demonstrates two of these problems:
In some cases, these issues can be resolved by using a _bstr_t object. This object encapsulates the BSTR data type, automatically manages resource allocation and deallocation, and has a wider set of automatic data conversions (one of which is very helpful to the simple client application).
In the MyClient sample application (specifically the OnUpdateProp function), the current value of the ObjectString property is passed to a common MFC function, CWnd::SetDlgItemText. One of the parameters is an LPCTSTR. Unfortunately, the BSTR data type doesn't have the necessary conversion. However, _bstr_t can be converted to an LPCTSTR. This allows the following code to compile without any explicit casting of the property value:
//Update static text with new value
BSTR tmpBStr;
m_pObject1->get_ObjectString(&tmpBStr);
_bstr_t tmpbstr(tmpBStr, FALSE); //necessary to avoid a memory leak
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
Note The conversion issue does not apply in a Unicode build. However, conversion is needed in a Win32 build.
In addition to the casting issue, there is also a memory issue. If the following code sample had been used, instead of the preceding code, a memory leak would have resulted:
//Update static text with new value
BSTR tmpBStr;
m_pObject1->get_ObjectString(&tmpBStr);
_bstr_t tmpbstr;
tmpbstr= tmpBStr; //Caution: Memory leak occurs
SetDlgItemText(IDC_CURPROPVAL, tmpbstr);
The leak occurs when the tmpbstr variable is initialized. A call to SysAllocString (reference available in the Component Services section of the Platform SDK) is automatically made when creating the tmpbstr variable. This new allocation is never freed later, resulting in a memory leak. Using this version of the _bstr_t constructor avoids the issue by attaching the BSTR object to tmpbstr without a call to SysAllocString. For more information on this issue, see _bstr_t::_bstr_t (reference available in the C++ Language Reference section of the Visual C++ Documentation).
For further reading on the development of an ATL COM server and its attendant MFC client, the following articles are recommended. They can be found in the MSDN Library: