Magazine |
| | Community |
| | Workshop |
| | Tools & Samples |
| | Training |
| | Site Info |
|
|
||||||||
|
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.
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?
ATL is the recommended library for writing ASP and other ActiveX components in C++ for the following reasons:
ATL version 2.0 requires Visual C++ version 4.2b. If you are using Visual C++ version
4.2, you must upgrade to Visual C++ version 4.2b 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.
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.
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* pbstrRetValin 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 \EnableSvcLocDo the same for MSFTPSVC and GOPHERSVC, if you're running them. On a production server, the service locater should be enabled.
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 CComPtrFinally, add the following method definitions to your .CPP file:m_piRequest; CComPtr m_piResponse; CComPtr m_piApplication; CComPtr m_piSession; CComPtr m_piServer;
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>.
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 .
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());
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; #endifIt'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.
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.
George V. Reilly works on ASP and IIS performance issues. He wrote many of the IIS Sample Components for Active Server Pages.
Did you find this article useful? Gripes? Compliments? Suggestions for other articles? Write us!
© 1998 Microsoft Corporation. All rights reserved. Terms of use.