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.
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. 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 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.
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, 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.
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.
|