Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

Writing ActiveX ISAPI Extensions


Dr. Dobb's Journal August 1997: Writing ActiveX ISAPI Extensions

VB5 and C++ are the tools you need

Al is a contributing editor for Dr. Dobb's Sourcebook. He recently wrote a training course on activating web content using Normandy for major online services. You can contact Al at http://www.al-williams.com/awc/.


The traditional approach to creating active web pages is to write a CGI program that can accept input (from a form or URL) and send output to a web browser. For example, a CGI program might accept your name and e-mail address, look up your account in a database, and display your current bill.

If you use Microsoft's Internet Information Server (IIS), you can still write CGI programs, but there is another way -- ISAPI. An ISAPI DLL actually becomes part of the server and is generally more efficient than a classic CGI program. I've written ISAPI DLLs both in C and using MFC. Neither way is very difficult. Recently, a friend asked whether ISAPI extensions could be written in Visual Basic. Initially, my answer was no, because VB can't make traditional DLLs (only ActiveX DLLs).

Subsequently, I ran across OLEISAPI, a Microsoft sample that enables VB ISAPI. After a couple of days of trying to use it, I gave up because it just doesn't work well--it does not encapsulate ISAPI in an object-oriented way, nor does it let you fully access ISAPI features. Consequently, I wrote CBISAPI, an ISAPI module that lets you write ActiveX ISAPI extensions. Although I wrote it with VB5 in mind, you can use it with any ActiveX-capable language.

The Plan

My idea was simple: Write an ISAPI extension DLL that calls an ActiveX server. The DLL passes the server an ActiveX object that it uses to read the HTTP information and manipulate the HTTP output (usually an HTML file). This is a bit odd: The DLL is both an ISAPI DLL and an ActiveX server. It, in turn, serves an object to another ActiveX server (the VB ISAPI extension). In truth, the VB ISAPI extension doesn't really have to be an OLE server, but that is the only kind of DLL VB can make.

Table 1 shows the members of the object the VB server uses to interact with IIS (the Internet Information Server). The VB server's Sub may have any name, but it must take an object as an argument; see Example 1. You use the object (server, in this case) in the VB code by using the members in Table 1.

A May-December Marriage

Although VB5 makes it easy to create an ActiveX DLL, it doesn't have a good way to make an ISAPI DLL. Therefore, the main ISAPI DLL uses MFC (see ISERVER.CPP, available electronically; see "Availability," page 3). Although MFC has special provisions for creating ISAPI DLLs, this server doesn't use them since they try to add another layer on top of ISAPI. Instead, the DLL is just an ordinary MFC DLL with the correct ISAPI entry points. The supporting DLL code (CBISAPI.CPP and some header files) is also available electronically.

My example ISAPI extension uses VB, but any language that can create a comparable OLE server will work. In this article, however, I'll call the OLE extension server a VB server to distinguish it from the C++ ISAPI extension DLL.

How do you write a URL that calls your code? It is a three-step process.

1. Get ISAPI to call the C++ DLL (CBISAPI.DLL).

2. Name the VB server in the URL's query string (the part following a question mark).

3. Add a colon, and the name of the OLE method you want to call; see Example 2.

What does all that mean? The part before the question mark invokes the ISAPI DLL. When you create an OLE server with VB, the server's name will be the project name, a period, and the name of the VB Class module that contains the object you want to work with. In this example, the HILO project has all of its code in a class module named DLL. Inside that module is a Sub named Guess. The plus sign signifies the end of the OLE server name. Everything after that is part of the query string the server sends to the program. Notice that the C++ server doesn't change the query string -- it is up to your code to skip the first part, parse HTTP escape sequences, and otherwise process the query string.

A Quick Look at ISAPI

ISAPI programs are straightforward to write in C or C++. There are two types of ISAPI-related DLLs: The extension DLL (the one presented in this article) that generates output dynamically; and a filter that can handle certain requests for data.

For the type of DLL you want to write, you need the functions GetExtensionVersion and HttpExtensionProc. GetExtensionVersion informs the server what version of IIS your DLL expects and provides a description string. You can copy this code directly from the online help -- it is mindless. HttpExtensionProc is where all the work occurs. It receives a single argument, but that argument is a pointer to an EXTENSION_CONTROL_BLOCK (Table 2 and Table 3) that contains quite a bit of data. Compare Table 1 and Table 2, and you'll notice that the members in Table 1 generally encapsulate the EXTENSION_CONTROL_BLOCK in an object-oriented way.

You have to be careful when writing an extension DLL. Just as DLLs that you use in a program become part of your process, your extension DLL will become part of IIS. If you crash, you could crash the server. If you throw an exception, the server will quietly continue and terminate your DLL. CBISAPI is careful to run the VB code inside a try block. It reports any exceptions it finds to the HTML stream.

Writing the HILO.DLL Server

VB5 makes writing an ActiveX DLL simple. From the starting screen, simply select ActiveX DLL. This correctly sets up the project, and starts you with a class module to contain your properties and code. You'll want to be sure to rename the project and the class module -- these will make up the name of the server.

For a CBISAPI server, you only need to define a Public Sub that takes an Object as an argument. The HILO.DLL class module (Listing One has several private Subs to handle internal processing. These are strictly for the benefit of the Guess subroutine -- the one the HTML code calls.

Although this class server only has one public entry point, there is no reason you couldn't have multiple entry points in a server. You can add more class modules, too. This would allow you to group related functions together in each class and group related classes in one DLL.

You can see the finished product in Figure 1. The game actually is a simple binary search. It will always guess correctly within ten tries. Of course, you could always cheat. If the content (the data submitted by a form) is empty, the program assumes you are just starting the game. It expects the query string to have two variables, HI and LO, that specify the range of numbers. The program just calls the private GuessAgain routine to generate a form that displays the current guess, and offers three radio buttons that specify if the guess is high, low, or correct. The form submits back to the same Guess method via CBISAPI. The program sets the query string to reflect the current high and low values.

On subsequent calls, the content will contain the status of the form buttons. The code detects that content is present and recalculates the high and low limits. It then calls GuessAgain to generate a new form. Of course, if the guess is correct, the code doesn't generate the form. Instead, it calls the Win routine to generate an appropriate message.

Listing Two is the HTML file that starts the whole thing running. Of course, you could embellish this if you like. Also, the guessing form in Figure 1 could be fancier. For example, you might put some JavaScript in the form so that when you click a radio button it automatically submits the form. Still, the existing code does the job and is enough to show how the ISAPI interface works.

Installation and Distribution

The C++ DLL has to create an ActiveX object. However, it does so on behalf of an anonymous Internet user. Therefore, the default Internet user must have privileges to create ActiveX objects. For NT 4.0, you can set this by running DCOMCNFG (a program in your SYSTEM32 directory). Select the default security tab and add IUSR_xxx (where xxx is your server's name) to the Default Access Permissions and Default Launch Permissions sections. When you first click the Add button on each choice, you'll only see group names. Click the Show Users button to show individual users (including IUSR_xxx).

Of course, all the DLLs required by each piece of the puzzle must be on the server. If you build CBISAPI to use the MFC DLLs, then they must be present (preferably in the \WINNT\SYSTEM32 directory). The VB portion requires the VB run-time DLLs, also.

You must register your VB server on the Internet server machine using REGSVR32 for DLLs or by running the executable. Registering the server on a different machine only affects that machine's registry. If you fail to do this, you'll get an exception with an error code of REGDB_E_CLASSNOTREG (0x80040154).

Even though CBISAPI provides an ActiveX object, it doesn't require registration. That's because no other program ever creates its object. It creates the object itself and passes it to other ActiveX programs. Of course, that means the object has no type information. This prevents VB from validating your calls at compile time. Instead, you'll discover any type mismatches or misspelled names at run time.

Inside the C++ DLL

The C++ DLL is an MFC OLE DLL. CBISAPI.CPP (available electronically) contains entry points required for ActiveX (provided by MFC's App Wizard) and the ISAPI entry points. The standard HTTPEXT.H prototypes the functions as C entry points so the C++ compiler won't alter the names. However, this same header doesn't declare the functions as exportable (using _declspec(dllexport)). Therefore, you must mention the functions in the EXPORTS section of the DEF file so that IIS can locate them.

The HttpExtensionProc routine isn't very sophisticated. It parses the query string to find the name of the server and the method name. This name must be the first thing in the query string. The parsing completes at the end of the query string or at the first plus sign the code encounters. If you omit the method name, CBISAPI tries to call the default method (ISAPI).

Notice the use of Unicode characters for the member name. If you pass in a name, the program uses the A2W function to convert the string to Unicode. Otherwise, it uses the string literal L"ISAPI" (the L indicates a Unicode constant). This is the first (but not the last) place this problem rears its ugly head. IIS supports HTTP, of course. HTTP uses ANSI characters (the normal characters we all know and love). However, ActiveX uses Unicode (2-byte character) strings for everything. In theory, the whole world will eventually switch to Unicode. In theory, the U.S. will switch to the metric system. Meanwhile, you have to resort to conversions. There are many ways to convert ANSI and Unicode characters. I elected to use the MFC functions from AFXCONV.H. (See MFC's Technical Note #59 for more about these macros. The note, by the way, inaccurately states that you need AFXPRIV.H for the macros; this used to be true, but now you should use AFXCONV.H.)

Once the code knows what object to create, it uses the MFC class CDispatchDriver to represent it. A simple call to CreateDispatch will create the object. Next, a call to GetIDsOfNames converts the member name to a dispatch ID (DISPID). DISPIDs are function codes that ActiveX automation objects use to identify members (and properties, too). Armed with the DISPID, a call to InvokeHelper calls the VB code. A previous call to GetIDispatch retrieves the pointer you need to pass to the VB code so that it can access the server object.

Notice that CBISAPI protects the InvokeHelper call with a try statement. This ensures that any exceptions return to CBISAPI. CBISAPI reports errors by printing to the HTML stream. This works as long as the VB extension hasn't started writing some non-HTML data type before causing the exception.

The server object (IServer.CPP, available electronically) is where all the real work occurs. This class is easier to construct than you might expect. First, I used Class Wizard to create a CCmdTarget-derived class. All ActiveX automation objects in MFC derive from CCmdTarget. Then, I used Class Wizard's OLE Automation tab to add the properties and methods (Figure 2). The only hard part is writing the code.

The only odd part about the code is the conversion between BSTRs (ActiveX Unicode strings) and char * (IIS ANSI strings). In several places, I used the MFC conversion functions I mentioned earlier. However, in several cases I had the data in an MFC CString and decided to use CString::AllocSysString() to create a BSTR. That's where I ran into a little trouble.

BSTRs are not C (or C++) strings. They may contain embedded null characters. To facilitate this, each BSTR has a count of characters. Usually, the string ends in a null out of consideration for C/C++ programmers (and the Windows API), but the terminal null isn't usually part of the string. For example, a BSTR that contains the string "Dobb's\0" should have a count of 6 unless your intent is to embed the null character inside the string. However, the size returned by ISAPI includes the null character. Who cares? Well, VB cares. Consider the ServerVariable method in the CBISAPI server object. If it creates a BSTR that contains the null, what happens? Consider the VB code in Example 3(a), where everything works fine and the trailing zero byte is innocuous. Suppose in Example 3(b), however, that the value of HOST_NAME is "www.al-williams.com\0" and the value of SCRIPT_NAME is "ztest.dll\0." VB dutifully forms the string "www.al-williams.com\0ztest.dll\0." However, the C code that drives the Write method stops at the first null.

To prevent this problem, subtract 1 from the size that ISAPI returns. Alternately, you could let CString recalculate the size. Either way, you must not set the size to include the null byte.

Debugging Tips

Debugging ISAPI extensions is an unpleasant business. To debug the C++ portion, you can fudge IIS to run as a user process and run it under a debugger (see MFC Technical Note #63). Debugging the VB portion is even worse. The easiest thing to do is pepper your code with temporary WriteLine commands so you can see things in your HTML stream. It's not ideal, but it works. Luckily, CBISAPI will report any exceptions that occur, so you only need to worry about logical errors.

Another annoyance is that you usually have to shut down IIS so you can copy over (or relink) your files. You can set the HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/W3SVC/Parameters/CacheExtensions registry key to zero to make IIS release the files more quickly. Also, each time you recreate the VB server, you must reregister it on the IIS machine.

Future Directions

There are many enhancements you could make to the C++ DLL, CBISAPI. One welcome change would be automatic parsing of the query string and of content. For example, you could create a parameterized property named ParsedQueryString. It could take the name of an argument and return the string value, like Example 4.

I also toyed with adding a debugging mode you could turn on with a query string option. Another idea would be to make a debugging version of CBISAPI.DLL (perhaps CBISAPID.DLL). This version would print debugging information out to the HTML stream. There are two problems with this idea. First, if the extension you are writing wants to set headers, it must do so before any output (including your debugging output). Second, what about extensions that output something other than an HTML file (for example, a GIF file)?

Finally, it would probably be a good idea to disallow remote users from creating arbitrary ActiveX servers on your machine. Of course, the risk is minimal because the ActiveX server would have to have an entry point that expects a single object, too. Still, it would be simple to make CBISAPI read a configuration file on the server that defined symbolic names for ActiveX servers. If the request specifies a name that isn't on the list, CBISAPI could reject the command.

What About ASP?

If you've been following Microsoft's Normandy initiative, you know that the latest version of IIS supports something called "Active Server Pages" (ASP), which let you embed Basic code inside web pages. Often, you can use this capability instead of ISAPI -- especially since ASP scripts can create ActiveX automation objects and call them. In a nutshell, you enclose Basic code inside <% and %> brackets; see Example 5(a). If you want the output to appear in the HTML stream, you use <%= instead of <%, as in Example 5(b) or you can mix Basic and HTML; see Example 5(c).

While this is a great idea, it doesn't completely replace the need for ISAPI. For one thing, IIS is not the only ISAPI-capable web server. Also, while scripting is good for creating web pages, it isn't appropriate for certain tasks (generating graphics, for example).

If you have ASP available, you should certainly consider using ASP instead of ISAPI. However, there are still cases where ISAPI is the best choice, and the code presented here can help simplify your ISAPI development.

Conclusion

ISAPI is an efficient alternative to CGI programming, but it isn't very programmer friendly. CBISAPI is a step toward making ISAPI more object oriented and more accessible (particularly to VB programmers).


Listing One

VERSION 1.0 CLASSBEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "DLL"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = True
Option Explicit
Private Sub svrerr(server As Object, errstr As String)
server.WriteLine "Error: " & errstr
server.statcode = 400
server.retval = 4
End Sub


</p>
Private Sub Win(server As Object)
server.WriteLine "<HTML><HEAD><TITLE>I Win</TITLE></HEAD><BODY>"
server.WriteLine "I got it right!</BODY></HTML>"
End Sub


</p>
Private Sub GuessAgain(server As Object, Hi As Long, Lo As Long)
Dim servername As Variant
Dim script As Variant
server.WriteLine "<HTML><HEAD><TITLE>HiLo!</TITLE></HEAD><BODY>"
server.WriteLine "My guess is " & CInt((Hi + Lo) / 2) & "<P>"
server.ServerVariable "SERVER_NAME", servername
server.ServerVariable "SCRIPT_NAME", script
server.WriteLine "Is my guess:<P>"
server.Write "<FORM ACTION=http://" & servername
server.Write "/" & script
server.WriteLine "?HILO.DLL:Guess+HI=" & Hi & "+LO=" & Lo & " METHOD=POST>"
server.WriteLine "High <INPUT TYPE=RADIO NAME=ANSWER VALUE=HI><P>"
server.WriteLine "Correct <INPUT TYPE=RADIO NAME=ANSWER VALUE=OK><P>"
server.WriteLine "Low <INPUT TYPE=RADIO NAME=ANSWER VALUE=LO><P>"
server.WriteLine "<INPUT TYPE=SUBMIT>"
server.WriteLine "</FORM>"
server.WriteLine "</BODY></HTML>"
End Sub
Public Sub Guess(server As Object)
 Dim Guess As Long
 Dim Hi As Long
 Dim Lo As Long

 Dim pos As Long
 Dim ans As String
 pos = InStr(1, server.QueryString, "HI=", vbTextCompare)
 If pos = 0 Then
   svrerr server, "Can't find HI"
   Exit Sub
 End If
 Hi = Val(Mid(server.QueryString, pos + 3))
 pos = InStr(1, server.QueryString, "LO=", vbTextCompare)
 If pos = 0 Then
   svrerr server, "Can't find LO"
   Exit Sub
 End If
 Lo = Val(Mid(server.QueryString, pos + 3))
 If server.ContentLength = 0 Then
  GuessAgain server, Hi, Lo
 Else
  Guess = (Hi + Lo) / 2
  pos = InStr(1, server.Content, "ANSWER=", vbTextCompare)
  If pos = 0 Then
    svrerr server, "Form error"
    Exit Sub
  End If
 ans = Mid(server.Content, pos + 7, 2)
 If ans = "OK" Then Win server
 If ans = "LO" Then GuessAgain server, Hi, Guess
 If ans = "HI" Then GuessAgain server, Guess, Lo
 If ans <> "OK" And ans <> "LO" And ans <> "HI" Then svrerr server, "Unknown 
Response: " & server.Content
End If
End Sub

Back to Article

Listing Two

<HTML><HEAD>
<TITLE>Play Hi-Lo!</TITLE>
</HEAD>
<BODY>
I'll guess your number.<BR>
Think of a number between 1 and 1024 and I'll guess it.<BR>
Think of your number and
<A HREF=http://www/scripts/cbisapi.dll?HILO.DLL:GUESS+HI=1024+LO=1>
click here to play</A>
</BODY>
</HTML>


</p>

Back to Article

DDJ


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.