C/C++ Users Journal May 2004
In my article "Integrating XML Web Services with VB6 Applications" (Dr. Dobb's Journal, February 2004), I examined how you can consume XML web services in legacy applications by means of the Microsoft SOAP Toolkit. While the SOAP Toolkit is suitable for wrapping most existing COM components as XML web services, in some cases you may have to modify the original COM component to make such wrapping possible and the resulting XML web service usable to the extent close to that of the original COM component. Therefore, in some cases, it may be feasible to write a custom XML web-service wrapper for a COM component that allows full control over state management and data type conversion. In this article, I present a streamlined approach for wrapping C++ COM components as XML web services by means of ATL Server web-service projects.
When implementing custom XML web-service wrappers for COM components, you must take into consideration the following important issues:
These considerations must be applied not only to the XML web-service wrapper, but also to the COM component itself when the situation permits modifying the underlying COM interface. In fact, when the source COM component can be modified, it is possible to arrive at an interface that can be used effectively both by COM and web-service clients with the latter being facilitated by the XML web-service wrapper that you are about to write.
The approach I present in this article involves developing an XML web-service wrapper using the ATL Server project. In this case, the source COM component is presumed mutable, and the following modifications are effected by the object's interface:
To illustrate this approach, consider that, in Listing 1, the interface originally contained a read-only ReleaseDate property, which was replaced with the GetReleaseDate() method. The DATE data type is preserved in the method because it will be possible to implement an efficient and seamless conversion in the web-service wrapper code (which is not the case for methods operating on arrays of UDTs with DATE fields). Also, the ToString() and InitFromString() hidden methods are added to the interface to support serialization and deserialization of the object state to/from character data.
The actual COM object implementing the interface supports the COM error interface ISupportErrorInfo by means of the support_error_info attribute (Listing 2). Once the support_error_info attribute is applied to the class definition, you can call the CComCoClass::Error() method to provide meaningful error information (Listing 3). It is the task of the web-service wrapper to query the IErrorInfo interface to extract error information when COM component method calls fail (for example, return E_FAIL error code).
The next step is to create a C++ ATL Server web-service project. You should check the Generate Combined DLL box on the Project Settings page of the ATL Project Wizard dialog to generate a Web Service DLL combining ATL Server and ISAPI extension functionality in the same file. Also, check the Sessions Service box on the Server Options page to enable support for session storage. You can use either OLEDB or memory-backed session storage. While memory-backed session storage is more efficient performance wise, OLEDB-backed session storage can provide permanent persistence and seamless support for load balancing. When multiple instances of the web service are hosted on multiple web servers, all of them can access shared OLEDB session storage and it would not matter which physical web server answers a particular web-method request. The same cannot be easily achieved with memory-backed session storage as it is local and private to each particular web server.
Once you have created an ATL Server web-service project, you should extend the web-service interface to mimic your COM component interface as closely as possible. For example, for the IUltraMax interface, the web-service interface in Listing 4 is proper. Note the difference in declaration for the GetSongs() method returning the array of UDTs, which, in the COM interface case, is defined as:
HRESULT GetSongs([out] LONG* Size, [out, retval, size_is(, *Size)] SongInfo** Songs);
and in the case of the web service interface is defined as:
HRESULT GetSongs([out] LONG* Size, [out, retval, size_is(*Size)] SongInfo** Songs);
Also, the GetReleaseDate() method in the web-service interface now employs the BSTR data type rather than the COM DATE data type for date representation. In the web-service wrapper, it is necessary to replace the DATE data type in all web methods with BSTR data types to ensure smooth date conversion between XML and COM for the reasons just outlined. Lastly, ToString() and InitFromString() COM interface methods are internal, and need not be exposed in the web-service interface.
Listing 5 is the corresponding class implementing the web-service interface. The InitializeHandler() method was injected by the Application Wizard, however, the session-handling code required uncommenting.
Besides injected code, the web-service class contains these custom methods and properties:
struct MyHeader {
BSTR m_SessionID;
};
Although you may be inclined to define m_Header as BSTR rather than UDT, this approach will prove futile when consuming the resulting web service in .NET clients such as C# or VB.NET. It turns out that the .NET web-service client interface represented by the .NET Framework SoapHttpClientProtocol class always expect SOAP headers to be represented by a complex type (such as UDT) and will throw an exception when the SOAP header is represented by a primitive type such as an XML string (COM BSTR). Also notice that the m_Header member appears in the soap_header attribute applied to each web method to ensure that the session ID information is transmitted with each web-method call. Naturally, the LogOn() method does not require session ID information on input because the main purpose of the method is to create a new session for authenticated users. Therefore, the required parameter of the soap_header attribute applied to the LogOn() method is set to False while all other web methods require the SOAP header information to succeed.
Now the groundwork for the web service is laid and the only thing that remains is to implement web methods corresponding to the wrapped component COM interface methods. To streamline the process of repetitious duplication of COM interface method calls, I have defined the following macro:
#define COM_CALL(obj_method) \ HRESULT hr = Prolog(); \ if ( FAILED(hr) ) return hr; \ hr = obj_method; \ return Epilog(hr); \
With the help of the COM_CALL macro, you can wrap a COM method call in a single line of code; for instance:
HRESULT CUltraMaxService::LogOn(BSTR sLoginID, BSTR sPassword)
{
COM_CALL(m_pUltraMax->raw_LogOn(sLoginID, sPassword));
}
With the COM_CALL macro, the majority of wrapped COM methods have a simple and clean appearance with the exception of methods that require data type conversion, such as COM DATE to XML dateTime conversion; see Listing 10. The GetReleaseDate() web-method code performs such conversion relying on the ATL COleDateTime class to perform dateTime parsing and formatting to ensure smooth integration with .NET web-service consumers.
Now the web-service wrapper code is complete and the resulting web service can be consumed in legacy applications (with the help of the SOAP Toolkit) or in managed .NET clients.
There are two tricks worth mentioning that should be applied to the generated Web Reference classes when consuming the resulting web-service wrapper in managed .NET clients:
void GetReleaseDate([SoapElement(DataType = "date")] ref DateTime CurrentDate)
The SoapElement(DataType="date") attribute is applied to the CurrentDate parameter to ensure that only the date portion of the CurrentDate parameter is formatted and parsed as an XML date data type rather than dateTime, which is critical for correct date parsing and formatting in the web-service wrapper code (which relies on the ATL COleDateTime class for this purpose). In this way, the web method definition in the web-service client code matches the original COM interface method definition in the sense that the actual native date data types are employed in both places. The only place where the date representation is temporarily textual is in web-service wrapper code, which, in the case of C++ ATL Server projects, does not support XML date/dateTime data types directly and has to resort to manual date conversion between SOAP/XML and COM.
Now, the Web Reference class is ready for using in managed code. Keep in mind that you should be catching SoapExceptions as those may contain error information returned by the underlying COM component.
The approach I've presented here provides a straightforward mechanism for exposing existing C++ COM components for managed and unmanaged web-service consumers while providing maximum usability of the original COM component and requiring little change to the original COM interface. The complete code samples illustrating this approach are available at http://www.cuj.com/code/.