George V. Reilly
Software Design Engineer, Internet Information Server
April 2, 1997
Contents
Why Bother?
ATL: Active Template Library
ATL 2.x
Creating a component with ATL
Simple Example
ASP Intrinsics
Threading
Reporting Errors
Exceptions
Character Sets
Samples
This article tells you how to write an Active Server Pages (ASP) component with the Active Template Library (ATL), and when you might want to. 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 with writing C++ components for your Web server now that ASP is an integral part of Microsoft's Internet Information Server (IIS) version 3.0? Surely you can throw away all of those laboriously written ISAPI extension DLLs and CGI programs and just whip up a concoction of HTML and 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 JScript are powerful and useful, but they have disadvantages, too.
The first two reasons apply only to components written in C++. The other reasons apply equally to components written in Visual Basic, Java, or other languages.
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, and 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?
Later articles in this series will discuss writing components with Visual Basic, Java, and MFC, and debugging components.
ATL, Microsoft's Active Template Library (formerly the ActiveX Template Library), is used to build simple COM objects that can be called from an ASP page, Visual Basic, or other automation clients.
ATL is the recommended library for writing ASP and other ActiveX components in C++ for the following reasons:
ATL version 2.x was released in mid-February, 1997. ATL version 2.0 is available for download for Microsoft Visual C++® version 4.2, and ATL version 2.1 is an integral part of Visual C++ 5.0 (available mid-March, 1997).
ATL version 2.0 requires Visual C++ version 4.2b or later. If you are using Visual C++ version 4.2, you must upgrade to Visual C++ version 4.2b or later with the Visual C++ 4.2b Technology Update . Note that this patch will not work with earlier or later versions of Visual C++, 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. In Visual C++ version 4.2, go to the Insert menu of Developer Studio and select Component.... The Component Gallery will appear. A number of tabs will appear at the bottom of the picture, such as Microsoft and OLE Controls. Scroll right until you see the ATL tab. Double-click the ATL Object Wizard.
In Visual C++ version 5.0, go to the Insert menu, where you'll see New ATL Object.... Or you can right-click the classes in the ClassView pane, where you'll also see New ATL Object....
When the ATL Object Wizard pops up, you'll see two panes. In the left pane, click Objects. In the right pane, double-click Simple Object. If you have Visual C++ version 5.0, you'll see a number of additional objects; click ActiveX Server Component instead.
The ATL Object Wizard Properties dialog box will appear. On the Names tab, type the short name of your object. The other names will be filled in automatically. You can edit them if you wish. It's quite likely that you'll want to edit the Prog ID.
On the Attributes tab, you may want to change the Threading Model to Both (see Threading below for a discussion of threading models). You probably don't need to support Aggregation. See Reporting Errors below for why you ought to support ISupportErrorInfo. The other attributes should not need to be changed.
On the ASP tab (only present in Visual C++ version 5), you'll see a number of options that will make much more sense after you read the section on ASP intrinsics below. You can selectively enable which intrinsics you want to use.
Let's build a really simple component, 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++ version 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++ version 5, right-click IUpper1 in the ClassView pane and click Add Method.... Type ToUpper as the Method Name and
[in] BSTR bstr, [out, retval] BSTR* pbstrRetVal
in the Parameters. Use the Attributes... button 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++ version 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 thus in your component's Upper1.cpp file:
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 wrapper class, CComBSTR, which adds some useful functionality to the native COM datatype, BSTR. Another useful class is CComVariant, which wraps VARIANTs. Two other wrapper classes, CComPtr and CComQIPtr, are discussed below in the section on ASP intrinsics.
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. In production code, you ought to handle these potential failures.
The ToUpper method can be called with the following script, 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, then the script will abort with an error message, unless there's some error handling in it (e.g., 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 version 5), 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 FTP and Gopher services, if you're running them. On a development machine, just 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 following value in the registry, of type REG_DWORD, and set it 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 locater 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 them to make full use of ASP's facilities.
To use the intrinsics, you must provide two methods in your object, OnStartPage and OnEndPage. These optional methods are called by ASP 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 QueryInterface'd for a pointer to an IScriptingContext interface, which provides methods for getting pointers to the intrinsic objects.
Visual C++ version 5.0 allows you to automatically add these methods when you create the object, by using the ASP tab in the ATL Object Wizard Properties dialog box.
In Visual C++ version 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 class, CObj:
public: STDMETHOD(OnStartPage)(IDispatch*); STDMETHOD(OnEndPage)(); private: // ASP intrinsic objects CComPtr<IRequest> m_piRequest; CComPtr<IResponse> m_piResponse; CComPtr<IApplicationObject> m_piApplication; CComPtr<ISessionObject> m_piSession; CComPtr<IServer> m_piServer;
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(). 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.
Note:
OnStartPage and
OnEndPage
are only called on page-level and session-level objects. If your object
has application-level scope (e.g., 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 some 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 InstallDir\ASP\Cmpnts\asptlb.h to your include directory, \Program Files\DevStudio\VC\include. On Windows NT, the default installation directory is %SystemRoot%\System32\Inetsrv. On Windows 95, it is \Program Files\WebSvr\System. If you get linker errors, you may need to #include <initguid.h> in one .CPP file before you #include <asptlb.h>.
These are the threading models that you need to understand.
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: 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 that, 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());
C++ exceptions are turned off for ATL components, by default, to reduce the size of the components, 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, and it will be if 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, and you probably want other features of the CRT anyway. If you do turn on exceptions, be aware that it is considered extremely bad form to throw C++ exceptions or SEH 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 take short cuts such as the following:
CreateFileW(..., bstrFilename, ...)
as they will fail on Windows 95. 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 Windows NT Server samples site . They include a Registry Access Component. [Editor's note: Some of our readers have written in looking for samples that are no longer posted as of December 1998. We plan to repost several samples in March 1999.]
George V. Reilly works on ASP and IIS performance issues. He wrote many of the IIS Sample Components for Active Server Pages.
Back to top