次の方法で共有


C++ and ATL

Use ATL Server Classes to Expose Your Unmanaged C++ Code as an XML Web Service

Kirk Fertitta and Chris Sells

This article assumes you're familiar with ATL, COM, and C++

Level of Difficulty123

SUMMARY

Throughout this issue, you'll read all about the promise of Web Services and how the .NET Framework enables Web Service development. Many people will also be building their Web Services atop C++ code and frameworks like ATL Server, particularly when performance is paramount. In this article, the authors show how fully functional Web Services are built using ATL Server and Visual Studio .NET. Beginning with unmanaged C++ classes, they add ATL attributes that make the code work over HTTP.

Contents

Background
Web Service Anatomy
ISAPI Extensions and ATL Server
ATL Server Request Handlers
Web Services and ATL Attributes
Conclusion

I t seems you can't swing a mouse without hitting somebody raving about the power and productivity of programming with the Microsoft® .NET Framework. Equally likely is the chance that with that same swing you'll bonk a couple of other people discussing ideas for Web Services. Indeed, such accolades for .NET are well deserved, as it promises to usher in a bona fide revolution in how Windows®-based programs are built, deployed, and used. In particular, Web Services offer a new model for how software systems are developed and shared across the Internet.

Yet amid all the fervor over .NET, many people find they need to continue developing in the unmanaged world. There are a variety of reasons for this. First of all, there is a dizzying volume of existing C++ code in use that can't simply be ported to C# or some other .NET-compatible language. Often, this legacy code relies on large class libraries that encapsulate core business logic and that support many applications. Even if the lack of workhorse C++ language features such as templates were overcome when porting such class libraries, there would be a need to port all the applications that used those libraries. .NET Interop mitigates this situation by allowing managed and unmanaged code to communicate, but crossing the boundary between .NET code and native code is not without its disadvantages. Passing complex data types can require custom marshaling code, and marshaling even moderate amounts of data can have a marked impact on performance.

So, where is the unmanaged programmer to turn? The answer is ATL Server. Developers familiar with conventional COM programming using Visual Studio® 6.0 and ATL 3.0 will find an extraordinarily empowering suite of tools and classes in the new ATL 7.0 library that comes with Visual Studio .NET. The classes that support such Web application development as building XML Web Services are collectively termed ATL Server. This article will explain how the C++ programmer can use the facilities available in ATL Server to develop high-performance Web Services quickly.

Background

Traditional Web applications allow humans to interact with a program located on a Web server, typically via a browser. By contrast, XML Web Services allow programs to interact with other programs by directly invoking methods over the Web. While distributed object technologies such as DCOM and CORBA have provided remote method invocation for years, these systems haven't worked well in heterogeneous networks. To allow for interoperability among programs running on disparate computer systems, Web Services rely on ubiquitous industry standards such as XML and HTTP. With Web Services, client programs invoke methods on remote programs by sending XML-formatted data over the wire using HTTP. The potential for programs to interact seamlessly with one another irrespective of operating system or programming language has fueled much of the excitement over this new programming paradigm. Indeed, Microsoft expects Web Services to usher in an entirely new model for building, integrating, and distributing software.

Of course, with great power comes great responsibility—wise words from Spiderman, an early pioneer in an altogether different sort of web service. To be specific, the plumbing required to support Web Services is formidable. As you might have guessed, ATL Server comes to the rescue with an impressive arsenal of library classes and wizard support—almost like our own digital Web superhero (and you thought we were finished with that one). Before you see just what we mean, let's approach Web Services from the ground up to understand how to make them work.

First, a transport protocol must be used that is capable of delivering messages to the broadest possible audience of potential Web Service clients. That means the protocol must be truly ubiquitous and it must be capable of reaching Web Services located behind firewalls. The obvious choice for this is HTTP, as virtually every computer plugged into the Internet supports it. Another thing you need is a framing protocol for messages that allows both the client invoking the Web Service and the endpoint implementing the Web Service to understand details about the call, such as input parameters, results, and error information.

SOAP is an industry-standard framing protocol developed to meet exactly this need. SOAP defines how Web Services are invoked through a series of XML message exchanges. A SOAP packet is composed of standard XML elements that specify such things as the name of the method to call on the desired Web Service, the method input and output parameters, and details about any errors that occur in processing the invocation. SOAP also describes how these XML messages are transported using HTTP, though strictly speaking, other transport protocols may be used for SOAP packets. By relying only on ubiquitous, industry-standard protocols like XML and HTTP, SOAP ensures the broadest possible reach for Web Services. If a computer is capable of reading and writing XML and it knows how to receive and send HTTP packets, then it can both provide and consume Web Services.

The next thing that must be considered is exactly how to write code in the programmer's language of choice (C++ in this case) and have this code invoked in response to an XML-based SOAP request that arrives on our machine. After all, traditional clients call methods on C++ code by pushing parameters onto a call stack and jumping to an instruction address in memory. Web Services, on the other hand, are all about messaging over the Internet, so this stack-based mechanism for "binding" client code to server code won't work. Instead, we have to parse a bunch of XML, extract a method name and its parameters, dynamically dispatch to that method, harvest the return value and any other results, package all of that into an XML response packet, and send it on its merry way back across the Internet. It's as if our worst IDispatch nightmares have returned—this time wrapped up in angle brackets.

The final thing that must be considered is how clients obtain information about a particular Web Service. In order to invoke a Web Service, the client must know the name of the methods that are available, the name and type of the parameters each method accepts, and a host of other details. To meet this need, Web Services Description Language (WSDL) was developed. WSDL plays the same role in Web Services as Interface Definition Language (IDL) plays in traditional COM programming. Specifically, WSDL documents are XML schemas that define the interface contract between the Web Service client and the endpoint implementing the Web Service. A WSDL document describes everything a caller needs to know to properly invoke a method on a Web Service endpoint.

With all of these technological hoops to jump through, it seems you might be able to walk the code to your clients on the other end of the Internet faster than you can implement the plumbing to make it happen. Let's look in detail at how you can implement the code to fulfill each of the requirements we've described. The first problem to tackle is how to capture HTTP requests that represent Web Service method calls and produce HTTP responses that represent Web Service method results. For this, we turn to our old friend, Microsoft Internet Information Services (IIS). IIS is fundamentally an HTTP server. Its purpose in life is to listen for an HTTP request, pass the request on to a piece of executable code that knows how to process it, and retrieve an HTTP response to return to the caller. IIS supports an extensibility framework known as ISAPI for registering executable code with IIS to process specific types of requests. Application code is packaged into ISAPI extension DLLs that are registered with the IIS metabase to field all HTTP requests for files with a particular file extension. Additionally, they can be invoked by the requesting DLL.

ISAPI extensions can run in-process with IIS (inetinfo.exe), so passing an HTTP request from IIS to the appropriate ISAPI extension is extremely efficient. An example of this relationship between IIS and the ISAPI extensions it houses is shown in Figure 1.

Figure 1 IIS/ISAPI Relationship

Figure 1** IIS/ISAPI Relationship **

Historically, Web developers seeking the ultimate in control, performance, and flexibility have had to implement ISAPI extensions by hand. That meant doing some pretty tedious low-level programming. ISAPI only defines three export functions that are used to communicate with IIS and, ultimately, with the Web client:

// Standard ISAPI exports extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB); extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer); extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags);

If you peer into the definition of the LPEXTENSION_CONTROL_BLOCK parameter, you'll see a pretty intimidating structure of data members and callback functions that must be used to pass information to IIS and to write response content directly to the client. Since GetExtensionVersion and TerminateExtension are used primarily for initialization and cleanup code, the single LPEXTENSION_CONTROL_BLOCK parameter to an HttpExtensionProc implementation represents a single conduit through which virtually all of your Web application logic must ultimately pass in order to produce even the simplest client response.

Web Service Anatomy

To demonstrate how a Web Service is constructed in C++ from the ground up, we will implement a currency converter service that calculates the current exchange rate between two standard currencies. The service will expose two methods, one that returns the official exchange rate, and another that returns the best exchange rate available from local currency exchange businesses. If you're planning a trip to a foreign country, you may find that exchange rates offered by different currency services can vary quite a bit. When you want to sell back your foreign currency upon returning from your trip, you may find as well that buy-back exchange rates can vary even more.

Here is the definition for the C++ class that houses the two methods we want to expose as a Web Service:

class CCurrencyConverter { public: double GetOfficialRate(LPCSTR szRefCur, LPCSTR szDesiredCur); double GetBestRate(int zip, LPCSTR szRefCur, LPCSTR szDesiredCur, LPSTR szStore); };

Our task is to somehow route incoming SOAP requests for these methods to this C++ class, which provides the implementation. The SOAP packet arrives in the body of an HTTP request that is routed to our ISAPI extension through the HttpExtensionProc function. For the GetOfficialRate method of our CCurrencyConverter Web Service, a properly formatted HTTP request would look something like the code in Figure 2.

Figure 2 HTTP Request to CCurrencyConverter

POST /currencyconverter/currencyconverter.dll HTTP/1.1 Host: localhost Content-Type: text/xml; charset=utf-8 Content-Length: 407 SOAPAction: "https://mycompany.com/GetOfficialRate" <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi=https://www.w3.org/2001/XMLSchema-instance xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <GetOfficialRate xmlns="https://mycompany.com/"> <refCurrency>US Dollar</refCurrency> <desiredCurrency>Euro</desiredCurrency> </GetOfficialRate> </soap:Body> </soap:Envelope>

SOAP is actually very simple. Without diving into the details of the specification itself, it's easy to see how SOAP packets encapsulate key pieces of information about the method you want to invoke. The name of the method to invoke (GetOfficialRate, in this case) is encapsulated in the SOAPAction HTTP header. The SOAP packet itself follows the HTTP headers and begins with the Envelope element that serves as the parent element for everything within the SOAP packet. Within the SOAP Body element, we see that the input parameters to the GetOfficialRate method are encoded simply as child elements of the GetOfficialRate element.

Similarly, we must generate an appropriately formatted HTTP response that carries the results of the method invocation back to the client. That response packet will need to look something like the code in Figure 3.

Figure 3 HTTP Response from CCurrencyConverter

HTTP/1.1 200 OK Content-Type: text/xml; charset=utf-8 Content-Length: 396 <?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema" xmlns:soap="https://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <GetOfficialRateResponse xmlns="https://mycompany.com/"> <GetOfficialRateResult>1.07</GetOfficialRateResult> </GetOfficialRateResponse> </soap:Body> </soap:Envelope>

Recall that our work in processing these SOAP packets begins and ends with the only real connection our code has to the outside world—the venerable HttpExtensionProc function. Let's review for a moment what our implementation of HttpExtensionProc must do to realize our Web Service. We must interrogate the LPEXTENSION_CONTROL_BLOCK parameter to retrieve from the request the SOAPAction header that specifies which method is to be called. Then we must parse the XML text within the SOAP packet to get the values for the method input parameters. Using this information, we dispatch the call to the correct method on our CCurrencyConverter class and then collect the return value and any output parameters. All of the resulting information, including error information if an error occurred, must be bundled into a SOAP response packet and returned to the client. That's a pretty tall order.

Let's start simple and see how much code it takes just to return the result of the method call. The HttpExtensionProc implementation to return a SOAP packet that carries a return value of 1.07 back from a call to GetOfficialRate would look something like the code in Figure 4.

Figure 4 HTTPExtensionProc for GetOfficialRate

extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK pECB) { CHAR szHeader[1024]; CHAR szStatus[] = "200 OK"; DWORD cchContent; BOOL bResult; CHAR szContent[] = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n" "<soap:Envelope \r\n" "xmlns:xsi=\"https://www.w3.org/2001/XMLSchema-instance\"\r\n" "xmlns:xsd=\"https://www.w3.org/2001/XMLSchema\"\r\n" "xmlns:soap=\"https://schemas.xmlsoap.org/soap/envelope/\">\r\n" "<soap:Body>\r\n" "<GetOfficialRateResponse xmlns=\"https://tempuri.org/\">\r\n" "<GetOfficialRateResult>1.07</GetOfficialRateResult>\r\n" "</GetOfficialRateResponse>\r\n" "</soap:Body>\r\n" "</soap:Envelope>\r\n" ; cchContent = lstrlen( szContent ); wsprintf(szHeader,"Content-Type: text/xml\r\n" "\r\n"); // Populate SendHeaderExInfo struct SendHeaderExInfo.fKeepConn = FALSE; SendHeaderExInfo.pszStatus = szStatus; SendHeaderExInfo.pszHeader = szHeader; SendHeaderExInfo.cchStatus = lstrlen(szStatus); SendHeaderExInfo.cchHeader = lstrlen(szHeader); // Send HTTP header bResult = pECB->ServerSupportFunction(pECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX, &SendHeaderExInfo, NULL, NULL); if (!bResult) return HSE_STATUS_ERROR; // Send the SOAP payload as the HTTP body bResult = pECB->WriteClient(pECB->ConnID, szContent, &cchContent, HSE_IO_SYNC); if (!bResult) return HSE_STATUS_ERROR; return HSE_STATUS_SUCCESS; }

That code is looking a bit scary already and we haven't really done any work yet. We're just writing a hardcoded response back to the client. We're not examining the HTTP request to determine which method the client wants to call and the values of the parameters to the method, and we're not even using our CCurrencyConverter class to process the request, which is the whole point of our Web Service. It's time to get some help from ATL Server.

We turn first to a very useful class the ATL Server provides for communicating directly with IIS: CServerContext. This class wraps the EXTENSION_CONTROL_BLOCK structure that we've been dealing with directly and provides convenient methods for accessing all sorts of information about the HTTP request. With this class, you can retrieve HTTP headers, MIME content type, query string information, impersonation tokens, and so on. Creating a CServerContext instance and attaching it to the EXTENSION_CONTROL_BLOCK for a request we're handling is a simple matter, as shown by the following lines:

extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK pECB) { CComObject<CServerContext>* psc = NULL; CComObject<CServerContext>::CreateInstance(&psc); psc->Initialize(pECB); }

With an initialized CServerContext in hand, we can access specialized helper classes for dealing with the request and the response explicitly. The CHttpRequest class exposes methods for extracting information about the request, such as cookies, headers, and the HTTP body content itself. The CHttpResponse is modeled as a stream, so writing result context back to the client is as simple as using the << operator. You use CHttpResponse to set the response content type (text/xml in our case), response headers, status code, and so on.

We'll modify our Web Service implementation to use the CHttpRequest class to extract the method being invoked by the client and the CHttpResponse class to return a response. This builds a bridge between messages traveling on the Internet and C++ code on our own machine. The code shown in Figure 5 will faithfully capture an incoming HTTP request for our Web Service, call our C++ code, and send back an HTTP response. Yet, before we celebrate our accomplishment, we should point out some serious inadequacies with our latest implementation. Some of the shortcomings are obvious while others will require some more detailed explanation.

Figure 5 Handling HTTP Requests and Responses

extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK pECB) { CComObject<CServerContext>* psc = NULL; CComObject<CServerContext>::CreateInstance(&psc); psc->AddRef(); psc->Initialize(pECB); CHttpRequest req; req.Initialize(psc); CCurrencyConverter converter; // our C++ class doing the real work CString strResponse; // string to use to build our response text // Method to invoke is specified in SOAPAction HTTP header CString strAction; BOOL b = req.GetServerVariable("HTTP_SOAPACTION", strAction); if (strAction == "GetOfficialRate") { LPSTR szRefCur = NULL, szDesiredCur = NULL; // Extract input parameters from XML elements in SOAP body // ... double dRate = converter.GetOfficialRate(szRefCur, szDesiredCur); // Compose a SOAP packet to return the rate in strResponse // ... } else if (strAction == "GetBestRate") { int zip = 0; LPSTR szRefCur = NULL, szDesiredCur = NULL; // Extract input parameters from XML elements in SOAP body // ... char szStore[1024]; double dRate = converter.GetBestRate(zip, szRefCur, szDesiredCur, szStore); // Compose a SOAP packet to return the rate and store in // strResponse ... } else { // Unsupported web method requested // ... compose a SOAP fault response } CHttpResponse resp; resp.Initialize(psc); resp << strResponse; // writes SOAP packet to client psc->Release(); return HSE_STATUS_SUCCESS; }

First of all, as the comments indicate, we've completely omitted the code required to parse the input parameters from the SOAP packet and compose the results into a SOAP response. Additionally, SOAP defines a standard SOAP fault element that must be properly formatted when an error occurs in executing a method on the Web Service. To handle these tasks, we must enlist the support of an XML parser and we must have knowledge of the XML schema that defines the precise format of the SOAP payload. This kind of grunge code is completely orthogonal to the actual work we're trying to perform with our CCurrencyConverter class.

As unpleasant as that may seem, this is not even the biggest problem. Consider for a moment how IIS dispatches work to our ISAPI extension in the first place. IIS has a lot of work to juggle. It must listen for HTTP requests for any number of Web Services and Web applications and route those requests to the appropriate bits of executable code. In order to maintain any semblance of responsiveness from multiple applications, IIS can't dispatch requests using a single thread. Rather, it must maintain a thread pool to efficiently dispatch requests to many ISAPI extensions. The caveat is that the IIS thread pool is fixed in size, and once the work queue serviced by the threads is full, requests are lost. This means our ISAPI extension must now implement a private thread pool to prevent burdening the IIS thread pool. That involves dealing with low-level Win32® threading APIs.

What about asynchronous processing of requests? In general, execution of a Web Service method might take a while, so we need to free up the calling thread to continue work while we process the call and return the result at some later time. This also means our ISAPI extension must be completely thread safe, which involves carefully incorporating the appropriate Win32 synchronization primitives. Oh, and did we mention that on top of all this we still have the other two standard exports to implement to complete our ISAPI extension? They are GetExtensionVersion and TerminateExtension. Critical initialization and shutdown logic is typically performed in these two functions. It's time to bring in the big guns: enter CIsapiExtension.

ISAPI Extensions and ATL Server

ATL Server encapsulates a complete ISAPI extension implementation in CIsapiExtension. With this class in hand, our ISAPI implementation becomes easier. We simply delegate all three standard ISAPI exports to a global instance of CIsapiExtension, as shown in Figure 6.

Figure 6 HTTP Request and Response Using Web Service

typedef CIsapiExtension<> ExtensionType; // The ATL Server ISAPI extension ExtensionType theExtension; // Delegate ISAPI exports to theExtension // extern "C" DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK pECB) { return theExtension.HttpExtensionProc(pECB); } extern "C" BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) { return theExtension.GetExtensionVersion(pVer); } extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) { return theExtension.TerminateExtension(dwFlags); }

This class provides a thread pool for us that queues incoming requests. CIsapiExtension also incorporates the correct synchronization logic to field requests asynchronously, and it even uses a private memory heap to allocate data to improve overall performance by avoiding the memory fragmentation that invariably occurs when the normal process heap is used. These are but a few of the features that CIsapiExtension provides. A brief look at the definition of this class reveals that it uses template parameters to allow for complete customization of core functionality.

Alternate classes may be supplied as template parameters to control such things as thread pool management, error reporting, and caching statistics. For instance, three template parameters in the following definition may be independently substituted to control various aspects of the threading policy:

template < class ThreadPoolClass=CThreadPool<CIsapiWorker>, class CRequestStatClass=CNoRequestStats, class HttpUserErrorTextProvider=CDefaultErrorProvider, class WorkerThreadTraits=DefaultThreadTraits, class CPageCacheStats=CNoStatClass, class CStencilCacheStats=CNoStatClass> class CIsapiExtension : public IServiceProvider, public IIsapiExtension, public IRequestStats { ... }

The ThreadPoolClass specifies the class to use for managing the thread pool. This class itself is parameterized by a worker class that performs the actual work in processing the request. In the previous code, CIsapiWorker is shown as the default implementation provided by ATL Server. The third class involved in implementing the extension's thread pool management is the WorkerThreadTraits. This class is responsible for actually creating the threads that will be used to service incoming requests.

The default implementation supplied by ATL Server will be adequate in many cases, but remember that it was the prospect of performance and control that drew us to ATL Server in the first place. So, it's nice to see that the framework provides this extensibility to us in a very convenient manner.

ATL Server Request Handlers

If we look at where we are in the implementation of our Web Service, it seems we've lost sight of our CCurrencyConverter class. Linking this class to the Web is the whole point of our efforts thus far. CIsapiExtension has taken care of implementing the ISAPI extension that we know we need, but how do we instruct the extension to dispatch requests to our C++ class? First of all, it's important to realize that separating the pure ISAPI logic from our Web Service implementation is a big improvement from a design perspective. With all the development investment that we might want to make in customizing CIsapiExtension, it would be really nice to have the ability to reuse the resulting implementation across multiple Web Service projects. In fact, there is nothing specific to Web Services at all in the CIsapiExtension class provided by ATL Server—the same class is used to implement ISAPI extensions for traditional Web applications as well.

Here's what we have to do to hook our CCurrencyConverter class back into the new ISAPI extension we've implemented. In order to maintain separation of our generic ISAPI logic from our Web Service implementation code, we first package our class into a standalone DLL so that the extension can load it dynamically. This is commonly referred to as our application DLL, as it houses the code specific to our particular application. We declare our methods using the __interface keyword and supply an implementation of those methods, just like a classic COM server. You can see an example of this technique in Figure 7.

Figure 7 CCurrencyConverter in ISAPI Extension

namespace CurrencyConverterService { [ uuid("23B6550E-EA52-4B0B-8E34-34D14A896292"), object ] __interface ICurrencyConverterService { [id(1)] HRESULT GetOfficialRate([in] BSTR bstrRefCur, [in] BSTR bstrDesiredCur, [out,retval] DOUBLE* pdRate); [id(2)] HRESULT GetBestRate([in] INT nZip, [in] BSTR bstrRefCur, [in] BSTR bstrDesiredCur, [out] BSTR* pbstrStore, [out,retval] DOUBLE* pdRate); }; class CCurrencyConverterService : public ICurrencyConverterService { public: HRESULT GetOfficialRate(BSTR bstrRefCur, BSTR bstrDesiredCur, DOUBLE* pdRate) { // ... return S_OK; } HRESULT GetBestRate(INT nZip, BSTR bstrRefCur, BSTR bstrDesiredCur, BSTR* pbstrStore, DOUBLE* pdRate) { // ... return S_OK; } }; // class CCurrencyConverterService } // namespace CurrencyConverterService

When a request for our application DLL arrives, CIsapiExtension will load the DLL and attempt to locate the appropriate C++ class within the DLL to handle the request. CIsapiExtension expects to find a well-known entry point called GetAtlHandlerByName that tells it which handler class to use given a particular handler string name. The signature for the GetAtlHandlerByName method we must supply looks like this:

extern "C" BOOL __declspec(dllexport) __stdcall GetAtlHandlerByName (LPCSTR szHandlerName, IIsapiExtension *pExtension, IUnknown **ppHandler) throw() { // ... tell CIsapiExension }

Fortunately, ATL provides a convenient macro for declaring the mappings between handler string names and C++ classes. The HANDLER_ENTRY macro provides a table-driven implementation of GetAtlHandlerByName that allows us to easily declare multiple handler name/class associations. We simply add the macro anywhere beneath our CCurrencyConverterService class declaration, like so:

HANDLER_ENTRY("Default", CCurrencyConverterService)

The "Default" handler string name identifies our CCurrencyConverterService as the default handler class, which means that this endpoint is accessed using only a raw URL. If our server contained more than one class that was accessed as a Web Service, then another HANDLER_ENTRY macro would be used to associate that class with another handler string name and the endpoint would be accessed using a URL, such as the following:

https://myserver/currencyservice/currencyservice.dll? Handler=NewHandler

This same mechanism is also what ATL uses to serve up the WSDL description of your Web Service. Recall that in implementing a Web Service you must provide a means for Web clients to access WSDL documents describing the details of how to connect to and invoke methods on the Web Service. Navigating to a URL like the following invokes a method on a class generated by the ATL Web Services attributes called GenCurrencyConverterWSDL that returns the full WSDL description to the client.

https://myserver/currencyservice/currencyservice.dll? Handler=GenCurrencyConverterWSDL

What CIsapiExtension expects to receive from the call to GetAtlHandlerByName is an implementation of the standard IRequestHandler interface. This interface defines methods for initialization and shutdown of the handler class as well as methods for indicating the handler's level of asynchronous support and page caching behavior.

The most important method, however, is the one that does all the work of processing the HTTP request: IRequestHandler::HandleRequest. This method is called directly by CIsapiExtension and, in the case of a Web Service, the method is responsible for parsing the SOAP packet, extracting the method name and parameter values, calling our C++ class, retrieving a result, and packaging that result into an appropriate SOAP response packet. That's some tedious work and precisely the kind of thing that ATL Server loves to do. Indeed, ATL Server provides a class called CSoapHandler that implements IRequestHandler for us and performs all of the nasty SOAP/XML processing. In classic ATL fashion, all we have to do is derive our CCurrencyConverterService class from CSoapHandler:

class CCurrencyConverterService : public ICurrencyConverterService, public CSoapHandler<CCurrencyConverterService> { ... }

The CSoapHandler implementation knows all about the proper layout of SOAP payloads and SOAP fault messages, and it even utilizes an efficient SAX parser for dealing with all of the XML processing involved in handling SOAP requests. What it doesn't know anything about, however, is the methods on our CCurrencyConverterService that it needs to call. CSoapHandler must know the full signature of each method implemented by our C++ class so that it can dispatch the call and harvest any results. We provide this information to the CSoapHandler base class by implementing the well-known GetFunctionMap method:

const _soapmap ** GetFunctionMap() throw();

The return value of this function is an array of _soapmap structures that describe all the details about each parameter to each function on our CCurrencyConverterService class. The _soapmap structure for the GetOfficialRate method would look like the following snippet:

extern __declspec(selectany) const _soapmap _GetOfficialRate_map = { ••• "GetOfficialRate", // name of function ••• _GetOfficialRate_entries, // array of structures describing // method parameters ••• 2, // method has 2 parameters 0, // index of return value ••• };

Drilling down just a bit further, we uncover the required layout of the _GetOfficialRate_entries structure that describes each parameter to the GetOfficialRate method. The arrangement of the parameters is provided by a simple structure:

struct _GetOfficialRate_struct { BSTR bstrRefCur; BSTR bstrDesiredCur; double pdRate; };

The details of each parameter within this structure are indicated with an array of _soapmapentry structures, as follows:

extern __declspec(selectany) const _soapmapentry _GetOfficialRate_entries[] = { { ••• "pdRate", // name of parameter ••• SOAPTYPE_DOUBLE, // parameter data type SOAPFLAG_RETVAL | SOAPFLAG_OUT, // this is an [out,retval] ••• }, // ... struct for bstrRefCur input parameter // ... struct for bstrDesiredCur input parameter { 0x00000000 } // marks end of array };

With all of that in place, we can finally implement the single function that CSoapHandler needs in order to call our Web Service methods. This function must also be supplied by our CCurrencyConverterClass and, as shown in Figure 8, it looks suspiciously familiar to the dispatch code we wrote by hand in HttpExtensionProc before we started using ATL Server.

Figure 8 Calling Web Service Methods

ATL_NOINLINE inline HRESULT CCurrencyConverterService::CallFunction( void *pvParam, const wchar_t *wszLocalName, int cchLocalName, size_t nItem) { wszLocalName; cchLocalName; HRESULT hr = S_OK; switch(nItem) { case 0: { _GetOfficialRate_struct *p = (_GetOfficialRate_struct *) pvParam; hr = GetOfficialRate(p->bstrRefCur, p->bstrDesiredCur, &p->pdRate); break; } case 1: { _GetBestRate_struct *p = (_GetBestRate_struct *) pvParam; hr = GetBestRate(p->nZip, p->bstrRefCur, p->bstrDesiredCur, &p->pbstrStore, &p->pdRate); break; } default: hr = E_FAIL; } return hr; }

Well, we're glad that it works and that CSoapHandler relieved the tedium of parsing XML and dealing with SOAP packets and such. But if we have to write all of that code to plug into the framework for each method we want to expose, our Web Service clients may just have to wait a bit—probably forever. By the way, we didn't have the heart to mention at the time that a matching suite of structs and maps must be implemented to support processing SOAP headers. To make any of this practical, we perform a little trust exercise together—it's time to close your eyes, lean backwards, and let ATL Server catch you. If you can do that, then you're going to love ATL attributes.

Web Services and ATL Attributes

Rather than manually defining our HANDLER_ENTRY macros and manually writing all of that code required by CSoapHandler, we're simply going to adorn our C++ class with some special attributes using an IDL-like syntax. These attributes are processed by the compiler and inject all of the code we need to turn our CCurrencyConverterService class into a fully functional Web Service. ATL attributes are extraordinarily easy to use and reduce our Web Service implementation to the code in Figure 9.

Figure 9 Using ATL Attributes

[ request_handler(name="Default"), soap_handler( name="CurrencyConverterService", namespace="urn:CurrencyConverterService", protocol="soap" ) ] class CCurrencyConverterService : public ICurrencyConverterService { public: [ soap_method ] HRESULT GetOfficialRate(BSTR bstrRefCur, BSTR bstrDesiredCur, DOUBLE* pdRate) { // ... return S_OK; } [ soap_method ] HRESULT GetBestRate(INT nZip, BSTR bstrRefCur, BSTR bstrDesiredCur, BSTR* pbstrStore, DOUBLE* pdRate) { // ... return S_OK; } }; // class CCurrencyConverterService

The request_handler attribute is required to identify this class as a handler class for HTTP requests. This attribute will automatically generate HANDLER_ENTRY macros for each class to which it is applied. The soap_handler attribute injects CSoapHandler as a base class of CCurrencyConverterService and triggers the generation of much of the grungy method-mapping code we just revealed. Applying the soap_method attribute to a method causes the compiler to inject the appropriate _soapmapentry structures and to generate an implementation of CallFunction that lets CSoapHandler invoke the method. With a few simple attributes, ATL Server has swept away a towering heap of plumbing code that has nothing to do with our business logic. We're left with a Web Service implementation that is virtually identical in simplicity to the normal C++ classes we've been writing for years.

If that 101-key text wizard we've been using so far isn't your cup of tea, then you'll be glad to learn that Visual Studio .NET provides another wizard that emits a lot of starter code for you. Select File | New | Project to bring up the New Project window. From there, select the ATL Server Web Service project type and click OK. A variety of options exist within the ATL Server Project Wizard, but the default options will be appropriate in most circumstances.

Figure 10 Solution Explorer

Figure 10** Solution Explorer **

When you click Finish, ATL Server will generate two projects for you—one for the ISAPI extension and another for the application DLL that contains the methods we want to expose as a Web Service. Solution Explorer, shown in Figure 10, displays both our CurrencyConverter project for the application DLL and our CurrencyConverterIsapi project for the ISAPI extension.

Figure 11 Setting Expand Attributed Source

Figure 11** Setting Expand Attributed Source **

If you want to see the code generated for you by the attributes we discussed, simply right-click the application project and select Properties. Under the C++ folder, the Output Files section contains a property called Expand Attributed Source. Setting this to Yes, as shown in Figure 11, will cause the compiler to generate two merge files: one, .mrg.h, showing the code injected by the compiler into the header file, and a second, .mrg.cpp, showing the code injected into the project implementation file.

Conclusion

This brief journey through a simple Web Service project has uncovered the complexity involved in realizing a fully functional XML Web Service. Yet, if you look back at how we changed our C++ to create a Web Service, you'll see that all we really did was apply two simple attributes to our class definition and one attribute to each method we wanted to expose. That's it! ATL Server took care of the Web Services protocol details, allowing us to concentrate on how to best implement a Web Service.

For related articles see:
SOAP: Using ATL Server to Build an Asynchronous SOAP Client in Unmanaged C++
ATL Server and Visual Studio .NET: Developing High-Performance Web Applications Gets Easier
The XML Files: The Birth of Web Services
C++ Attributes: Make COM Programming a Breeze with New Feature in Visual Studio .NET
System.Web.Services Namespace

For background information see:
ATL Internals by Chris Sells and Brent Rector (Addison-Wesley, 1999)
Essential COM by Don Box (Addison-Wesley, 1997)

Kirk Fertittais the Chief Technical Officer for Pacific MindWorks, a San Diego-based software engineering firm specializing in Windows-based application development, particularly COM, ATL, and .NET. Kirk is also an instructor for DevelopMentor, where he teaches courses on .NET programming and C#.

Chris Sellsis an independent consultant, specializing in distributed applications in .NET and COM. He is also an instructor for DevelopMentor. More information about Chris and his various projects is available at https://www.sellsbrothers.com.