Partager via


Developing Active Server Components with ATL

 

George V. Reilly
Software Design Engineer, Internet Information Server

April 2, 1997

Contents
Why Bother? ATL: Active Template Library ATL version 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 Bother?

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.

  • Performance. Interpreted languages are inherently slower than well-written C++. There is also overhead associated with parsing and executing them, which uses CPU cycles that might be put to better use in making the server more responsive to other users.
  • Access to the operating system. Many features of the operating system are difficult or impossible to use from VBScript: no access to the system registry; no thread support; no file mapping; and so forth.
  • Access to the missing features of Visual Basic. VBScript is missing many of the features of Visual Basic. Some of those features, such as forms, have no place in a server-side scripting language; but being able to use all of, say, the date-formatting facilities would sometimes be very convenient.
  • Leverage existing code/code reuse. You may have existing C++ code that you want to use in an ASP application; just wrap it up in an ASP component.
  • Separate the UI from the data processing. You can put ever-fancier VBScript user interfaces on your applications, while keeping the guts of the application in a common core.
  • Encapsulating business logic. Wrapping your business logic in a component makes it easier to debug and easier to distribute.
  • Protect your Intellectual Property. If you're in the business of selling ASP applications to other Web sites and you're writing those ASP applications in VBScript, then you're selling your source code to your customers.

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.

When to Write a Component

A few points you should bear in mind when deciding whether to write a component:

  • Writing components is time consuming. You're likely to spend considerably more time writing and debugging a component in C++ than you would writing an equivalent ASP program. If you're writing the component in the name of the great god Efficiency, be sure that you really need to write the component. Prototype it first in VBScript, and measure the performance. Be sure that it really is a bottleneck, that it really is too slow, that it really does use too much CPU time.
  • Writing components is expensive. They take more time to write and time is money. They require more skilled, and hence more expensive, programmers.
  • Components can't do everything that you might want to do on a Web server. Neither can ISAPI extension DLLs. If you want to do custom logging or change the HTML data stream sent back to users' browsers, you'll have to write an ISAPI filter DLL. That's beyond the scope of this article.

Which Language?

Which language should you use to write a component?

  • Visual Basic version 5 Visual Basic version 5 components are easy to write because Visual Basic takes care of lots of things for you, such as memory management and hiding many of the details of COM. Visual Basic version 5 can create apartment-threaded objects, which are recommended for good performance, whereas Visual Basic version 4 is restricted to single-threaded objects and its runtime is not thread-safe. There are some disadvantages: Visual Basic version 5, like VBScript, restricts you in accessing the operating system.
  • **Java/Visual J++**™ Java is a powerful language, well suited for creating server-side components. It too takes care of many of the tedious details for you, and it gives you better access to the operating system than Visual Basic version 5 or VBScript, but there are some things that you cannot do. For example, some of the Win32 APIs expect pointers, and Java has no notion of a pointer.
  • C++ C++ gives you the most power. It's faster than Visual Basic version 5 or Java, and it gives you full access to the operating system. The down side is that it's harder to write, and even with good class libraries like ATL, you have more bookkeeping to do.
  • Other languages Any language capable of creating COM Automation servers can be used to build ASP components. For best results, you should build both- or apartment-threaded in-proc servers.

Later articles in this series will discuss writing components with Visual Basic, Java, and MFC, and debugging components.

ATL: Active Template Library

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:

  • It produces small, fast, industrial-strength components ("lean and mean")
  • It supports all COM threading models (single, apartment, free)
  • It supports IDispatch interfaces
  • It makes dual interfaces easy
  • It supports the COM error mechanism
  • It calls methods very quickly
  • It gives fine control over COM features ("closer to the metal")
  • It allows you to build several different types of objects and controls, including:
    • Minimal COM objects
    • Full controls
    • Internet Explorer controls
    • Property pages
    • Dialog boxes

ATL version 2.x

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.

Creating a Component with ATL

To create a new component:

  1. Start a New Project Workspace in Developer Studio.
  2. Select ATL COM AppWizard.
  3. Enter the name and location of the project.
  4. Click Create.
  5. Accept the defaults and click Finish.

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.

Simple Example

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.

ASP Intrinsics

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.

CComPtr and CComQIPtr

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.

Object Scope

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.

asptlb.h

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

Threading

These are the threading models that you need to understand.

  • Single-threading model. Only one thread uses COM and all calls to COM objects are synchronized by COM. Except for the simplest of applications, this leads to unacceptable performance on a server such as ASP.
  • Apartment-threading model. One or more threads in a process use COM, and calls to COM objects are synchronized by COM. An instance of an object is always called on the same thread, guaranteeing serial access to it. Interfaces are marshalled between threads. You need to protect shared data only, not per-instance data. Apartment-threaded objects give acceptable performance.
  • Free-threading model. One or more threads in a process use COM, and calls to COM objects are synchronized by the objects themselves. Interfaces are not marshalled between threads. You must also protect per-instance data.
  • "Both"-threading model. Objects are marked as both apartment-threaded and free-threaded. This is the default for objects produced by ATL, and this is the recommended model.

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

Reporting Errors

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());

Exceptions

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.

Character Sets

OLE/ActiveX is all-Unicode, Windows NT uses Unicode internally, but Windows 95 uses the ANSI character set. ASP runs on both 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.

Samples

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.