Chapter 13. Hello, ATL Server: A Modern C++ Web Platform

Throughout the previous 12 chapters, we’ve focused on ATL as a framework for building both COM components and user interfaces (via ATL’s windowing classes and ActiveX control support). Now we look at ATL as a modern platform for building web applications. That framework is a set of classes known collectively as ATL Server.

Although most of the discussion thus far has involved ATL’s COM support, ATL Server actually has very little to do with COM. Because its purpose is to handle Hypertext Transfer Protocol (HTTP) requests, the crux of ATL Server is built around Microsoft’s Internet Service API (ISAPI). The library just happens to use COM inside. We start by briefly reviewing Microsoft’s web platform. Then, we examine how ATL Server leverages that platform to provide a viable means of creating web applications and web services. There’s much more to ATL Server than can be covered in this book, so we just hit the high points in this chapter.

The Microsoft Web Platform (Internet Information Services)

When the World Wide Web began taking root in the early 1990s, many sites were little more than collections of Hypertext Markup Language (HTML) files and perhaps some image files. Clients with Internet browsers surfed to various sites using Universal Resource Locators (URLs) that took a form like this: http://www.example.com.

After typing the URL and sending the request through a maze of routers, the request finally ended up at a server somewhere. In the earliest days of the Web, the server was probably a UNIX box. Web technologies soon evolved to handle more elaborate requests (not just file requests). Through the development of the Common Gateway Interface and the inclusion of HTML tags representing standard GUI controls (such as combo boxes, push buttons, and text boxes), the Web became capable of handling interactive traffic. That is, a client could carry on a two-way conversation with web servers that extended beyond simple file requests. Of course, this capability led to the great upsurge in web sites during the late 1990s, giving rise to such enterprises as Amazon.com and Google.

The earliest means of supporting interactive capabilities over the Web were made available through the Common Gateway Interface (CGI). CGI worked by handling separate incoming requests with a new process for each request. Although the Microsoft platform supports the CGI, it doesn’t work quite as well as on a UNIX box. Starting new processes on a UNIX box is not as expensive as starting new processes on a Windows box. To compensate, Microsoft introduced the Internet Services API and its own new Internet server: Internet Information Services (IIS). The IIS strategy is that it’s much faster to load a DLL to respond to an HTTP request than it is to start a whole new process.

IIS and ISAPI

The IIS component is the heart of Microsoft’s web platform. Most modern web sites built around the Microsoft platform involve IIS and ISAPI DLLs. All the modern Microsoft web-based programming frameworks are extensions of the IIS/ISAPI architecture. Even classic ASP and the more modern ASP.NET rely on IIS and ISAPI at their core. And as you’ll soon see, ATL Server depends upon IIS and ISAPI as well.

Regardless of the programming framework used (raw sockets programming, ASP, ASP.NET, or ATL Server), processing web requests is similar from framework to framework. A component listens to port 80 for HTTP requests. When a request, comes in, the component parses the request and figures out what to do with it. The request might vary from sending back some specific HTML, to returning a graphics file, or perhaps even to invoking a method of some sort.

When programming to the modern Microsoft web-based frameworks, IIS is the component that listens to port 80. IIS handles some requests directly and delegates others to specific ISAPI extension DLLs to execute the request. By default, IIS handles requests for standard HTML files directly. As an alternative, a custom file extension might be mapped to a handwritten ISAPI DLL that executes the request.

Classic ASP and ASP.NET integrate into IIS as ISAPI DLLs. IIS handles requests ASP files (*.asp) by mapping them to an ISAPI DLL named ASP.DLL, which handles the request by parsing the request string, loading the ASP file, parsing the contents of the file, and servicing the request according to the instructions given in the ASP file. ASP.NET files (*.aspx) are handled by an ISAPI DLL named ASPNET_ISAPI.DLL, which brings in the Common Language Runtime to help it process requests.

To set the stage for understanding ATL Server, let’s take a look at how ISAPI extension DLLs work.

ISAPI Extension DLLs

Although IIS does a perfectly fine job responding to requests for standard web file types (such as HTML and JPG), its real power lies in the capability to extend your server by writing custom DLLs to respond to requests.

The core ISAPI infrastructure is actually fairly simple. ISAPI extension DLLs implement three entry points:

1BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer);
2
3DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB);
4
5BOOL WINAPI TerminateExtension(DWORD dwFlags);

These three methods are the hooks for writing your web site using custom DLLs. GetExtensionVersion is called when IIS invokes the application for the first time. GetExtensionVersion must set the version number in the HSE_VERSION_INFO structure passed in and then return trUE for IIS to be capable of using your ISAPI DLL.

IIS calls the TerminateExtension entry point when IIS is ready to unload the DLL from its process. If you don’t need to do any cleanup, the TerminateExtension entry point is optional.

The heart of the extension is the HttpExtensionProc function. Notice that HttpExtensionProc takes a single parameter: an EXTENSION_CONTROL_BLOCK structure. The structure includes everything you’d ever want to know about a request, including the kind of request made, the content of the request, the type of content, a method for getting the server variables (for example, information about the connection), a method for writing output to the client, and a method for reading data from the body of the HTTP request. Here’s the EXTENSION_CONTROL_BLOCK:

 1typedef struct _EXTENSION_CONTROL_BLOCK {
 2  DWORD cbSize;             // size of this struct.
 3  DWORD dwVersion;          // version info of this spec
 4  HCONN ConnID;             // Context number not to be modified!
 5  DWORD dwHttpStatusCode;   // HTTP Status code
 6  CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; // null terminated log
 7                        // info specific to this Extension DLL
 8
 9  LPSTR lpszMethod;         // REQUEST_METHOD
10  LPSTR lpszQueryString;    // QUERY_STRING
11  LPSTR lpszPathInfo;       // PATH_INFO
12  LPSTR lpszPathTranslated; // PATH_TRANSLATED
13  DWORD cbTotalBytes;       // Total bytes indicated from client
14  DWORD cbAvailable;        // Available number of bytes
15  LPBYTE lpbData;           // pointer to cbAvailable bytes
16  LPSTR lpszContentType;    // Content type of client data
17
18  BOOL (WINAPI * GetServerVariable)(
19    HCONN hConn, LPSTR lpszVariableName,
20    LPVOID lpvBuffer, LPDWORD lpdwSize);
21
22  BOOL (WINAPI * WriteClient)(HCONN ConnID, LPVOID Buffer,
23    LPDWORD lpdwBytes, DWORD dwReserved);
24
25  BOOL (WINAPI * ReadClient)(HCONN ConnID, LPVOID lpvBuffer,
26    LPDWORD lpdwSize);
27
28  BOOL (WINAPI * ServerSupportFunction)(HCONN hConn,
29    DWORD dwHSERequest, LPVOID lpvBuffer, LPDWORD lpdwSize,
30    LPDWORD lpdwDataType);
31} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

When IIS receives a request, it packages the information into the EXTENSION_CONTROL_BLOCK and passes a pointer to the structure into the ISAPI DLL via the HttpExtensionProc entry point. The ISAPI extension’s job is to parse the incoming request into a useable form. After that, it’s completely up to the ISAPI DLL to do whatever it wants to with the request. For example, if the client makes some sort of request using query parameters (perhaps a product lookup), the ISAPI DLL might use those parameters to create a database query. The ISAPI DLL passes any results back to the client using the pointer to the WriteClient method passed in through the extension block.

If you’ve had any experience working with frameworks such as classic ASP or ASP.NET, most of this structure will appear familiar to you. For example, when you call Write through ASP’s intrinsic Response object, execution eventually ends up passing through the method pointed to by WriteClient.

The Simplest ISAPI Extension That Could Possibly Work [1]

Let’s take a look at a simple “Hello, World” ISAPI extension. The result looks like Figure 13.1.

Figure 13.1. Hello World, ISAPI style

[View full size image]

_images/13atl01.jpg

Writing the Extension

Starting with a Win32 DLL project (not an ATL DLL project) named HelloISAPI, including the HttpExt.h system header file in stdafx.h brings in the ISAPI extension types, which enables us to implement the GetExtensionVersion function:

1BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer) {
2  pVer->dwExtensionVersion = HSE_VERSION;
3  strncpy_s( pVer->lpszExtensionDesc,
4    HSE_MAX_EXT_DLL_NAME_LEN,
5    "Hello ISAPI Extension", _TRUNCATE );
6  return TRUE;
7}

All I did here was set the ISAPI version number (HSE_VERSION is a constant defined in HttpExt.h) and fill in the description field for use by IIS. We use the HSE_VERSION constant because we’re reporting the version of the ISAPI API that we’re using, not the version number of our extension DLL. This way, as IIS and ISAPI changes, IIS can maintain backward-compatibility with older extensions.

The meat of the extension is the HttpExtensionProc function:

 1DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) {
 2  char *header =
 3    "<html>"
 4      "<head>"
 5        "<title>Hello from ISAPI</title>"
 6      "</head>"
 7      "<body>"
 8        "<h1>Hello from an ISAPI Extension</h1>";
 9
10DWORD size = static_cast< DWORD >( strlen( header ) );
11pECB->WriteClient( pECB->ConnID, header, &size, 0 );
12
13char *intro = "<p>Your name is: ";
14size = static_cast< DWORD >( strlen( intro ) );
15pECB->WriteClient( pECB->ConnID, intro, &size, 0 );
16  size = static_cast< DWORD >(
17    strlen( pECB->lpszQueryString ) );
18  pECB->WriteClient( pECB->ConnID, pECB->lpszQueryString,
19    &size, 0 );
20
21  char *footer =
22        "</p>"
23      "</body>"
24    "</html>";
25
26  size = static_cast< DWORD >( strlen( footer ) );
27  pECB->WriteClient( pECB->ConnID, footer, &size, 0 );
28
29  return HSE_STATUS_SUCCESS;
30}

This method simply builds up a series of strings and then sends them out to the browser via the pECB->WriteClient() method. We also grab the query string (the stuff in the URL after the ?) by using the lpszQueryString field in the EXTENSION_CONTROL_BLOCK.

In this case, the TerminateExtension method doesn’t need to do anything and, thus, is technically optional, but adding a minimal stub for completeness looks like this:

1BOOL WINAPI TerminateExtension(DWORD dwFlags) {
2  return TRUE;
3}

Returning trUE tells IIS that it can unload this extension. If we had loaded anything that needed to be cleaned up, the call to TerminateExtension would be the place to do it.

There’s one more thing we need to add to the project before we can deploy it: We have to export the ISAPI entry points. Normal practice is to add the __declspec(dllexport) declaration to each of the ISAPI functions; this tells the compiler that we want the suitably decorated function to be exported from the DLL. Unfortunately, when you do this with an ISAPI extension, you get this:

 1c:\helloisapi\helloisapi.cpp(24)  : error C2375: 'GetExtensionVersion' :
 2redefinition; different linkage
 3        c:\program files\microsoft visual studio
 48\vc\platformsdk\include\httpext.h(526) : see declaration of
 5'GetExtensionVersion'
 6c:\helloisapi\helloisapi.cpp(33)  : error C2375: 'HttpExtensionProc' :
 7redefinition; different linkage
 8        c:\program files\microsoft visual studio
 98\vc\platformsdk\include\httpext.h(527) : see declaration of
10'HttpExtensionProc'
11c:\helloisapi\helloisapi.cpp(65)  : error C2375: 'TerminateExtension' :
12redefinition; different linkage
13        c:\program files\microsoft visual studio
148\vc\platformsdk\include\httpext.h(528) : see declaration of
15'TerminateExtension'

If we look inside HttpExt.h on lines 526528, we see the problem:

1/************************************************************
2*    Function Prototypes
3*    o  for functions exported from the ISAPI Application DLL
4************************************************************/
5
6
7BOOL  WINAPI   GetExtensionVersion( HSE_VERSION_INFO *pVer );
8DWORD WINAPI   HttpExtensionProc( EXTENSION_CONTROL_BLOCK *pECB );
9BOOL  WINAPI   TerminateExtension( DWORD dwFlags );

The HttpExt.h file includes prototypes for the ISAPI entry points, but alas, the prototypes don’t include __declspec(dllexport). When the compiler sees the prototype without the export directive and the actual implementation with the directive, it gives the error I just showed you.

To work around this, we need to do exports the old-fashioned way: Add a .def file to the project. This is a separate file that explicitly lists the functions we want exported from our DLL. As the number of exports grows, a .def file gets difficult to maintain, but we’ve got only three exports here, and they’re the same for every ISAPI extension, so it’s not too much of a burden.

The .def file looks like this:

1;HelloISAPI.def  : Declares the module parameters for the DLL.
2LIBRARY    "HelloISAPI"
3
4EXPORTS
5   HttpExtensionProc
6   GetExtensionVersion
7   TerminateExtension

Configuring the Server

Next, we need to get the extension onto the server. Luckily, Visual Studio has some web-deployment tools built in to help, but when you’re starting from scratch, they’re not set up yet.

First things first: We need an IIS Virtual Directory. I recommend creating a directory that’s separate from your build directory for this (you’ll see why in a moment). The Internet Services Manager is a nice place to create virtual directories; the sample is called HelloISAPI. [2]

Next, we need to configure permissions on the directory so that the ISAPI extension will be executed and, more important, so we can debug it. In Internet Services Manager, right-clicking the HelloISAPI virtual directory and choosing Properties yields Figure 13.2.

Figure 13.2. Virtual directory properties

_images/13atl02.jpg

We need to make sure two settings are correct in this dialog box. The first is the Execute Permissions drop-down list, which must be set to Scripts and Executables. (It defaults to Scripts Only.) Without this setting, instead of loading the extension and executing it, IIS attempts to download the DLL to your browser.

The second setting is the Application Protection drop-down list. For easy debugging, you want this set to Low. This causes IIS to load an extension directly into the IIS process. This is generally a bad thing to do on a production server because misbehaving extensions can take the IIS process down with it, but for debugging, it just makes things easier. Do this only on a development server, not on your production boxes! [3]

Configuring the Project

With a virtual directory created and configured to receive our ISAPI extension code, we want to configure Visual Studio to deploy the code on a successful build. In the project’s property pages, there’s an entry called Web Deployment (see Figure 13.3).

Figure 13.3. Web Deployment settings

[View full size image]

_images/13atl03.jpg

By default, the Excluded From Build property is set to Yes, which means that web deployment won’t happen. Changing it to No enables web deployment, which, by default, uses the name of the project at the IIS virtual directory name that will receive the result of a successful buildin this example, our HelloISAPI.DLL file.

At this point, compiling the project and surfing to http://localhost/HelloISAPI/HelloISAPI.dll?SomeName should get back a result that looks like Figure 13.1. If you make changes to the project and rebuild, VS is smart enough to tell ISAPI to unload our DLL so that it can be replaced. This is also the reason that we wanted a separate build directory from our virtual directory. If we compile straight into the virtual directory, IIS typically has the DLL locked. You would need to use a prebuild step to shut down IIS so that Visual Studio could regenerate the .dll file after it compiles. By using web deployment, you don’t need to pay the compile-time speed hit for resetting IIS until the compile succeeds.

If you click F5 to start a debugging session, by default, you get the helpful Executable for Debug Session dialog box. We want to configure Visual Studio to automatically attach to IIS when debugging. To do this, navigate to the Debugging page of the project properties and set the options to match those shown in Figure 13.4.

Figure 13.4. Debugging settings

[View full size image]

_images/13atl04.jpg

You need to change these two fields:

  • Command. Set this to inetinfo.exe, which is the name of the IIS process.

  • Attach. Set this to Yes. If it is set to No, the debugger attempts to start a second copy of inetinfo.exe. Yes means “Attach to already running copy.”

Now, to debug your extension, set your breakpoints wherever you want and click F5 to start the debugger. Visual Studio crunches for a while after it attaches to IIS and then looks like it’s doing nothing. At this point, start a browser and point it at your extension’s URL; your breakpoints should fire.

Wrapping ISAPI

You now have a simple extension written directly to ISAPI, but programming against raw ISAPI is rather awkward. It sure would be nice to have some more object-oriented wrappers. Luckily, ATL Server provides exactly that. The CServerContext class is a COM object that provides a wrapper around the ECB.

 1class CServerContext :
 2    public CComObjectRootEx<CComMultiThreadModel>,
 3    public IHttpServerContext {
 4public:
 5    BEGIN_COM_MAP(CServerContext)
 6        COM_INTERFACE_ENTRY(IHttpServerContext)
 7    END_COM_MAP()
 8
 9    void Initialize(__in EXTENSION_CONTROL_BLOCK *pECB);
10
11    LPCSTR GetRequestMethod();
12    LPCSTR GetQueryString();
13    LPCSTR GetPathInfo();
14    LPCSTR GetScriptPathTranslated();
15    LPCSTR GetPathTranslated();
16    DWORD GetTotalBytes();
17    DWORD GetAvailableBytes();
18    BYTE *GetAvailableData();
19    LPCSTR GetContentType();
20    BOOL GetServerVariable(
21        LPCSTR pszVariableName,
22        LPSTR pvBuffer,
23        DWORD *pdwSize);
24
25    BOOL WriteClient(void *pvBuffer, DWORD *pdwBytes);
26    BOOL AsyncWriteClient(void *pvBuffer, DWORD *pdwBytes);
27    BOOL ReadClient(void *pvBuffer, DWORD *pdwSize);
28    BOOL AsyncReadClient(void *pvBuffer, DWORD *pdwSize);
29
30    BOOL SendRedirectResponse(__in LPCSTR pszRedirectUrl);
31    BOOL GetImpersonationToken(__out HANDLE * pToken);
32
33    BOOL SendResponseHeader(
34        LPCSTR pszHeader = "Content-Type: text/html\r\n\r\n",
35        LPCSTR pszStatusCode = "200 OK",
36        BOOL fKeepConn=FALSE);
37
38    BOOL DoneWithSession(__in DWORD dwHttpStatusCode);
39    BOOL RequestIOCompletion(__in PFN_HSE_IO_COMPLETION pfn,
40        DWORD *pdwContext);
41
42    BOOL TransmitFile(
43        HANDLE hFile,
44        PFN_HSE_IO_COMPLETION pfn,
45        void *pContext,
46        LPCSTR szStatusCode,
47        DWORD dwBytesToWrite,
48        DWORD dwOffset,
49        void *pvHead,
50        DWORD dwHeadLen,
51        void *pvTail,
52        DWORD dwTailLen,
53        DWORD dwFlags);
54
55    BOOL AppendToLog(LPCSTR szMessage, DWORD *pdwLen);
56
57    BOOL MapUrlToPathEx(LPCSTR szLogicalPath, DWORD dwLen,
58        HSE_URL_MAPEX_INFO *pumInfo);
59};

The CServerContext class provides a somewhat more convenient wrapper for the ISAPI interface. Before you use it, you need to add an ATL module object to your project and initialize it in DLL main. This code takes care of the housekeeping:

 1// Our module class definition
 2class CHelloISAPI2Module :
 3  public CAtlDllModuleT<CHelloISAPI2Module> {};
 4
 5// Required global instance of module.
 6CHelloISAPI2Module _AtlModule;
 7
 8// Initialize / shutdown module on DLL load or unload
 9BOOL APIENTRY DllMain( HMODULE hModule,
10  DWORD ul_reason_for_call, LPVOID lpReserved ) {
11  // Set up shared data for use by CServerContext
12  return _AtlModule.DllMain(ul_reason_for_call, lpReserved);
13}

Having initialized shared data that CServerContext makes use of, we can now take advantage of CServerContext:

 1DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) {
 2  CComObject< CServerContext > *pCtx;
 3  CComObject< CServerContext >::CreateInstance( &pCtx );
 4  pCtx->Initialize( pECB );
 5  // We use this smart pointer to ensure proper cleanup
 6  CComPtr< IUnknown > spContextUnk( pCtx );
 7
 8  char *header =
 9    "<html>"
10      "<head>"
11        "<title>Hello from ISAPI</title> "
12      "</head>"
13      "<body>"
14        "<h1>Hello from an ISAPI Extension</h1>";
15
16  DWORD size = static_cast< DWORD >( strlen( header ) );
17  pCtx->WriteClient( header, &size );
18
19  char *intro = "<p>Your name is: ";
20  size = static_cast< DWORD >( strlen( intro ) );
21  pCtx->WriteClient( intro, &size );
22
23  size = static_cast< DWORD >(
24    strlen( pCtx->GetQueryString( ) ) );
25  pCtx->WriteClient(
26    const_cast< LPSTR >(pCtx->GetQueryString( )), &size );
27
28  char *footer =
29      "\r\n</body>\r\n"
30  "</html>\r\n";
31
32  size = static_cast< DWORD >( strlen( footer ) );
33  pCtx->WriteClient( footer, &size );
34
35  return HSE_STATUS_SUCCESS;
36}

The bold lines show where we’ve replaced direct calls to the ECB with calls via the context. The WriteClient method is at least a little shorter, but we actually haven’t gained much at this point. Luckily, there’s more API wrapping to be done.

Request and Response Wrappers

Anyone who has done ASP or ASP.NET development is familiar with the Request and Response objects. The former gives access to the contents of the HTTP request: URL, query string, cookies, and so on. The latter gives you a way to write data to the output stream. ATL Server provides similar wrappers that make both reading and writing from the ECB much more pleasant.

The CHttpRequest object is a wrapper object that lets you get at the contents of the HTTP request:

 1class CHttpRequest : public IHttpRequestLookup {
 2public:
 3  // Constructs and initializes the object.
 4  CHttpRequest(
 5    IHttpServerContext *pServerContext,
 6    DWORD dwMaxFormSize=DEFAULT_MAX_FORM_SIZE,
 7    DWORD dwFlags=ATL_FORM_FLAG_NONE);
 8
 9  CHttpRequest(IHttpRequestLookup *pRequestLookup);
10
11  // Access to Query String parameters as a collection
12  const CHttpRequestParams& GetQueryParams() const;
13
14  // Get the entire raw query string
15  LPCSTR GetQueryString();
16
17  //... Other methods omitted - we'll talk about them later
18}; // class CHttpRequest

The CHttpRequest object gives you easy access to everything in the request. Using the CHttpRequest object simplifies getting at query string variables. In the raw C++ extension code, I cheated and just passed data (such as ?Chris), to make using that data easier. However, typical web pages take named query parameters separated with the ampersand (&) and potentially containing special characters encoded in hex:

1http://localhost/HelloISAPI2/HelloISAP2.dll?name=Chris&motto=I%20Love%20ATL

Unfortunately, decoding and parsing query strings in their full glory requires quite a bit of work to get right, which is where ATL Server really starts to shine via the CHttpRequest class:

 1DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) {
 2  CComObject< CServerContext > *pCtx;
 3  CComObject< CServerContext >::CreateInstance( &pCtx );
 4  pCtx->Initialize( pECB );
 5  // We use this smart pointer to ensure proper cleanup
 6  CComPtr< IUnknown > spContextUnk( pCtx );
 7
 8  CHttpRequest request( pCtx );
 9
10  char *header =
11    "<html>"
12      "<head>"
13        "<title>Hello from ISAPI</title>"
14      "</head>"
15      "<body>"
16        "<h1>Hello from an ISAPI Extension</h1>";
17
18  DWORD size = static_cast< DWORD >( strlen( header ) );
19  pCtx->WriteClient( header, &size );
20
21  CStringA name;
22  name = request.GetQueryParams( ).Lookup( "name" );
23  if( name.IsEmpty( ) ) {
24    char *noName = "<p>You didn't give your name";
25    size = static_cast< DWORD >( strlen( noName ) );
26    pCtx->WriteClient( noName, &size );
27  }
28  else {
29    char *intro = "<p>Your name is: ";
30    size = static_cast< DWORD >( strlen( intro ) );
31    pCtx->WriteClient( intro, &size );
32    size = name.GetLength( );
33    pCtx->WriteClient( name.GetBuffer( ), &size );
34  }
35
36  char *footer =
37        "\r\n</p>\r\n"
38      "\r\n</body>\r\n"
39    "</html>\r\n";
40
41  size = static_cast< DWORD >( strlen( footer ) );
42  pCtx->WriteClient( footer, &size );
43
44  return HSE_STATUS_SUCCESS;
45}

The bold lines show where we’re calling into the CHttpRequest object and parsing the named query parameter name. If it’s encoded with embedded spaces or other special characters, or if it’s included with other query parameters, ATL Server parses it correctly.

One minor thing that might seem odd is the use of CStringA in the previous code sample. Why didn’t I just use the plain CString? If you look in the documentation for CHttpRequestParams::Lookup (the method I used to get the query string values), you’ll see this:

1class CHttpRequestParams : ... {
2  ...
3  LPCSTR Lookup(LPCSTR szName) const;
4  ...
5};

Notice that the return type is LPCSTR, not LPCTSTR. In general, when dealing with strings coming in or going out via HTTP, ATL Server explicitly uses 8-bit ANSI character strings.

The CString class is actually a string of TCHARs. As you recall from Chapter 2, “Strings and Text,” this means that the actual type of the underlying characters changes depending on your compile settings for Unicode. However, the return type for the Lookup method never changes: It’s always ANSI. So, the sample code uses CStringA explicitly to say, “I don’t care what the UNICODE compile setting is, I always want this to be 8-bit characters.”

On the output side, ATL Server provides the CHttpResponse class:

 1class CHttpResponse :
 2  public IWriteStream,
 3  public CWriteStreamHelper {
 4public:
 5  // Implementation: The buffer used to store the
 6  // response before the data is sent to the client.
 7  CAtlIsapiBuffer<> m_strContent;
 8
 9  // Numeric constants for the HTTP status codes
10  // used for redirecting client requests.
11  enum HTTP_REDIRECT {
12    HTTP_REDIRECT_MULTIPLE=300,
13    HTTP_REDIRECT_MOVED=301,
14    HTTP_REDIRECT_FOUND=302,
15    HTTP_REDIRECT_SEE_OTHER=303,
16    HTTP_REDIRECT_NOT_MODIFIED=304,
17    HTTP_REDIRECT_USE_PROXY=305,
18    HTTP_REDIRECT_TEMPORARY_REDIRECT=307
19  };
20
21  // Initialization
22
23  CHttpResponse();
24  CHttpResponse(IHttpServerContext *pServerContext);
25  BOOL Initialize(IHttpServerContext *pServerContext);
26  BOOL Initialize(IHttpRequestLookup *pLookup);
27
28  // Writing to the output
29
30  BOOL SetContentType(LPCSTR szContentType);
31  HRESULT WriteStream(LPCSTR szOut, int nLen, DWORD *pdwWritten);
32  BOOL WriteLen(LPCSTR szOut, DWORD dwLen);
33
34  // Redirect client to another URL
35  BOOL Redirect(LPCSTR szUrl,
36    HTTP_REDIRECT statusCode=HTTP_REDIRECT_MOVED,
37    BOOL bSendBody=TRUE);
38  BOOL Redirect(LPCSTR szUrl, LPCSTR szBody,
39    HTTP_REDIRECT statusCode=HTTP_REDIRECT_MOVED);
40
41  // Manipulate various response headers
42
43  BOOL AppendHeader(LPCSTR szName, LPCSTR szValue);
44  BOOL AppendCookie(const CCookie *pCookie);
45  BOOL AppendCookie(const CCookie& cookie);
46  BOOL AppendCookie(LPCSTR szName, LPCSTR szValue);
47  BOOL DeleteCookie(LPCSTR szName);
48
49  void ClearHeaders();
50  void ClearContent();
51
52  // Send the current buffer to the client
53  BOOL Flush(BOOL bFinal=FALSE);
54  ... other methods omitted for clarity
55  }
56}; // class CHttpResponse

The CHttpResponse class provides all the control you need to manipulate the response going back to the client. But the best thing about CHttpResponse isn’t in this class definitionit’s in the base class, CWriteStreamHelper:

 1class CWriteStreamHelper {
 2public:
 3  ... Other methods omitted for clarity
 4
 5  CWriteStreamHelper& operator<<(LPCSTR szStr);
 6  CWriteStreamHelper& operator<<(LPCWSTR wszStr);
 7  CWriteStreamHelper& operator<<(int n);
 8  CWriteStreamHelper& operator<<(short int w);
 9  CWriteStreamHelper& operator<<(unsigned int u);
10  CWriteStreamHelper& operator<<(long int dw);
11  CWriteStreamHelper& operator<<(unsigned long int dw);
12  CWriteStreamHelper& operator<<(double d);
13  CWriteStreamHelper& operator<<(__int64 i);
14  CWriteStreamHelper& operator<<(unsigned t64 i);
15  CWriteStreamHelper& operator<<(CURRENCY c);
16};

These operator<< overloads mean that CHttpResponse can be used much like C++ iostreams can, and they make it much, much easier to build up your output. Using CHttpResponse, our final HttpExtensionProc now looks like this:

 1DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB) {
 2  CComObject< CServerContext > *pCtx;
 3  CComObject< CServerContext >::CreateInstance( &pCtx );
 4  pCtx->Initialize( pECB );
 5  // We use this smart pointer to ensure proper cleanup
 6  CComPtr< IUnknown > spContextUnk( pCtx );
 7
 8  CHttpRequest request( pCtx );
 9  CHttpResponse response( pCtx );
10
11  response << "<html>" <<
12    "<head>" <<
13      "<title>Hello from ISAPI</title>" <<
14    "</head>" <<
15    "<body>" <<
16      "<h1>Hello from an ISAPI Extension</h1>";
17
18  CStringA name;
19  name = request.GetQueryParams( ).Lookup( "name" );
20  if( name.IsEmpty( ) ) {
21    response << "<p>You didn't give your name";
22  } else {
23    response << "<p>Your name is: " << name;
24  }
25
26  response << "</p>" <<
27      "</body>" <<
28    "</html>";
29
30  return HSE_STATUS_SUCCESS;
31}

This use of the CHttpResponse object gives us a much simpler programming model. All those ugly casts are gone, and we don’t need to worry about presizing anything.

The use of the CServerContext, CHttpRequest, and CHttpResponse objects does make the basics of request processing easier, but other issues haven’t yet been addressed. IIS does some subtle work with threading, and if you don’t handle threads properly, you’ll destroy your server’s performance. There’s also dealing with form fields and form validation, something every web app has to do. Finally, does it really make sense that you have to recompile your C++ code every time you want to change some HTML?

To address these issues, let’s take a look at how ATL Server implements the ISAPI extension.

ATL Server

ISAPI DLLs provide the necessary interface to build web applications, but ISAPI is an extremely low-level way to work. It forces the developer to manage every detail of the web application, even if the requirement is just to slap a username on a “Hello, World” HTML page. ATL Server is a set of classes that simplify the development of ISAPI DLLs in the same style that the rest of ATL simplifies COM development: reasonable implementations of default functionality, and the capability to override those defaults efficiently when needed.

Figure 13.5 presents a very high-level view of the typical ATL Server application architecture.

Figure 13.5. ATL Server architecture

[View full size image]

_images/13atl05.jpg

The ATL Server architecture divides your web application into three parts:

  • An ISAPI extension DLL

  • Server Response Files (.SRF), also referred to as stencils

  • Response DLLs

These three parts neatly divide up the details of implementing a web application. At the front of the stack is the ISAPI DLL. This is responsible for two things: interfacing with IIS (by implementing the ISAPI functions) and dispatching the incoming requests to the appropriate handler (more about handlers later).

In most cases, your application needs to return HTML that is a combination of static and dynamic content. ATL Server provides a custom extension mapping, .SRF, to a Server Response File. These files contain static text that’s sent directly to the client, along with substitution markers that are evaluated at runtime to generate the dynamic content.

The Response DLLs contain the code that evaluates the substitution markers, and this is where your application logic typically resides.

Another way to look at the architecture is this: The ISAPI DLL handles the plumbing. The SRF files are your presentation. The Response DLLs contain your business logic. Each of these concerns is separated so that you work on each individually as needed.

Hello ATL Server

I next rebuilt the Hello ISAPI using ATL Server directly. The first step was to create a new ATL Server project (as shown in Figure 13.6).

Figure 13.6. Creating an ATL Server project

[View full size image]

_images/13atl06.jpg

The first page of the wizard is the usual “Welcome to the wizard” stuff. The next page, Project Settings (see Figure 13.7), is where things get interesting.

Figure 13.7. ATL Server Project Settings page

[View full size image]

_images/13atl07.jpg

The Project Settings page lets you choose what the wizard generates. By default, you get two projects: an ISAPI extension DLL (much like the one I already wrote) and a web application DLL that contains the logic that drives the .srf file. This page also specifies what the virtual directory should be (and the wizard automatically creates and configures the virtual directory for us, saving a manual step).

The next page, Server Options (see Figure 13.8), lets you turn on various ATL Server services that the ISAPI DLL provides.

Figure 13.8. ATL Server project Server Options page

[View full size image]

_images/13atl08.jpg

Each of the check boxes in the Server Options page corresponds to a service that ATL Server provides. One of the more important services is caching, and ATL Server provides several kinds of caches. The Blob Cache check box adds a service that lets you cache arbitrary chunks of memory (Binary Large OBjects) in memory in the web server. The File Cache check box adds a service that manages data that is stored in temporary files. The Data Source Cache check box adds a service that stores opened OLE DB connection objects. This way you can open the connection once and then just reuse it out of the cache, saving the cost of opening the connection on each request.

Session state is something else that most web applications need but that HTTP doesn’t support. Checking the Session Services check box lets you add one of the two session-state implementations in ATL Server. The OLE DB-Backed Session-State Services uses a database (accessed via OLE DB) to store the session information. If a database is undesirable, you can instead choose Memory-Backed Session-State Services, which stores the session state in memory on the web server. This is much faster to access, but because the data is stored in the web server, it’s not accessible from other machines; more specifically, if you’ve got a web farm, in-memory session state will be a problem.

The Predefined Performance Counters option causes the ISAPI DLL to automatically update Windows performance counters for the number of accesses, pages per second, and other such statistics.

The Browser Capabilities Support option turns on ATL Server’s support for identifying the client’s browser, which lets you, for example, output inline JavaScript only to browsers that support it.

I don’t need any of the server options for this particular project, so I left everything unchecked.

The next page, Application Options (see Figure 13.9), lets you specify options for the generated application DLL.

Figure 13.9. ATL Server Application Options page

[View full size image]

_images/13atl09.jpg

Validation Support adds a skeleton implementation of the ValidateAndExchange() method to the request-handler class. This method is called once before the .srf file is processed; this is the place to put input validation code.

Stencil Processing Support provides a skeleton REPLACEMENT_MAP in the code and a sample replacement method. The stencil processor uses this map in the class declaration to find the proper method to execute when it hits a replacement token in the .srf file.

Create as Web Service adds a web service handler to the project instead of a regular HTML request handler (we discuss this one later).

The remaining two options affect the generated sample .srf file. The Use Locale and Use Codepage check boxes add a sample locale and codepage directive to the .srf file.

In the sample app I’m writing, the defaults are appropriate.

The final page, Developer Support (see Figure 13.10), gives a couple miscellaneous options that affect several files in the generated code.

Figure 13.10. ATL Server Developer Support page

[View full size image]

_images/13atl10.jpg

Generate TODO Comments tells the wizard to add TODO comments to the code to mark spots where you’ll want to change things. Attributed Code tells the wizard to output code with ATL attributes. Custom Assert and Trace Handling Support adds extra code to the debug build of the project that allows trace messages to be output to the WinDbg debugger. Again, the defaults are fine for what I need for the sample.

After clicking Finish and waiting for Visual Studio to crunch, I had a solution containing two projects. The first contained the code for the new ISAPI extension. This project was already configured with an appropriate .def file to export the ISAPI methods and set up for web deployment.

The ISAPI DLL implementation was as follows:

 1// HelloAtlServerIsapi.cpp : Defines the entry point
 2// for the DLL application.
 3
 4
 5#include "stdafx.h"
 6// For custom assert and trace handling with WebDbg.exe
 7#ifdef _DEBUG
 8CDebugReportHook g_ReportHook;
 9#endif
10
11class CHelloAtlServerModule
12    : public CAtlDllModuleT<CHelloAtlServerModule> { };
13
14CHelloAtlServerModule _AtlModule;
15
16typedef CIsapiExtension<> ExtensionType;
17
18// The ATL Server ISAPI extension
19ExtensionType theExtension;
20
21
22// Delegate ISAPI exports to theExtension
23
24extern "C"
25DWORD WINAPI HttpExtensionProc(LPEXTENSION_CONTROL_BLOCK lpECB) {
26  return theExtension.HttpExtensionProc(lpECB);
27}
28
29extern "C"
30BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO* pVer) {
31  return theExtension.GetExtensionVersion(pVer);
32}
33
34extern "C"
35BOOL WINAPI TerminateExtension(DWORD dwFlags) {
36  return theExtension.TerminateExtension(dwFlags);
37}
38
39#ifdef _MANAGED
40#pragma managed(push, off)
41#endif
42
43// DLL Entry Point
44
45extern "C"
46BOOL WINAPI DllMain(HINSTANCE hInstance,
47  DWORD dwReason, LPVOID lpReserved) {
48  hInstance;
49  return _AtlModule.DllMain(dwReason, lpReserved);
50}
51#ifdef _MANAGED
52#pragma managed(pop)
53#endif

The ISAPI DLL provides the implementation of the three ISAPI exports, but the implementations here don’t do any work. Instead, they forward the calls to a global instance of CIsapiExtension, which is the class that implements all the ISAPI plumbing.

The #pragma managed(push, off) line is there in case you’re using managed C++. The DllMain enTRy point must be in native code for Windows to properly call it at load time. The #pragma turns managed code off the DllMain function, and then the second #pragma managed(pop) turns managed code back on. The mixture of managed and native code is beyond the scope of this book, but it is nice to know that the ATL engineers considered it.

For this particular project, I don’t need to touch the ISAPI DLL. The interesting part is in the Application DLL project. This project contains two important things. The first is the HelloAtlServer.srf file:

 1<html>
 2{{// use MSDN's "ATL Server Response File Reference" to
 3learn about SRF files.}}
 4{{handler HelloAtlServer.dll/Default}}
 5<head>
 6</head>
 7<body>
 8This is a test: {{Hello}}<br>
 9</body>
10</html>

Much as ASP files are HTML with the addition of <% %> markers, .srf files are HTML with the addition of {{ }} markers that indicate that substitutions should be performed at that point. Two kinds of substitution markers exist: directives (such as the {{hander.. }} marker) that control the execution of the stencil engine, and substitution markers that indicate where text should be replaced. Additionally, comments are also available (the {{// }} form) and are handy for putting notes into your code that don’t get sent back to clients in the HTML.

To make this project’s page match the output from my previous one, I edited the default .srf file to look like this instead:

 1<html>
 2{{handler HelloAtlServer.dll/Default}}
 3<head>
 4  <title>Hello from ATL Server</title>
 5</head>
 6<body>
 7<h1>Hello from ATL Server</h1>
 8{{if NameGiven}}
 9<p>Your name is {{Name}}
10{{else}}
11<p>You didn't give your name!
12{{endif}}
13</p>
14</body>
15</html>

This .srf file uses both flow control (the if block) and substitution (the Name block). An .srf file is connected to request handler classes via the handler marker. In this file, the handler marker says that the request-handler class is in the Hello-AtlServer.dll file, and it’s marked with the name Default. The HelloAtlServer.cpp file shows how the name Default is mapped to an actual C++ class:

1// HelloAtlServer.cpp
2...
3
4BEGIN_HANDLER_MAP()
5    HANDLER_ENTRY("Default", CHelloAtlServerHandler)
6END_HANDLER_MAP()

The HANDLER_MAP() in an ATL Server project serves the same purpose as the OBJECT_MAP() in an ATL project; [4] it maps a key (in this case, the handler name) to a specific class.

The CHelloAtlServer class is where we actually put the substitutions. The default version output from the wizard looks like this:

 1// HelloAtlServer.h : Defines the ATL Server
 2// request handler class
 3
 4#pragma once
 5
 6class CHelloAtlServerHandler
 7  : public CRequestHandlerT<CHelloAtlServerHandler> {
 8public:
 9  BEGIN_REPLACEMENT_METHOD_MAP(CHelloAtlServerHandler)
10    REPLACEMENT_METHOD_ENTRY("Hello", OnHello)
11  END_REPLACEMENT_METHOD_MAP()
12
13  HTTP_CODE ValidateAndExchange() {
14    // TODO: Put all initialization and validation code here
15
16    // Set the content-type
17    m_HttpResponse.SetContentType("text/html");
18
19    return HTTP_SUCCESS;
20  }
21
22protected:
23  // Here is an example of how to use a replacement
24  // tag with the stencil processor
25  HTTP_CODE OnHello(void) {
26    m_HttpResponse << "Hello World!";
27    return HTTP_SUCCESS;
28  }
29};

The REPLACMENT_METHOD_MAP() macros give the mappings between the markers in the .srf file and the actual methods in the class that get called at substitution time.

The class also includes the ValidateAndExchange() method. This method gets called once at the start of page processing, and this is the place where you can process and validate your inputs.

To implement our “Hello, World” page, we need to first check to see if a name parameter was passed on the query string, pull out the name, and provide implementations for the NameGiven and Name substitutions:

 1class CHelloAtlServerHandler
 2  : public CRequestHandlerT<CHelloAtlServerHandler> {
 3
 4public:
 5  BEGIN_REPLACEMENT_METHOD_MAP(CHelloAtlServerHandler)
 6    REPLACEMENT_METHOD_ENTRY("NameGiven", OnNameGiven)
 7    REPLACEMENT_METHOD_ENTRY("Name", OnName)
 8  END_REPLACEMENT_METHOD_MAP()
 9
10  HTTP_CODE ValidateAndExchange() {
11    m_name = m_HttpRequest.GetQueryParams( ).Lookup( "name" );
12    return HTTP_SUCCESS;
13  }
14protected:
15  HTTP_CODE OnNameGiven( ) {
16    if( m_name.IsEmpty( ) ) {
17      return HTTP_S_FALSE;
18    }
19      return HTTP_SUCCESS;
20  }
21
22  HTTP_CODE OnName( ) {
23    m_HttpResponse << m_name;
24    return HTTP_SUCCESS;
25  }
26
27private:
28  // Storage for the name in the query string
29  CStringA m_name;
30};

The m_name member stores the name retrieved from the query string. To retrieve it, I used the m_HttpRequest member provided by the CRequestHandlerT base class. This is an instance of the CHttpRequest class that has already been initialized by the time ValidateAndExchange is called.

The OnName method is called when the {{Name}} substitution is hit in the .srf file. It uses the m_HttpResponse member from the base class, which is a ready-to-go instance of CHttpResponse. It simply writes the name to the output. All successful substitution methods must return HTTP_SUCCESS, or the processing will be aborted with an error.

The OnNameGiven method is slightly different. This is used from the {{if ...}} block and is used to control which chunk of HTML is actually output from the .srf file. For a method that must return true or false, true is indicated by an HTTP_SUCCESS return code. HTTP_S_FALSE [5] indicates a false value. Any other return value aborts the processing. The HTTP code returned to the client depends on the actual return value. HTTP_FAIL is probably the most common; its returns an HTTP code 500 (Server Error).

Because the wizard enables web deployment, compiling the project automatically deploys it as well. Pressing F5 causes Visual Studio to automatically attach to IIS, and brings up a browser showing the result of my .srf file processing [6] (see Figure 13.11). Tweaking the URL by adding ?name=Chris%20Tavares gave the result in Figure 13.12.

Figure 13.11. SRF processing, no name on query string

[View full size image]

_images/13atl11.jpg

Figure 13.12. SRF processing with name on query string

[View full size image]

_images/13atl12.jpg

So, what does the wizard-generated ATL Server project buy us? First, it saves a lot of effort in configuring the project: setting up virtual directories, configuring web deployment, and setting up for debugging. Next, we get a clean separation between the ISAPI plumbing and the business logic. ATL Server provides a high-performance implementation of ISAPI applications out of the box, one that would take a great deal of effort to do correctly by hand. Finally, we get a good separation of presentation and logic. We no longer need to recompile just to tweak the HTML; all we need to do is change the .srf file and redeploy that file to the web server.

ATL Server also gave me something that is rather subtle but very important. Consider the URL that was used. For the “by hand” project, the URL was this:

1http://localhost/HelloISAPI/HelloISAPI.dll

For the ATL Server project, the URL was this:

1http://localhost/HelloAtlServer/HelloAtlServer.srf

Notice that the first URL references the DLL directly, whereas the second one goes to the .srf file. The big deal here is simply this: In the home-grown version, if I want multiple web pages, I need to provide the logic to map URLs (or query strings) to particular pages myself. ATL Server, on the other hand, automatically uses the appropriate DLLs when you reference an .srf file. If you want a new page, just drop in a new .srf file. The ATL Server Project Wizard configured the .srf extension to map to the ISAPI DLL, as shown in Figure 13.13.

Figure 13.13. IIS extension mappings for an ATL Server project

_images/13atl13.jpg

IIS automatically routes any request for an .srf file in this virtual directory to the ISAPI extension; at that point, ATL Server takes over and routes the request to the correct .srf file and request handlers.

Web Services in ATL Server

ATL Server does more than generate HTML: It generates XML as well. More specifically, you can use ATL Server to implement web services using the same ISAPI infrastructure it uses to generate HTML pages.

Whenever I look at a new web services stack (which has happened quite frequently in recent years), I like to start by building a simple service that will convert strings to upper- or lowercase, and that will return the length of a string. This lets me concentrate on the plumbing without worrying much about the implementation.

I started by creating a new ATL Server project. Visual Studio shows two icons on the New Project dialog box (see Figure 13.6) for ATL Server. I used the ATL Server project icon in the previous example. It turns out that the two project wizards are almost identical. The only difference between the two is the Create as Web Service check box on the Application Options page (see Figure 13.9). With the ATL Server Web Service project, this check box is on by default.

One other important difference exists. The marshalling code between XML and C++ is complicated and requires a lot of code to be generated. Instead of swamping your projects with generated code that’s hard to edit and update, the ATL Server team built the code generation into ATL attributes. This means that, for all practical purposes, web services are the only part of ATL that requires the use of attributed code. When the Create as Web Service check box is set, the Developer Support page (see Figure 13.10) has the Attributed check box set and disabled. [7]

I created a project called StringLib for my new web service and got the expected ISAPI and Application projects, just like my previous one. The ISAPI project contained this code:

 1// StringLibIsapi.cpp : Defines the entry point for
 2// the DLL application.
 3#include "stdafx.h"
 4// For custom assert and trace handling with WebDbg.exe
 5#ifdef _DEBUG
 6CDebugReportHook g_ReportHook;
 7#endif
 8[ module(name="MyStringLib", type=dll) ];
 9[ emitidl(restricted) ];
10
11typedef CIsapiExtension<> ExtensionType;
12// The ATL Server ISAPI extension
13ExtensionType theExtension;
14// Delegate ISAPI exports to theExtension
15//
16extern "C" DWORD WINAPI HttpExtensionProc(
17  LPEXTENSION_CONTROL_BLOCK lpECB) {
18  return theExtension.HttpExtensionProc(lpECB);
19}
20
21extern "C" BOOL WINAPI GetExtensionVersion(
22  HSE_VERSION_INFO* pVer) {
23  return theExtension.GetExtensionVersion(pVer);
24}
25
26extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) {
27  return theExtension.TerminateExtension(dwFlags);
28}

The module attribute generates the type definition for the ATL module class. The emitidl attribute is used to prevent anything in that file after that point from going into the generated IDL file. The rest of the file is pretty much identical to the unattributed version. As with the HelloATLServer project, I didn’t need to touch the ISAPI extension.

The interesting stuff ended up in the StringLib.h file in the application DLL project:

 1// StringLib.h
 2...
 3namespace StringLibService {
 4// all struct, enum, and typedefs for your web service
 5// should go inside the namespace
 6
 7// IStringLibService - web service interface declaration
 8//
 9[ uuid("5CEAB050-F80B-4054-8E1B-43510E61B8CE"),
10  object ]
11__interface IStringLibService {
12  // HelloWorld is a sample ATL Server web service method.
13  // It shows how to declare a web service method and
14  // its in-parameters and out-parameters
15  [id(1)] HRESULT HelloWorld([in] BSTR bstrInput,
16    [out, retval] BSTR *bstrOutput);
17  // TODO: Add additional web service methods here
18};
19  // StringLibService - web service implementation
20  //
21  [ request_handler(name="Default", sdl="GenStringLibWSDL"),
22    soap_handler(
23      name="StringLibService",
24      namespace="urn:StringLibService",
25      protocol="soap" ) ]
26  class CStringLibService :
27    public IStringLibService {
28  public:
29    // This is a sample web service method that shows how
30    // to use the soap_method attribute to expose a method
31    // as a web method
32    [ soap_method ]
33    HRESULT HelloWorld(/*[in]*/ BSTR bstrInput,
34      /*[out, retval]*/ BSTR *bstrOutput) {
35      CComBSTR bstrOut(L"Hello ");
36      bstrOut += bstrInput;
37      bstrOut += L"!";
38      *bstrOutput = bstrOut.Detach();
39
40      return S_OK;
41    }
42    // TODO: Add additional web service methods here
43}; // class CStringLibService
44
45} // namespace StringLibService

This default demonstrates quite well how you build web services with ATL Server. You start with an IDL interface and specify the inputs and outputs using various COM types. [8] Then, you create a class that implements that interface and add the request_handler and soap_handler attributes to tell the ATL Server plumbing that this is a web service implementation. Finally, you implement the interface methods, decorating each one with the soap_method attribute to wire up the incoming XML requests to the appropriate methods.

So, I removed the sample code, and created my interface:

 1[ uuid("5CEAB050-F80B-4054-8E1B-43510E61B8CE"),
 2  object ]
 3__interface IStringLibService {
 4  [id(1)] HRESULT ToUpper([in] BSTR bstrInput,
 5    [out, retval] BSTR *pbstrOutput);
 6  [id(2)] HRESULT ToLower([in] BSTR bstrInput,
 7    [out, retval] BSTR *pbstrOutput );
 8  [id(3)] HRESULT GetLength([in] BSTR bstrInput,
 9    [out, retval] long *pResult );
10};

And the implementation class:

 1[ request_handler(name="Default", sdl="GenStringLibWSDL"),
 2  soap_handler(
 3    name="StringLibService",
 4    namespace="urn:StringLibService",
 5    protocol="soap"
 6  ) ]
 7class CStringLibService :
 8  public IStringLibService {
 9public:
10
11  [soap_method]
12  HRESULT ToUpper( BSTR bstrInput, BSTR *pbstrOutput ) {
13    CComBSTR result( bstrInput );
14    HRESULT hr = result.ToUpper( );
15    if( FAILED( hr ) ) return hr;
16    *pbstrOutput = result.Detach( );
17    return S_OK;
18  }
19
20  [soap_method]
21  HRESULT ToLower( BSTR bstrInput, BSTR *pbstrOutput ) {
22    CComBSTR result( bstrInput );
23    HRESULT hr = result.ToLower( );
24    if( FAILED( hr ) ) return hr;
25    *pbstrOutput = result.Detach( );
26    return S_OK;
27  }
28
29  [soap_method]
30  HRESULT GetLength( BSTR bstrInput, long *pResult ) {
31    *pResult = ::SysStringLen( bstrInput );
32    return S_OK;
33  }
34};

Aside from the attributes, this code wouldn’t look out of place in any COM server implementation.

To use the web service in question, you need a WSDL file. ATL Server automatically generates the WSDL; it’s accessible at http://localhost/StringLib/StringLib.dll?Handler=GenStringLibWSDL.

Consuming a Web Service in C++

After my web service was deployed, I wanted to test it. You can call most web services from almost every language. Because this is a C++ book, I created a C++ client. Luckily, Visual Studio includes a code generator that makes it fairly easy to make calls on a web service.

I started by creating a Win32 Console application in Visual Studio. To bring in the web service, I used the Add Web Service feature of Visual Studio to read the WSDL file from the web service and generate a proxy class [9] that wraps access to the web service. The rest of the C++ code simply collected some input and called the web service:

 1#include <iostream>
 2#include <string>
 3#include <atlconv.h>
 4
 5using namespace std;
 6
 7class CoInit {
 8public:
 9  CoInit( ) { ::CoInitialize( 0 ); }
10  ~CoInit( ) { ::CoUninitialize( ); }
11};
12
13void _tmain(int argc, _TCHAR* argv[]) {
14  CoInit coInit;
15
16  cout << "Enter a string: ";
17  string input;
18  getline( cin, input );
19
20  StringLibService::CStringLibService proxy;
21
22  CComBSTR bstrInput( input.c_str( ) );
23  CComBSTR bstrToLower;
24  HRESULT hr = proxy.ToLower( bstrInput, &bstrToLower );
25  if( FAILED( hr ) ) {
26    cerr << "Call to ToLower failed with HRESULT " << hr;
27    return -1;
28  }
29  cout << "This string in upper case is:" <<
30    CW2A( bstrToLower ) << endl;
31
32  CComBSTR bstrToUpper;
33  hr = proxy.ToUpper( bstrInput, &bstrToUpper );
34  if( FAILED( hr ) ) {
35    cerr << "Call to ToUpper failed with HRESULT " << hr;
36    return -1;
37  }
38  cout << "This string in lower case is:" <<
39    CW2A( bstrToUpper ) << endl;
40
41  int length;
42  hr = proxy.GetLength( bstrInput, &length );
43  if( FAILED( hr ) ) {
44    cerr << "Call to GetLength failed with HRESULT " << hr;
45    return -1;
46  }
47  cout << "This string is " << length <<
48  " characters long." << endl;
49}

The lines in bold show the calls to the web service. Calling via the proxy acts much like a call into a COM object: All calls return an hrESULT, the actual return value is given via an out parameter, and there are special rules for memory management (which are explained in the MSDN documentation). Because the web service client proxy uses the Microsoft XML 3.0 Server XMLHTTP object by default, I needed to initialize COM before making the calls.

This code simply calls all three of the methods in the web service. Figure 13.14 shows the result of running the test harness.

Figure 13.14. Output from Web Service test client

[View full size image]

_images/13atl14.jpg

SOAP Box: Why ATL Server Does Web Services Poorly

So, it’s possible to implement web servicesand do so in a way that’s fairly comfortable to COM developers. What’s not to like? A lot, as it turns out.

When using ATL Server Web Services, you have absolutely no control over the actual XML that gets sent out. ATL Server maps your COM interface into RPC-encoded SOAP; you can’t hand it an XML schema. You can’t even tweak the WSDL after the fact. You’re stuck with what ATL Server gives you.

Now, perhaps you’re thinking that this isn’t too bad. Maybe you’re defining a new service and don’t need to conform to an existing XML schema. Unfortunately, you might be in for problem even then.

SOAP uses two basic styles to map the XML in the SOAP document to the underlying programming model. ATL Server uses the RPC encoding. This is a set of rules defined in the SOAP specification that say how an integer, string, array, and so on is represented in XML. This encoding does not use the XML Schema definition (although it does use the XSD type system) because the XSD specification wasn’t finished when the SOAP spec shipped.

You would think that the RPC style would be sufficient, but the SOAP spec was sufficiently ambiguous that different vendors implemented RPC encoding in different and incompatible ways. With ATL Server adopting RPC encoding, you’re in for interop trouble.

The other option is to use document-literal encoding. With doc literal, you simply treat the body of the SOAP document as XML. This turns out to be much more flexible and interoperable than RPC encoding. In fact, in the current SOAP 1.2 specification, [10] Section 5 (which defines RPC encoding) is completely optional. More toolkits are moving toward doing doc literalstyle web services and are leaving out RPC encoding altogether. [11]

As a result, I simply can’t recommend using ATL Server Web Services in any serious application. It might be useful if you have existing C++ code that has to be exposed as a web service now, now, now! But for anything that needs to interop with other environments, you’re better off choosing a web service layer that supports more modern web services styles such as doc-literal and XML Schema.

Summary

ATL Server is an extension of ATL that enables you to write ISAPI applications and bring your C++ skills to the World Wide Web. An ATL Server project consists of an ISAPI DLL that implements the ISAPI entry points for IIS, a set of .``srf`` files that include the HTML to send back to the client, and an application DLL that provides request handlers that fill in the substitutions in the .srf files.

The ATL Server ISAPI implementation provides you with a built-in thread pool for high-performance web applications. ATL Server also provides optional caching mechanisms to further improve performance.

An ATL Server request handler is used to validate the incoming requests and (in concert with the .srf files) generate the output. ATL Server provides the CHttpRequest class that wraps the HTTP request. CHttpRequest provides access to query string parameters, form variables, cookies, and a whole lot more. CHttpResponse wraps the output and gives you the capability to easily generate output to the client in the style of a C++ iostream.

ATL Server can also be used for quick web service wrappers around existing COM code.

In the next chapter, we take a look at how ATL Server does all this under the hood.