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:
// StringLibIsapi.cpp : Defines the entry point for // the DLL application. #include "stdafx.h" // For custom assert and trace handling with WebDbg.exe #ifdef _DEBUG CDebugReportHook g_ReportHook; #endif [ module(name="MyStringLib", type=dll) ]; [ emitidl(restricted) ]; typedef CIsapiExtension<> ExtensionType; // The ATL Server ISAPI extension ExtensionType theExtension; // Delegate ISAPI exports to theExtension // extern "C" DWORD WINAPI HttpExtensionProc( LPEXTENSION_CONTROL_BLOCK lpECB) { return theExtension.HttpExtensionProc(lpECB); } extern "C" BOOL WINAPI GetExtensionVersion( HSE_VERSION_INFO* pVer) { return theExtension.GetExtensionVersion(pVer); } extern "C" BOOL WINAPI TerminateExtension(DWORD dwFlags) { return theExtension.TerminateExtension(dwFlags); }
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:
// StringLib.h ... namespace StringLibService { // all struct, enum, and typedefs for your web service // should go inside the namespace // IStringLibService - web service interface declaration // [ uuid("5CEAB050-F80B-4054-8E1B-43510E61B8CE"), object ] __interface IStringLibService { // HelloWorld is a sample ATL Server web service method. // It shows how to declare a web service method and // its in-parameters and out-parameters [id(1)] HRESULT HelloWorld([in] BSTR bstrInput, [out, retval] BSTR *bstrOutput); // TODO: Add additional web service methods here }; // StringLibService - web service implementation // [ request_handler(name="Default", sdl="GenStringLibWSDL"), soap_handler( name="StringLibService", namespace="urn:StringLibService", protocol="soap" ) ] class CStringLibService : public IStringLibService { public: // This is a sample web service method that shows how // to use the soap_method attribute to expose a method // as a web method [ soap_method ] HRESULT HelloWorld(/*[in]*/ BSTR bstrInput, /*[out, retval]*/ BSTR *bstrOutput) { CComBSTR bstrOut(L"Hello "); bstrOut += bstrInput; bstrOut += L"!"; *bstrOutput = bstrOut.Detach(); return S_OK; } // TODO: Add additional web service methods here }; // class CStringLibService } // 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:
[ uuid("5CEAB050-F80B-4054-8E1B-43510E61B8CE"), object ] __interface IStringLibService { [id(1)] HRESULT ToUpper([in] BSTR bstrInput, [out, retval] BSTR *pbstrOutput); [id(2)] HRESULT ToLower([in] BSTR bstrInput, [out, retval] BSTR *pbstrOutput ); [id(3)] HRESULT GetLength([in] BSTR bstrInput, [out, retval] long *pResult ); };
And the implementation class:
[ request_handler(name="Default", sdl="GenStringLibWSDL"), soap_handler( name="StringLibService", namespace="urn:StringLibService", protocol="soap" ) ] class CStringLibService : public IStringLibService { public: [soap_method] HRESULT ToUpper( BSTR bstrInput, BSTR *pbstrOutput ) { CComBSTR result( bstrInput ); HRESULT hr = result.ToUpper( ); if( FAILED( hr ) ) return hr; *pbstrOutput = result.Detach( ); return S_OK; } [soap_method] HRESULT ToLower( BSTR bstrInput, BSTR *pbstrOutput ) { CComBSTR result( bstrInput ); HRESULT hr = result.ToLower( ); if( FAILED( hr ) ) return hr; *pbstrOutput = result.Detach( ); return S_OK; } [soap_method] HRESULT GetLength( BSTR bstrInput, long *pResult ) { *pResult = ::SysStringLen( bstrInput ); return S_OK; } };
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:
#include <iostream> #include <string> #include <atlconv.h> using namespace std; class CoInit { public: CoInit( ) { ::CoInitialize( 0 ); } ~CoInit( ) { ::CoUninitialize( ); } }; void _tmain(int argc, _TCHAR* argv[]) { CoInit coInit; cout << "Enter a string: "; string input; getline( cin, input ); StringLibService::CStringLibService proxy; CComBSTR bstrInput( input.c_str( ) ); CComBSTR bstrToLower; HRESULT hr = proxy.ToLower( bstrInput, &bstrToLower ); if( FAILED( hr ) ) { cerr << "Call to ToLower failed with HRESULT " << hr; return -1; } cout << "This string in upper case is:" << CW2A( bstrToLower ) << endl; CComBSTR bstrToUpper; hr = proxy.ToUpper( bstrInput, &bstrToUpper ); if( FAILED( hr ) ) { cerr << "Call to ToUpper failed with HRESULT " << hr; return -1; } cout << "This string in lower case is:" << CW2A( bstrToUpper ) << endl; int length; hr = proxy.GetLength( bstrInput, &length ); if( FAILED( hr ) ) { cerr << "Call to GetLength failed with HRESULT " << hr; return -1; } cout << "This string is " << length << " characters long." << endl; }
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
SOAP Box: Why ATL Server Does Web Services Poorly
So, it's possible to implement web services—and 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 literal–style 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.