George V. Reilly
Senior Software Engineer, MicroCrafts, Inc.
April 2, 1997
George V. Reilly is a Senior Software Engineer at MicroCrafts, Inc., a Redmond, WA –based consulting firm. He wrote many of the IIS Sample Components for Active Server Pages.
This article tells you how to write and when to use an Active Server Pages (ASP) component with the Microsoft Active Template Library (ATL). It assumes that you're familiar with C++, know a little about Component Object Model (COM) and ActiveX™, and have a basic understanding of how ASP works.
Why would you want to bother writing C++ components for your Web server now that ASP is an integral part of Microsoft® Internet Information Server (IIS) version 3.0? Surely you can throw away all of those laboriously written Internet Server Application Programming Interface (ISAPI) extension dynamic-link libraries (DLLs) and Common Gateway Interface (CGI) programs and just whip up a concoction of Hypertext Markup Language (HTML) and Microsoft Visual Basic® Scripting Edition (VBScript) in a tenth of the time?
Yes and no. It's certainly true that you can replace many ISAPI extension DLLs and CGI programs with ASP scripts that are easier to write, easier to customize, and easier to update, but there is still a place for C++ programs on your Web server.
VBScript and Microsoft JScript™ are powerful and useful, but they have disadvantages, too. Here are a few reasons why you might want to use languages other than VBScript or JScript:
However, do not underestimate the usefulness of VBScript. You can produce working ASP applications using VBScript in a fraction of the time that it takes to write C++ code, and they'll be good enough most of the time. The edit/compile/debug cycle is notably shorter, plus it's much easier to tweak the look of your pages.
A few points you should bear in mind when deciding whether to write a component:
Which language should you use to write a component?
Note: With Active Server Pages, a pure free-threaded object will not perform as well as a both-threaded object (an object marked as both apartment-threaded and free-threaded) or an apartment-threaded object.
The Microsoft Active Template Library (ATL) is used to build COM objects that can be called from an ASP page, from Visual Basic, or from other Automation clients.
ATL is the recommended library for writing ASP and other ActiveX components in C++ for the following reasons:
ATL versions 2.0 and 2.1 were released in mid-February 1997. ATL 2.0, for Microsoft Visual C++® version 4.2, is available for download at http://www.microsoft.com/visualc/prodinfo/. ATL 2.1 is an integral part of Visual C++ 5.0.
ATL 2.0 requires Visual C++ 4.2b. If you are using Visual C++ 4.2, you must upgrade to Visual C++ 4.2b with the Visual C++ 4.2b Technology Update. Download the Visual C++ Technology Update from http://www.microsoft.com/visualc/prodinfo/archives/download/vc42b.htm. Note that this update will work only with Visual C++ version 4.2.
To create a new component:
If you're worried about 8.3 names, be sure that the base name of your project is no more than six characters, as IDL will generate Project_i.c, Project_p.c, ProjectPS.def, and ProjectPS.mak.
Now that you've created the project, it's time to create a COM object within the project.
Let's build a really simple component called Upper. It has one method, ToUpper, which takes a string and converts it to uppercase. For the sake of this example, we'll use Upper1 as the short name of the component.
To create a method that returns a value to VBScript, make the return value be the last parameter to the method and declare it as [out, retval].
If you're using Visual C++ 4.2, put the following in your Upper.idl file, in the interface IUpper1 : IDispatch block:
[helpstring("Convert a string to uppercase")]
HRESULT ToUpper([in] BSTR bstr,
[out, retval] BSTR* pbstrRetVal);
If you're using Visual C++ 5.0, right-click IUpper1 in the ClassView pane and click Add Method . . .. Type ToUpper as the method name and include the following in the parameters:
[in] BSTR bstr,
[out, retval] BSTR* pbstrRetVal
Click Attributes . . . to change the helpstring. When you click OK, appropriate code will be added to your .idl, .h, and .cpp files. Of course, you still need to add the body of the ToUpper method, as shown below.
In Visual C++ 4.2, declare the method in your component's Upper1.h file, at the end of the CUpper1 class:
public:
STDMETHOD(ToUpper)(BSTR bstr, BSTR* pbstrRetVal);
and define the ToUpper method in your component's Upper1.cpp file, as follows:
STDMETHODIMP
CUpper1::ToUpper(
BSTR bstr,
BSTR* pbstrRetVal)
{
// validate parameters
if (bstr == NULL || pbstrRetVal == NULL)
return E_POINTER;
// Create a temporary CComBSTR
CComBSTR bstrTemp(bstr);
if (!bstrTemp)
return E_OUTOFMEMORY;
// Make string uppercase
wcsupr(bstrTemp);
// Return m_str member of bstrTemp
*pbstrRetVal = bstrTemp.Detach();
return S_OK;
}
Note the use of the CComBSTR wrapper class, which adds some useful functionality to the native BSTR COM datatype. Another useful class is CComVariant, which wraps VARIANTs. Two other wrapper classes, CComPtr and CComQIPtr, are discussed later in the "ASP Intrinsics" section.
This code is quite paranoid. For quick and dirty tests, you can probably safely eliminate both tests, as ASP will call you with valid parameters and the CComBSTR constructor is unlikely to fail. However, in production code, you ought to handle these potential failures.
The ToUpper method can be called with the following script, named Upper.asp. Don't forget to put the script in an executable virtual directory.
<%
Set oUpper = Server.CreateObject("Upper.Upper1.1")
str = "Hello, World!"
upper = oUpper.ToUpper(str)
%>
The uppercase of "<% = str %>" is "<% = upper %>".
VBScript checks the HRESULT return value for you under the covers. If you return a failure error code, the script will abort with an error message unless there's some error handling in it (such as On Error Next).
If you move the component to another machine, you'll have to run regsvr32.exe to register it. The wizard-generated makefile does this automatically whenever you recompile the component.
Note If you're testing your components inside Active Server Pages 1.0 (instead of, say, Visual Basic 5.0), you will have to stop and restart the Web service before you can relink your components. You will also have to stop and restart the File Transfer Protocol (FTP) and Gopher services, if you're running them. On a development machine, you should turn the FTP and Gopher services off permanently unless you really need them.
You can make restarting the Web service considerably faster if you create the registry entry REG_DWORD in the following registry path, and set the value to zero:
HKEY_LOCAL_MACHINE
\SYSTEM
\CurrentControlSet
\Services
\W3SVC
\Parameters
\EnableSvcLoc
Do the same for MSFTPSVC and GOPHERSVC, if you're running them. On a production server, the service locator should be enabled.
The ASP intrinsics are the built-in Application, Session, Server, Request, and Response objects. Most ASP components need one or more of these to make full use of ASP facilities.
To use the intrinsics, you must provide two methods in your object, OnStartPage and OnEndPage. ASP calls these optional methods on an object whenever a page is opened or closed by the user's Web browser, and they bracket the lifetime of the page.
The OnStartPage method receives an IDispatch* that can be queried (using QueryInterface) for a pointer to an IScriptingContext interface, which provides methods for getting pointers to the intrinsic objects.
Visual C++ 5.0 allows you to add these methods automatically when you create the object by using the ASP tab in the ATL Object Wizard Properties dialog box.
In Visual C++ 4.2, add the following method declarations to your .idl file:
HRESULT OnStartPage(IDispatch* pScriptContext);
HRESULT OnEndPage();
In your .H file, add
#include <asptlb.h>
near the top and add the following declarations at the bottom of the CObj class:
public:
STDMETHOD(OnStartPage)(IDispatch*);
STDMETHOD(OnEndPage)();
private:
CComPtr<IRequest> m_piRequest; // Request Object
CComPtr<IResponse> m_piResponse; // Response Object
CComPtr<IApplicationObject> m_piApplication; // Application Object
CComPtr<ISessionObject> m_piSession; // Session Object
CComPtr<IServer> m_piServer; // Server Object
Finally, add the following method definitions to your .cpp file:
STDMETHODIMP
CObj::OnStartPage(
IDispatch* pScriptContext)
{
if (pScriptContext == NULL)
return E_POINTER;
// Get the IScriptingContext Interface.
CComQIPtr<IScriptingContext, &IID_IScriptingContext>
pContext = pScriptContext;
if (!pContext)
return E_NOINTERFACE;
// Get Request Object Pointer.
HRESULT hr = pContext->get_Request(&m_piRequest);
// Get Response Object Pointer.
if (SUCCEEDED(hr))
hr = pContext->get_Response(&m_piResponse);
// Get Application Object Pointer.
if (SUCCEEDED(hr))
hr = pContext->get_Application(&m_piApplication);
// Get Session Object Pointer.
if (SUCCEEDED(hr))
hr = pContext->get_Session(&m_piSession);
// Get Server Object Pointer.
if (SUCCEEDED(hr))
hr = pContext->get_Server(&m_piServer);
if (FAILED(hr))
{
// Release all pointers upon failure.
m_piRequest.Release();
m_piResponse.Release();
m_piApplication.Release();
m_piSession.Release();
m_piServer.Release();
}
return hr;
}
STDMETHODIMP
CObj::OnEndPage()
{
m_piRequest.Release();
m_piResponse.Release();
m_piApplication.Release();
m_piSession.Release();
m_piServer.Release();
return S_OK;
}
If you don't need all five objects, remove the ones you don't need from your code.
Take note of the use of the CComPtr and CComQIPtr variables above. These are type-safe smart pointer classes that encapsulate traditional pointers to interfaces and can be used interchangeably with them. They give you considerable notational convenience and the assurance that their destructors will automatically Release interfaces. A CComQIPtr automatically queries an interface when it is constructed; a CComPtr does not.
Note that for variables of both classes, you should use piFoo.Release() and not piFoo->Release() because piFoo.Release() resets piFoo.p to NULL after calling piFoo.p->Release(), while piFoo->Release() uses the overloaded operator-> to call p->Release() directly, leaving piFoo in an inconsistent state. That apart, you treat a CComPtr<IFoo> piFoo exactly as you would an IFoo* piFoo.
OnStartPage and OnEndPage are only called on page-level and session-level objects. If your object has application-level scope (for example, if it was created in Application_OnStart in global.asa and added to the Application object), these methods will not be called.
If your object is somehow created by means other than Server.CreateObject or <OBJECT RUNAT=Server ...>, your OnStartPage and OnEndPage methods will not be called either.
Therefore, check that your pointers to the intrinsics are valid before you use them with code such as this:
if (!m_piRequest || !m_piResponse)
return ::ReportError(E_NOINTERFACE);
You might wonder how "!" is being used on objects. Simple—CComPtr and CComQIPtr both define operator! to check their internal pointer, p, and return TRUE if it's NULL. See "Reporting Errors" for an explanation of ReportError.
To build an object that uses IScriptingContext, you will need to copy asptlb.h from your ASP installation directory to your include directory at \Program Files\DevStudio\VC\include. (On Microsoft Windows NT®, the default installation directory for asptlb.h is %SystemRoot%\System32\Inetsrv\ASP\Cmpnts. On Windows® 95, it is \Program Files\WebSvr\System\ ASP\Cmpnts.) If you get linker errors, you may need to add, in one .cpp file, "#include <initguid.h>" before "#include <asptlb.h>".
When creating components in C++, you should understand the following threading models:
Note With Active Server Pages, a pure free-threaded object will not perform as well as a both-threaded object or an apartment-threaded object.
Your objects must be thread-safe and they must not deadlock. It is up to you to protect shared data and global data with critical sections or other synchronization mechanisms. Remember that static data in functions, classes, and at file level is also shared data, as may be files, registry keys, mail slots, and other external system resources.
For a comprehensive discussion of threading models, see Knowledge Base article Q150777, "Descriptions and Workings of OLE Threading Models."
If you want to be a little friendlier to the users of your component, you can set the Error Info. It's up to the calling application to decide what to do with it. By default, ASP/VBScript will print the error number (and message, if there is one) and abort the page. Use On Error Next to override this behavior.
Here is some code that takes a Win32 error or an HRESULT, gets the associated error message (if it exists) and reports it, and then returns the error as an HRESULT.
HRESULT
ReportError(
DWORD dwErr)
{
return ::ReportError(HRESULT_FROM_WIN32(dwErr), dwErr);
}
HRESULT
ReportError(
HRESULT hr)
{
return ::ReportError(hr, (DWORD) hr);
}
HRESULT
ReportError(
HRESULT hr,
DWORD dwErr)
{
HLOCAL pMsgBuf = NULL;
// If there's a message associated with this error, report that.
if (::FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
NULL, dwErr,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
(LPTSTR) &pMsgBuf, 0, NULL)
> 0)
{
AtlReportError(CLSID_CObj, (LPCTSTR) pMsgBuf, IID_IObj, hr);
}
// TODO: add some error messages to the string resources and
// return those if FormatMessage doesn't return anything (not
// all system errors have associated error messages).
// Free the buffer, which was allocated by FormatMessage.
if (pMsgBuf != NULL)
::LocalFree(pMsgBuf);
return hr;
}
You might call it like this:
if (bstrName == NULL)
return ::ReportError(E_POINTER);
or like this:
HANDLE hFile = CreateFile(...);
if (hFile == INVALID_HANDLE_VALUE)
return ::ReportError(::GetLastError());
To reduce the size of the components, C++ exceptions are turned off for ATL components by default, as the C runtime library is required if exceptions are enabled. This has a few implications, notably that new does not throw exceptions, as it normally would. Instead it returns NULL. C++ exception handling can be turned on, however. It will be turned on if the Microsoft Foundation Class Library (MFC) is also being used. Accordingly, the ATL source is sprinkled with code like this:
CFoo* pFoo = NULL;
ATLTRY(pFoo = new CFoo(_T("Hello"), 7))
if (pFoo == NULL)
return E_OUTOFMEMORY;
where ATLTRY is defined as:
#if defined (_CPPUNWIND) & (defined(_ATL_EXCEPTIONS) | defined(_AFX))
# define ATLTRY(x) try{x;} catch(...) {}
#else
# define ATLTRY(x) x;
#endif
It's up to you to decide if you want to turn on exceptions. Making a component 25K larger by linking in the C runtime library is much less of an issue for server components than for downloadable browser components. You'll probably want other features of the C runtime library anyway. If you do turn on exceptions, be aware that it is considered extremely bad form to throw C++ exceptions or structured exception handling (SHE) exceptions across COM boundaries, so you should catch all exceptions thrown in your code. If you leave exceptions disabled, then you must check for NULL.
OLE/ActiveX is all Unicode, Windows NT uses Unicode internally, but Windows 95 uses the ANSI character set. ASP runs on both Windows NT and Windows 95. So, for maximum portability, you should not assume that your components will be running on a Unicode platform and you should not take short cuts such as the following, as they will fail on Windows 95:
CreateFileW(..., bstrFilename, ...)
ATL comes with a number of easy-to-use macros such as OLE2T for converting between BSTRs, Unicode, ANSI, and TCHARs. One caveat—these macros use _alloca internally, which allocates memory on the stack, so you must be careful about returning the results of these macros from functions.
A number of samples are now available on the Microsoft IIS Samples site at http://www.microsoft.com/IIS/UsingIIS/Developing/Samples/. They include:
Other components on the site include: