ISAPI Programming with C++Builder
by Matt Lawrence

Abstract

This paper will introduce and discuss the creation of Web-server extensions using the Internet Server Application Programming Interface, or ISAPI. I will include a general overview and implementation in standard C++, suitable for Borland C++ or C++Builder. C++Builder users will appreciate the ease with which they can incorporate their databases into their web extensions.

Motivation

CGI is powerful and quite flexible. However, ISAPI provides an alternative for users of Microsoft's Internet Information Server and Personal Web Server (and other's that support ISAPI). This is a small introduction to ISAPI covering the most used features, including database access. ISAPI extensions' main advantage is a speed increase over standard CGI, especially on servers with large volumes of hits.

Introduction

ISAPI provides two to types of DLL add-ons to your IIS Web Server:

 

Historical Reference: The Common Gateway Interface

The most common and certainly most portable server extensions follow the Common Gateway Interface (CGI). A CGI application is simply an executable. When a CGI app is called, the web server creates a new process for it and sends information via stdin and environment variables. Form information (field names/values) is sent through stdin, server variables are set as environment variables for the new process. When the CGI application is ready to transmit information (probably HTML text) back to the server, it writes to stdout which is sent right back to the browser.

Here is a small example that simply gets the REMOTE_HOST server variable (that holds the IP address of the client machine) and then prints to the browser "Hello World! You are at the computer whose IP address is xxx.xxx.xxx.xxx".

Example1.c

 


/* Build using the following command-line:



	bcc32 example1.c



*/

#include <stdlib.h>

#include <stdio.h>

 

int main()

{

	char *RemoteAddr;

	RemoteAddr=getenv("REMOTE_ADDR");

	printf("Content-type: text/html\n\n\n");

	printf("<H1>Hello World!<br>You are at the computer whose IP address is %s.</H1>",RemoteAddr);

	return 0;

}

 


To embed this CGI script, two things must be done:

  1. Copy this executable (example1.exe) into the scripts directory for the web server (mine is C:\WEBSHARE\SCRIPTS).
  2. Embed this tag in a Web page:

When this tag is activated, the web browser receives the HTML text sent to stdout (via printf) and displays it accordingly:

 

It is easy to see why CGI and scripting is so appealing. Although simple, this is a completely dynamic web page whose output is determined by the IP address of the user accessing it. More importantly, the level of C programming knowledge required is quite elementary. Since most web servers on most platforms support CGI, this code is quite portable.

 

Overview: The Internet Server Application Programming Interface (ISAPI)

ISAPI is a logical, specific, extension of the base functionality of CGI. ISAPI server extensions are built as dynamically linked libraries (DLLs) which are loaded by ISAPI-savvy web servers (a list of which can be found on Microsoft's Web Server at http://www.microsoft.com). In writing the examples, I used Microsoft's Personal Web Server for Windows95, but the scripts should work equally well with any web server supporting the ISAPI standard.

Unlike CGI applications, ISAPI DLLs become part of the web server process itself. As a loaded DLL, the web server simply creates a new thread for each client request, and passes a pointer to a structure (containing necessary information) to the DLL. It is much easier and faster to maintain a thread than to launch an entirely separate process.

 

How to build an ISAPI DLL

The only requirements for an ISAPI DLL is that it export two functions:

DWORD _export WINAPI GetExtensionVersion(HSE_VERSION_INFO *)

DWORD export WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *)

 

GetExtensionVersion

This is the first function called by the Web server. If this function is not provided, the call will fail and your DLL will be unusable. GetExtensionVersion simply sets version and description information in the HSE_VERSION_INFO structure pointer passed. The description is a string that should, well, describe your extension. The version information is not the version of your DLL, but the version of ISAPI it supports. It is recommended that you use the predefined constants HSE_VERSION_MINOR and HSE_VERSION_MAJOR (currently 0x0 and 0x1 respectively).

 

The function is as follows:

BOOL _export WINAPI GetExtensionVersion( HSE_VERSION_INFO  *pVer )

{

    pVer->dwExtensionVersion = MAKELONG( HSE_VERSION_MINOR,

                                         HSE_VERSION_MAJOR );

    lstrcpyn( pVer->lpszExtensionDesc,"My ISAPI Server Extension",

               HSE_MAX_EXT_DLL_NAME_LEN );

    return TRUE;

}

 

HttpExtensionProc

HttpExtensionProc is a callback function, similar to main(). It is the main entrypoint into your ISAPI DLL. This is where all of the work is going to be done. The function is passed a pointer to an EXTENSION_CONTROL_BLOCK. This structure contains method pointers and data members that provide the basic communication functionality between the client and the server.

The definition of EXTENSION_CONTROL_BLOCK from httpext.h:

 

typedef struct _EXTENSION_CONTROL_BLOCK {



    DWORD     cbSize;                 // size of this struct.

    DWORD     dwVersion;              // version info of this spec

    HCONN     ConnID;                 // Context number not to be modified!

    DWORD     dwHttpStatusCode;       // HTTP Status code

    CHAR      lpszLogData[HSE_LOG_BUFFER_LEN];// null terminated log info specific to this Extension DLL



    LPSTR     lpszMethod;             // REQUEST_METHOD

    LPSTR     lpszQueryString;        // QUERY_STRING

    LPSTR     lpszPathInfo;           // PATH_INFO

    LPSTR     lpszPathTranslated;     // PATH_TRANSLATED



    DWORD     cbTotalBytes;           // Total bytes indicated from client

    DWORD     cbAvailable;            // Available number of bytes

    LPBYTE    lpbData;                // pointer to cbAvailable bytes



    LPSTR     lpszContentType;        // Content type of client data



    BOOL (WINAPI * GetServerVariable) ( HCONN       hConn,

                                        LPSTR       lpszVariableName,                                                                                           

                                        LPVOID      lpvBuffer,

                                        LPDWORD     lpdwSize );



    BOOL (WINAPI * WriteClient)  ( HCONN      ConnID,

                                   LPVOID     Buffer,

                                   LPDWORD    lpdwBytes,

                                   DWORD      dwReserved );



    BOOL (WINAPI * ReadClient)  ( HCONN      ConnID,

                                  LPVOID     lpvBuffer,

                                  LPDWORD    lpdwSize );



    BOOL (WINAPI * ServerSupportFunction)( HCONN      hConn,

                                           DWORD      dwHSERRequest,

                                           LPVOID     lpvBuffer,

                                           LPDWORD    lpdwSize,

                                           LPDWORD    lpdwDataType );



} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;   

For the first ISAPI example, which is simply a re-write of our first CGI program, we will be concerned with the following members of the ECB structure:

 

ConnID

HCONN Context-ID. Used in function calls back to the server (i.e. WriteClient)

WriteClient

Function pointer used to write to the client (equivalent to stdout in CGI). Uses ConnID as the first param.

GetServerVariable

Function pointer used to retrieve server variables (such as REMOTE_ADDR). This is the equivalent to retrieving environment variables in CGI.

 

The simple CGI script retrieved the client's remote address and printed it back. Here is the equivalent CGI code:

 

Example2.c

 


/* 

	Build using the following command-line:



	bcc32 -WD example2.c

	(-WD is to build a 32-bit Windows DLL)



*/



#include "../include/httpext.h"



BOOL _export WINAPI  GetExtensionVersion(HSE_VERSION_INFO *pVer)

{

	long Version=MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR);

	pVer->dwExtensionVersion=Version;

	return TRUE;

}



DWORD _export WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpEcb)

{

	char *buf="HTTP/1.0 200 OK\nContent-type: text/html\n\n\n<HTML><H1>Hello World!<br>You are at the computer named "; 

	char RemoteAddr[80];

	unsigned long buflen=79;



	(*(lpEcb->GetServerVariable))(lpEcb->ConnID,"REMOTE_ADDR",RemoteAddr,&buflen);

	buflen=strlen(buf);

	(*(lpEcb->WriteClient))(lpEcb->ConnID,buf,&buflen,0);

	buflen=strlen(RemoteAddr);

	(*(lpEcb->WriteClient))(lpEcb->ConnID,RemoteAddr,&buflen,0);

	buf=".</H1></HTML>";

	buflen=strlen(buf);

	(*(lpEcb->WriteClient))(lpEcb->ConnID,buf,&buflen,0);

	return true;

}

 


While the example may look complex because of the parenthesis required for calling a function pointer. Trust me, it's no Lisp! We are passed in a pointer to an ECB (Extension Control Block) and we make calls to two functions:

 

GetServerVariable

BOOL WINAPI GetServerVariable(
HCONN
hConn,
LPSTR lpszVariableName,
LPVOID lpvBuffer,
LPDWORD lpdwSizeofBuffer
);

hConn

The current connection handle

lpszVariableName

The name of the variable whose contents you are retrieving.

lpvBuffer

The character buffer where you would like the contents stored.

lpdwSizeofBuffer

The address of a DWORD containing the size of the lpvBuffer. Upon return from the call, it will contain the number of bytes copied into lpvBuffer, including the null termination byte.

 

If the returned value is not TRUE, GetLastError() can be called to determine the reason for failure.

 

WriteClient

BOOL WriteClient(
HCONN
hConn,
LPVOID buffer,
LPDWORD lpdwBytes,
DWORD reserved
);

 

hConn

The current connection handle

buffer

A pointer to the character buffer containing the data to be written to the client

lpdwBytes

Address of DWORD containing the number of bytes to be written, and on completion, contains the number of bytes actually sent (synchronous only).

reserved

Contains flags: either HSE_IO_SYNC or HSE_IO_ASYNC (see Win32 SDK for more info)

 

Like most other Win32 calls, if this one fails by returning something other than TRUE, call GetLastError() to determine the problem.

 

This program produces the same results as the sample CGI script. To run it, simply copy the DLL to the web server's scripts directory and place a tag in a web page:

 

Because of the complexities involved in writing pure ISAPI, I found myself wrapping my needed calls in a class, which I will introduce here. This "framework" is by no means complete or usable by everyone. Its purpose is to make the examples easier to read (and write!)

 

ecb.h

 



#include "../include/httpext.h"



extern int Version;



class TExtCtrlBlock

{

   private:

	LPEXTENSION_CONTROL_BLOCK ECB;

	void GetMainServerVars();

   public:

	bool Secure;

 	// Server Variables-------------//

	char *ContentLength;		//

	char *ContentType;		//

	char *QueryString;		//

	char *RemoteAddr;		//

	char *RemoteHost;		//

	char *RemoteUser;		//

	char *ScriptName;		//

	char *ServerName;		//

	char *ServerPort;		//

	char *ServerPortSecure;		//

	char *ServerSoftware;		//

	char *URL;			// 2.0 Only!

	//------------------------------//





	HCONN ConnID;

	

	TExtCtrlBlock(EXTENSION_CONTROL_BLOCK *ECB);

	bool GetServerVariable(char *VarName,char *&Buffer,unsigned long size);

	bool WriteClient(char *buffer);



};

				

ecb.cpp

 


#include "ecb.h"

extern bool _import ExtensionMain(TExtCtrlBlock *ecb);

int Version;

TExtCtrlBlock::TExtCtrlBlock(LPEXTENSION_CONTROL_BLOCK lpECB)

{

	ECB=lpECB;	

	ConnID=ECB->ConnID;

	GetMainServerVars();

	if (ServerPortSecure[1]=='1') {

		Secure=true;

	}

	else

		Secure=false;

};



void TExtCtrlBlock::GetMainServerVars()

{

	ContentLength=new char[255];

	ContentType=new char[255];

	QueryString=new char[255];

	RemoteAddr=new char[255];

	RemoteHost=new char[255];

	RemoteUser=new char[255];

	ScriptName=new char[255];

	ServerName=new char[255];

	ServerPort=new char[255];

	ServerPortSecure=new char[255];

	ServerSoftware=new char[255];

	URL=new char[255];

	

	GetServerVariable("CONTENT_LENGTH",ContentLength,255);

	GetServerVariable("CONTENT_TYPE",ContentType,255);

	GetServerVariable("QUERY_STRING",QueryString,255);

	GetServerVariable("REMOTE_ADDR",RemoteAddr,255);

	GetServerVariable("REMOTE_HOST",RemoteHost,255);

	GetServerVariable("REMOTE_USER",RemoteUser,255);

	GetServerVariable("SCRIPT_NAME",ScriptName,255);

	GetServerVariable("SERVER_NAME",ServerName,255);

	GetServerVariable("SERVER_PORT",ServerPort,255);

	GetServerVariable("SERVER_PORT_SECURE",ServerPortSecure,255);

	GetServerVariable("SERVER_SOFTWARE",ServerSoftware,255);

	GetServerVariable("URL",URL,255);

}

bool TExtCtrlBlock::GetServerVariable(char *VName,char *&Buffer,unsigned long size)

{

	BOOL result;

	//     (*(ECB->WriteClient      ))(ECB->ConnID,buffer,&Bytes,0);

	result=(*(ECB->GetServerVariable))(ConnID,VName,Buffer,&size  ) ;

	if (result==FALSE)  {

		switch(GetLastError())

		{

		case ERROR_INSUFFICIENT_BUFFER:

			delete[] Buffer;

			Buffer=new char[size+1];

			return GetServerVariable(VName,Buffer,size);

			break;

		case ERROR_MORE_DATA:

			return true;

		case ERROR_INVALID_INDEX:

		case ERROR_INVALID_PARAMETER:

		case ERROR_INVALID_HANDLE:

		default:

			return false;

		}

	}

	return true;

}

bool TExtCtrlBlock::WriteClient(char *buffer)

{	

	unsigned long Bytes;

	Bytes=strlen(buffer);

	return (*(ECB->WriteClient))(ConnID,buffer,&Bytes,0);

}



BOOL _export WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVer)

{

   pVer->dwExtensionVersion=Version;

   return TRUE;

}



DWORD _export WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *lpEcb)

{

   TExtCtrlBlock *ecb;

   ecb=new TExtCtrlBlock(lpEcb);



   return ExtensionMain(ecb);

}

 

 

So far, the class only encapsulates GetServerVariable and WriteClient. The ISAPI code written with this class must have a function, ExtensionMain, which serves as the entry point (much like OwlMain) to the program. This function is passed a pointer to a TExtCtrlBlock class instance. This instance was created in "hidden" HttpExtensionProc.

 

Now, to issue a WriteClient command, we simply write:

 

Isn't that much easier to read!!! Now, here is the previous example, written with the simple class:

 

Example3.cpp

 


#include "ecb.h"





int ExtensionMain(TExtCtrlBlock *ecb)

{

	ecb->WriteClient("HTTP/1.0 200 OK\n");

	ecb->WriteClient("Content-type: text/html\n\n\n");

	ecb->WriteClient("<HTML>");

	ecb->WriteClient("<H1>Hello World!<br> You are the computer whose ");

	ecb->WriteClient("IP Address is ");

	ecb->WriteClient(ecb->RemoteAddr);

	ecb->WriteClient(". </H1>");

	ecb->WriteClient("</HTML>");

	return true;

}

 

 


Forms

We've created a simple, yet dynamic, web page using ISAPI server extensions. One of the more exciting uses of html/server extensions is forms. This allows the creation of pages that can respond to user input. The forms I created for these examples are quite simple, but get the point across. There are, however, several different kinds of controls available for creating html forms such as drop-down lists, radio-buttons, etc.

 

Our first form sample will simply ask a user for their name and print it out.

The HTML source for this page is overwhelmingly simple:

 


<HTML>

<HEAD>

<TITLE>Example 4 Form</TITLE>

</HEAD>



<B><I>Example 4</B></I>

<BR>

<HR>

<FORM METHOD=POST ACTION="/scripts/example4.dll">

<P>

Hi!  What's your name?:<BR>



<INPUT TYPE="TEXT" NAME="Name" VALUE="">

<INPUT TYPE="SUBMIT"  VALUE="Submit" NAME="SubmitButton">

</HTML>

 


For those unfamiliar with forms, the last two lines above </HTML> are crucial. They are creating two controls, on of type "TEXT" and the other of type "SUBMIT". These correspond, loosely, to Window's single-line edit control and OK button. When the Submit buttons is pressed, a the data is "POSTED" to the script specified in ACTION, as stated in the other important line:

When the submit button is pressed, our ISAPI DLL is called and the ECB member lpbData contains the information from the form. The control definition,

	<INPUT TYPE="TEXT" NAME="Name" VALUE="">

has three parts. The first is the type specifier ("TEXT"). The second is something like a variable name. When the contents of the edit control are passed to the ISAPI DLL, it will be prefaced by "Name=". If I were to type "Matt" in the text box and hit the submit button, the lpbData would look like this:

All of the variables and their values are sent in a continuous stream and can be read from lpbData, with one caveat: Only the first 49K is stored in lpbData. Data beyond that can be retrieved using the ReadClient() ECB call. There are some special cases to worry about:

 

I wrote some simple parsing functions that are contained in parse.h/parse.cpp. They simply write the lpbData stream to a INI file in the form:

[CGI]

Var=Value

Var=Value

. . .

 

I then use GetPrivateProfileString() to retrieve values from individual variables. While this method works, I did not complete the process fully. Since I use a possibly non-unique .ini filename, it is not safe for more than one client request. There would be little code to add to provide this functionality. Other methods of storing the variable/value pairs include linked lists and databases. There are several algorithms available that accomplish this task quite effectively.

For this example, I added a new member to the TExtCtrlBlock: char *ClientData, which points to the same data as lpbData.

 

parse.h

 


char stox(char ch0, char ch1);

bool ParseClientData(int Length,char *ClientData,char *inifile);

char* GetVar(char *var,char *inifile);

 

parse.cpp

 


#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#include <windows.h>



// Convert two ASCII characters to a two-digit hex character

char stox(char ch0, char ch1)

{

	char ret;



	if (ch0 >= '0' && ch0 <= '9')

		ret=(ch0-'0') * 16;

	else if (ch0 >= 'a' && ch0 <= 'f')	

		ret=(ch0-0x57) * 16;

	

	if (ch1 >= '0' && ch1 <= '9')

		ret=ret+(ch1-'0');

	else if (ch1 >= 'a' && ch1 <= 'f')	

		ret=ret+(ch1-0x57);



	return ret;

}



char* GetVar(char *var,char *inifile)

{

	char *ret;

	unsigned int err;



	ret=new char[255];



	err=GetPrivateProfileString("CGI",var," ",ret,254,inifile);

	return ret;	

}

bool ParseClientData(int Length,char *ClientData,char *inifile)

{

	FILE *ini;

	int i;

	char ch, ch0, ch1;

	int tp=0; //index into tch

	

	ini=fopen(inifile,"wt");

	if (ini ==NULL)

	{

		return false;

	}

	fprintf(ini,"[CGI]\n");

	for (i=0;i<Length;i++)

	{

		ch=ClientData[i];

		switch (ch)

		{

			case '%': 

				ch0=ClientData[i+1];

				ch1=ClientData[i+2]; // Get the next two characters

				i=i+2;

				fprintf(ini,"%c",stox(ch0,ch1));

				break;

			case '+': 

				fprintf(ini," ");

				break;

			case '&': 

				fprintf(ini,"\n");

				break;

			default:

				fprintf(ini,"%c",ch);

		}

	}	

	fclose(ini);

	return true;

}

 

example4.cpp

 


#include "ecb.h"

#include "parse.h"



int ExtensionMain(TExtCtrlBlock *ecb)

{

	bool ret;

	unsigned int ContentLength;

	ContentLength=atoi(ecb->ContentLength);

	ecb->WriteClient("HTTP/1.0 200 OK\n");

	ecb->WriteClient("Content-type: text/html\n\n\n");

	ecb->WriteClient("<HTML>");

	ret=ParseClientData(ContentLength,ecb->ClientData,".\\example4.ini");

	ecb->WriteClient("Hello, ");

	ecb->WriteClient(GetVar("Name",".\\example4.ini"));

	ecb->WriteClient(".");

	ecb->WriteClient("</HTML>");

	return true;

}

 

After, entering my name and hitting the submit button, I am delighted to be greeted by name!

 

 

Well…it's dynamic! But, alas not very exciting…Let's create a web page that does something useful. In times of late, I've found myself looking for a house in and around the Silicon Valley, where my wife and I work. One of the most important factors in determining which house to buy it to find out how much the loan will cost each month! Wouldn't it be nice to have a web page dedicated to calculating loan payments given amount, interest and years? I liked the idea so much, I wrote one!

 

First, let's lay out our web page for this simple example. We are going to need three TEXT boxes: one for the amount of the loan, interest rate (APR) and the number of years. And don't forget the SUBMIT button!

 

Notice that I've used the default values (VALUE="xxx") to pre-fill some of the boxes and to change the text of the submit button.

example5.htm

 


<HTML>

<HEAD>

<TITLE>Example 5 Form</TITLE>

</HEAD>



<B><I>Example 5</B></I>

<BR>

<HR>

<FORM METHOD=POST ACTION="/scripts/example5.dll">

<P>

<h1>Loan Payment Calculator</h1>

Amount: <INPUT TYPE="TEXT" NAME="Amount" VALUE="0.00">

Interest: <INPUT TYPE="TEXT" NAME="Interest" VALUE="8.00">

Years: <INPUT TYPE="TEXT" NAME="Years" VALUE="30"><p>

<INPUT TYPE="SUBMIT"  VALUE="Calculate Monthly Payment" NAME="SubmitButton">

</HTML>

Given the formula for figuring loan payments (see the Investment FAQ from misc.invest.misc), let's build the ISAPI backend for this page.

First, I found the formula:

            P*I*(1+I)^(Years*12)

payment = ------------------------

            ((1+I)^Years*12) -1

where P is the Principal, I is the Interest rate and Years, is, well the number of Years for the loan. Translated into C, it looks like this mess:

Payment=-((-Amount*Interest*(pow(1+Interest,Years*12)))/(pow(1+Interest,Years*12)-1));

 

Our ISAPI DLL must complete the following steps:

  1. Parse out the values for the HTTP variables Amount, Interest and Years and convert them to floats (doubles).
  2. Run them through the formula to get a payment
  3. Write the payment back to the client.

 

It's fairly straightforward. Here is how I did it:

example5.cpp

 


#include <stdio.h>

#include <math.h>

#include "ecb.h"

#include "parse.h"



int ExtensionMain(TExtCtrlBlock *ecb)

{

	double Amount;

	double Interest;

	double Years;

	double Payment;

	char PayStr[20];

	bool ret;

	unsigned int ContentLength;



	// Parse incoming variable/data pairs

	ContentLength=atoi(ecb->ContentLength);

	ret=ParseClientData(ContentLength,ecb->ClientData,".\\example5.ini");



	// Convert Amount, Interest and Years to floats	

	Amount=atof(GetVar("Amount",".\\example5.ini"));

	Interest=atof(GetVar("Interest",".\\example5.ini"));

	Years=atof(GetVar("Years",".\\example5.ini"));



	// Convert Interest to Monthly

	Interest=Interest/12;



	// Convert % to float number (i.e. 8% -> 0.08)

	Interest=Interest/100;



	// Calculate Payment and copy it into PayStr for output to client

	Payment=-((-Amount*Interest*(pow(1+Interest,Years*12)))/(pow(1+Interest,Years*12)-1));

	sprintf(PayStr,"%0.2f",Payment);





	// Write Web Page

	ecb->WriteClient("HTTP/1.0 200 OK\n");

	ecb->WriteClient("Content-type: text/html\n\n\n");

	ecb->WriteClient("<HTML>");

	ecb->WriteClient("Given a loan amount of $");

	ecb->WriteClient(GetVar("Amount",".\\example5.ini"));

	ecb->WriteClient(", at an interest rate of ");

	ecb->WriteClient(GetVar("Interest",".\\example5.ini"));

	ecb->WriteClient("% over ");

	ecb->WriteClient(GetVar("Years",".\\example5.ini"));

	ecb->WriteClient(" years, your payment would be: $");

	ecb->WriteClient(PayStr);

	ecb->WriteClient(".");

	ecb->WriteClient("</HTML>");

	return true;

}

 

Accessing a Database from an ISAPI DLL

We've covered the very basics of general-purpose ISAPI programming using Borland C++. Let's dive into the word of Web programming using databases. The most useful web pages available are powered by databases. While I am using C++Builder and ISAPI to write this code, keep in mind that while the code is VCL specific, you can use any database engine to accomplish the same tasks. Likewise, anything written in ISAPI can just as easily be written in CGI (should you find yourself with a non-ISAPI Web server!).

 

The basic steps in creating a database-aware ISAPI DLL are as follows:

  1. Create a new DLL (File | New | DLL)Create a new Data Module (File | New | Data Module)
  2. Drop a Table in the Data Module window
  3. Set the Table's Database Name, Table Name and Active properties, just as you would any C++Builder program.
  4. Write the two required ISAPI functions: GetExtensionVersion and HttpExtensionProc
  5. From within HttpExtensionProc, create an instance of the TDataModule
  6. You can now access the table (i.e. DataModule1->Table1->…) and use it as in any C++Builder program.

 

The first database example creates an on-line parts list. For the next two examples, I will be using the database Parts.db in the \CBuilder\Exampels\Data directory.

After creating a new DLL, a new DataModule and Table (steps 1 & 2), I need to set the Table's properties:

 

I set DatabaseName to BCDEMOS and TableName to PARTS.DB. Don't forget to set Active to true! If you don't you won't see any data!

Since we want the information contained in the Description field of PARTS.DB, we need to create a TField pointer, which I called Description. To "activate" this TField pointer, simply assign it to DataModule->Table->FieldByName(). After this is done, it will always point to the Description field of the currently active record.

As we progress through the table using Table->Next(), we want to print out the Description field. We can't pass a TField to WriteClient. To get the string portion of a TField, us TField.AsString.c_str(). All we do is iterate through the table using Table->Next() until Table->Eof() is true. With each record, call WriteClient with the c_str() of the Description field.

 

example6.cpp

 


//---------------------------------------------------------------------------

#include <vcl\vcl.h>

#include "ecb.h"

#include "unit1.h"

#pragma hdrstop

//---------------------------------------------------------------------------

//   Important note about DLL memory management:

//

//   If your DLL exports any functions that pass String objects (or structs/

//   classes containing nested Strings) as parameter or function results,

//   you will need to add the library BCBMM.LIB to both the DLL project and any

//   EXE projects that use the DLL.  This will change the DLL and its calling

//   EXE's to use the BCBMM.DLL as their memory manager.  In these cases,

//   the file BCBMM.DLL should be deployed along with your DLL.

//

//   To avoid using BCBMM.DLL, pass string information using "char *" or

//   ShortString parameters.

//---------------------------------------------------------------------------

USERES("example6.res");

USEDATAMODULE("Unit1.cpp", DataModule1);

USEUNIT("ecb.cpp");

//---------------------------------------------------------------------------

int ExtensionMain(TExtCtrlBlock *ecb)

{

   TField* Description;

   TDataModule1 *DM;

   DM=new TDataModule1(NULL);





   ecb->WriteClient("HTTP/1.0 200 OK\n");

   ecb->WriteClient("Content-type: text/html\n\n\n");

   ecb->WriteClient("<HTML>");

   ecb->WriteClient("<TITLE>Foo</TITLE>");

   ecb->WriteClient("<H2>Parts in Database</H2>\n");



   Description=DM->Table1->FieldByName("Description");

   while (!DM->Table1->Eof)

   {

   	ecb->WriteClient(Description->AsString.c_str());

   	ecb->WriteClient("<br>");

  	DM->Table1->Next();

   }

   ecb->WriteClient("</HTML>");



   delete DM;

   return true;

}//---------------------------------------------------------------------------

This small amount of code (including ecb.cpp) produces a web application that walks through a parts database and prints out the Descriptions of all of the parts.

 

Not too bad. Not too useful.

For the final example, let's combine parts of the last three web extensions. Let's create a form that asks the user to enter a search string and prints a listing of all of the parts containing that string in the Description field. Not only that, lets print a table containing the Description of the part, it's availability, price and estimated monthly payment on the Matt Lawrence Dive Shop credit card (3 years at 18.9%).

The only new twist is the searching. This is accomplished using a TQuery instead of TTable in our DataModule. This will allow us to narrow down our data set to only those parts in which the user is interested. To do the search, I will take the user's string and incorporate that into an SQL statement in the TQuery. Then I will iterate through the dataset, check the OnHand field for availability and print "Yes" or "No", print the price and produce a monthly payment (using the formula from example 4).

The result from this search on "Div" is:

 

 

This is now a useful web page for customers. It allows them to:

 

Conclusion

ISAPI is a flexible and powerful tool for making web pages dynamic and user-controllable. When combined with the limitless capabilities of databases, there is no end to the tools that can be created.

Please remember that all of the techniques discussed apply to other web extension APIs as well, such as CGI and NSAPI. If you find that you are using a Web server that does not support ISAPI, simply move to another API.

For more information on ISAPI and the servers that support it, please visit the Microsoft Web Site at http://www.microsoft.com.