The Wrapper DLL

A wrapper DLL is a dynamic link library that intercepts external library function calls made in an application. After a function call has been intercepted, the DLL has control of the application or process that instantiated it. The DLL can be designed to perform any task, set of tasks, or do nothing at all. Taking control of the instantiating application or process in this manner provides an efficient way to add, change, or remove functionality or scope without the burden of modifying the source code of the calling process or application.

For example, in the case of this conversion strategy, you can intercept Btrieve calls made in an application and change them to use ODBC to access Microsoft SQL Server instead of Btrieve itself. This technique allows the base application code to remain intact while the scope and/or targets of the operation change. This preserves the investment made in the base application code while seamlessly extending its capabilities to access SQL Server data.

Another methodology would be to create the wrapper DLL so that it retrieves data from SQL Server into buffers that it maintains on the client or another computer. Using this strategy, the application would then fetch data from the buffers instead of SQL Server directly using ISAM-like processing techniques. While this implementation allows the base application to access SQL Server without modification, it is complex and and can be difficult to implement. It is best suited for those instances when you do not want to use set operations or to develop a full ODBC- and SQL-based application. This methodology will not be discussed in the context of the conversion strategy presented in this paper.

Creating the Wrapper DLL

The wrapper DLL is designed to intercept Btrieve calls made in the base application and change them to use ODBC to access Microsoft SQL Server instead of Btrieve itself.

Determining the Functions That Must Be Wrapped

The wrapper DLL must cover the functions that the base application imports from the Btrieve library Wbtrv32.dll. You can determine these imports by using a binary file dumping utility to list the functions imported from the various external link libraries that the application references. The Microsoft Visual C++ equivalent is called Dumpbin.exe.

For example, the following samples list output excerpts from using the /IMPORTS switch of the DUMPBIN utility to determine the Btrieve functions imported from Wbtrv32.dll, and the /EXPORTS switch to determine the functions exported by Wbtrv32.dll.

DUMPBIN /IMPORTS BTRVAPP.EXE
Microsoft (R) COFF Binary File Dumper Version 4.20.6164
Copyright (C) Microsoft Corp 1992-1997. All rights reserved.

Dump of file BTRVAPP.EXE
File Type: EXECUTABLE IMAGE
         Section contains the following Imports:
            wbtrv32.dll
               Ordinal     3
               Ordinal     2
               Ordinal     1

DUMPBIN /EXPORTS WBTRV32.DLL
Microsoft (R) COFF Binary File Dumper Version 4.20.6164
Copyright (C) Microsoft Corp 1992-1997. All rights reserved.

Dump of file wbtrv32.dll
File Type: DLL
         Section contains the following Exports for wbtrv32.dll
                   0 characteristics
            31D30571 time date stamp Thu Jun 27 15:04:33 1996
                0.00 version
                   1 ordinal base
                  10 number of functions
                  10 number of names

            ordinal hint   name

                  1    0   BTRCALL  (000014EC)
                  8    1   BTRCALLBACK  (00003799)fs
                  2    2   BTRCALLID  (00001561)
                  9    3   DBUGetInfo  (00008600)
                 10    4   DBUSetInfo  (000089E8)
                  3    5   WBRQSHELLINIT  (00002090)
                  4    6   WBSHELLINIT  (00002A6A)
                  7    7   WBTRVIDSTOP  (00001812)
                  5    8   WBTRVINIT  (00002A4F)
                  6    9   WBTRVSTOP  (000017D2)

The information presented in these output excerpts is used to create the definition file for the wrapper DLL. Performing these tasks allows you to implement only the exported functions from Wbtrv32.dll that are used by the base application in the wrapper DLL. This eliminates the need to implement exported functions that are never used by the base application.

Creating a DEF File for the Wrapper DLL Function Mapping

After the Btrieve import functions in the base application have been determined, the next step is to map these functions to export functions in the wrapper DLL. The mapping is performed through the use of a definition file for our wrapper DLL. The definition is created using the following steps:

  1. Use DUMPBIN /IMPORTS application_file_name to obtain the list of imported symbols for Wbtrv32.dll. Based on the preceding sample output, the function symbols in Btrvapp.exe imported from Wbtrv32.dll are ordinals 3, 2, and 1.

  2. Use DUMPBIN /EXPORTS DLL_file_name to obtain the list of exported symbols for the DLL in question. The symbols appear in the "name" column of the table whose headings are "ordinal," "hint," and "name." In the example, these correspond to BTRCALL, BTRCALLID, and WBRQSHELLINIT.

  3. Create a DEF file that contains an EXPORTS section with the names of the functions listed in the "name" column of the DUMPBIN output.

The exact import/export combination will vary depending on what Btrieve functionality is used in the base application.

Implementing the Btrieve Functions within the Wrapper

The next step is to develop the basic framework within the wrapper so that all of the Btrieve operations are implemented properly. Most of the Btrieve operations will be performed using the BTRCALL and BTRCALLID. Their equivalent functions within the wrapper must be designed to address the operations used by the base applications. Each of these functions will have all of the data necessary to perform the operations by using the input parameters they receive.

The most important parameters will be the posBlock, operation, dataBuffer, keyBuffer, and ckeynum parameters. The posBlock parameter is discussed in "Addressing the Btrieve posBlock Handle" that follows. The operation parameter designates what operation is to be performed. The contents of the other parameters will be dependent on the operation being performed. You must use these parameters in the same way they would be used if the function was being processed by Btrieve. The following code fragment shows how the B_GET_EQUAL operation is handled by the BTRCALL function within Mybtrv32.dll:

DllExport int __stdcall BTRCALL (BTI_WORD operation, BTI_VOID_PTR posBlock,
BTI_VOID_PTR dataBuffer, BTI_ULONG_PTR dataLen32,
        BTI_VOID_PTR keyBuffer, BTI_BYTE keyLength, BTI_CHAR ckeynum)
{
SQLRETURN rc;    // Btrieve operation return code

/*Perform tasks based on operation used in the calling application */
switch(operation){

case B_GET_EQUAL:
// Get the first Title-Publisher record that matches the search
// criteria
if (!strcmp(posBlock, "titlepub.btr")){//Are we accessing title-publisher info
rc = GetTitlePublisher(henv1, hdbc1, hstmt, B_GET_EQUAL, ckeynum,keyBuffer);
if (rc != B_NO_ERROR)
      return rc;

//Copy title-publisher data to titlepub record structure tpRec
memcpy(dataBuffer, &tpRec, sizeof(tpRec));
}
else {   // Accessing sales info
rc=GetSales(henv1, hdbc2, hstmt2, B_GET_EQUAL, keyBuffer);
   if (rc != B_NO_ERROR)
return rc;

//Copy sales data to sales record structure salesRec
memcpy(dataBuffer, &salesRec, sizeof(salesRec));
}
break;

The preceding code fragment determines the target SQL Server table. The target table is set by examining the posBlock parameter by using the strategy outlined in "Translating Btrieve Calls to ODBC and SQL Using the Wrapper" that follows. After the target has been determined, a function is called to retrieve the first data record that matches the keyBuffer and ckeynum values from the appropriate SQL Server cursor. The same methodology is used throughout the wrapper DLL. Figure 2 shows the wrapper DLL concept.

Figure 2. The wrapper

In Figure 2, the base application, Btrvapp.exe, makes a request for the title and publisher information for TitleID BU1032. While processing this request, the application calls the Btrieve function BTRCALL to get the next record from the Titlepub.btr file. The wrapper DLL is designed to mimic the functionality of this function while accessing SQL Server data instead. It examines the opcode parameter and then performs the appropriate ODBC and SQL operations needed to satisfy the request. In this case, the wrapper will make ODBC and SQL calls to retrieve the record for TitleID BU1032 from the titlepub table in the database. After the data is received, it returns the data to the base application using the record data buffer passed as part of the original BTRCALL function call.

Accessing the Wrapper DLL in the base application

After the wrapper DLL has been completed, the next step is to get the base application to reference the wrapper DLL instead of the Btrieve DLL. This step is easy. Link the application with the wrapper DLL and ODBC library files (LIB) rather than with the Btrieve library file. Recompilation of the base code is not necessary. After the linking process has been completed, the base application will access SQL Server and not the Btrieve microkernel.

Translating Btrieve Calls to ODBC and SQL Using the Wrapper

The base application, Btrvapp.exe, can now use the wrapper DLL to access Microsoft SQL Server data. Essentially, the wrapper makes SQL Server look like Btrieve to Btrvapp.exe. How to use ODBC and SQL to access SQL Server data within the scope of the wrapper DLL is the next stage to consider. The wrapper is designed to use ISAM processing techniques to access SQL Server, which will be familiar to a Btrieve developer. The implementation successfully accesses SQL Server data without making changes in the base application code. However, why these design techniques are not optimal will be discussed later in this paper.

Addressing the Btrieve posBlock handle

The second parameter passed to BTRCALL and BTRCALLID is posBlock. In the Btrieve environment, this is a unique area of memory associated with each open file that contains logical positional information for accessing records. The Btrieve libraries initialize and use this memory area to perform data functions. The Btrieve application itself does not know about the contents of the posBlock. It simply passes a pointer to this posBlock into every Btrieve call that accesses the same data file.

Because the wrapper DLL doesn't need to maintain any Btrieve-specific data within the posBlock, it is free to use this memory area for its own purposes. Within the example wrapper, this will be the unique identifier that identifies what SQL Server data is affected by the requested operation. The contents of the posBlock maintained within the wrapper DLL implementation are not important as long as each memory block is unique to each corresponding SQL Server table set.

For example, Btrvapp.exe references two Btrieve files, Sales.btr and Titlepub.btr, where Sales.btr contains sales information for each title and Titlepub.btr maintains the title and publisher for each title. These files correspond to the bsales and titlepublishers tables that were created in the pubs database by the Morepubs.sql script that is part of the sample applications for this paper. In Btrvapp.exe, the B_OPEN operation opens the requested Btrieve file and creates its respective posBlock.

In the wrapper, the same posBlock will from this point forward reference a particular table by name. The wrapper DLL can be designed to store any form of a unique identifier that represents the SQL Server data that it accesses. Table names are used in the context of this migration strategy for ease of presentation. The keybuffer parameter contains the file name of the Btrieve file to be opened when B_OPEN is called. The wrapper DLL implementation of the B_OPEN function sets the posBlock equal to this file or table name. The following code fragment, taken from the wrapper DLL B_OPEN implementation (see source file Mybtrv32.c for more details), demonstrates this concept:

/*Step1:*/
if (strlen((BTI_CHAR *) keyBuffer) <= MAX_POSBLOCK_LEN)
memcpy((BTI_CHAR *) posBlock, (BTI_CHAR *) keyBuffer,  keyLength);
else   
memcpy((BTI_CHAR *) posBlock, (BTI_CHAR* ) keyBuffer,
MAX_POSBLOCK_LEN -1);

In the example, the Sales.btr posBlock is set to "Sales.btr" and the Titlepub.btr posBlock is set to "Titlepub.btr." Btrvapp.exe will always know what Microsoft SQL Server table set is being referenced based on the file name referenced in the posBlock.

The same data record structure formats will be used in both the base application and the wrapper DLL. This will allow the wrapper DLL to transport record data between SQL Server and Btrvapp.exe in the same format as if the data was coming from Btrieve. The data record structures used in Btrvapp.exe and Mybtrv32.dll are presented in the following example. For further details, see Btrvapp.c and Mybtrv32.c.

/************************************************************
   Data Record Structure Type Definitions
************************************************************/
// titlepub record structure
struct{
   char   TitleID[7];       // string
   char   Title[81];        // string
   char   Type[13];         // string
   char   PubID[5];         // string
   float   Price;           // money
   float   Advance;         // money
   int   Royalty;           // integer
   int   YTD_Sales;         // integer
   char   PubName[41];      // string
   char   City[21];         // string
   char   State[3];         // string
   char   Country[31];      // string
}tpRec;

// sales record structure
struct
{
   char   StorID[5];        // string
   char   TitleID[7];       // string
   char   OrdNum[21];       // string
   int   Qty;               // integer
   char   PayTerms[13];     // string
}salesRec;

Establishing the ODBC connections and initializing data access

Within the sample wrapper implementation, the B_OPEN operation establishes a connection to SQL Server for each table set referenced by the base application Btrvapp.exe. The operation also creates and opens the cursors used to reference the SQL Server data tables. The cursors are opened on the entire table without any WHERE clauses to restrict the number of rows returned. These connections and cursors will be used throughout Mybtrv32.dll to reference the SQL Server tables. To avoid the time and processing overhead associated with making or breaking connections to the server, the connections are not terminated until the application is closed.

The connection and cursor implementation mentioned in the previous paragraph was chosen for two reasons. First, because it simulates the way a normal Btrieve application would access a Btrieve file, one posBlock for every open file referenced by the application. The second reason is to demonstrate inefficient use of connection management when accessing SQL Server. Only one connection is needed in the context of this wrapper implementation because multiple server cursors can be opened and fetched concurrently on a single connection. Thus, the second connection is only providing overhead within the application. A more optimal connection management methodology would be to use only single connection with multiple cursors opened on that connection.

ODBC and SQL implementation within the Wrapper

There are many different techniques to use ODBC and SQL to access SQL Server data. The wrapper Mybtrv32.dll uses server-side cursors. Cursors were chosen for several reasons:

Each Btrieve operation that is performed in the base application is ported to an ODBC and SQL equivalent within the wrapper DLL. Some of the operations, like the B_SET_DIR operation, are not applicable to the SQL Server environment and do nothing within the wrapper DLL. Optimal implementation strategies of ODBC and SQL for both the wrapper DLL and the final application port are discussed in "Converting the Application to ODBC and SQL" that follows.

Error handling

The wrapper DLL must use Btrieve return codes when exiting each function. Each wrapper function must return B_NO_ERROR or a Btrieve error code corresponding to the type of error that was encountered. The return codes must be identical. As a result, the base application code does not know that the library function it is using is accessing SQL Server instead of Btrieve. You must return the Btrieve return codes that are expected by the base application in order for the wrapper DLL to work properly.

However, there is not a direct mapping of Microsoft SQL Server to Btrieve error codes. You must translate all SQL Server errors encountered in the ODBC code of the wrapper DLL to a Btrieve return code equivalent. The following example taken from the MakeConn function in the wrapper DLL source file Mybtrv32.c demonstrates this concept.

// Allocate a connection handle, set login timeout to 5 seconds, and
// connect to SQL Server
   rc = SQLAllocHandle(SQL_HANDLE_DBC, henv, hdbc);

// Set login timeout
   if (rc == SQL_SUCCESS || rc == SQL_SUCCESS_WITH_INFO) 
      rc=SQLSetConnectAttr(*hdbc, SQL_LOGIN_TIMEOUT,(SQLPOINTER)5, 0);
   else{
   // An error has been encountered: notify the user and return
      ErrorDump("SQLAllocHandle HDBC", SQL_NULL_HENV, *hdbc,
SQL_NULL_HSTMT);
return B_UNRECOVERABLE_ERROR;
}

The preceding code fragment allocates a connection handle for use in the wrapper DLL. If an error is encountered the SQL Server code must be mapped to a Btrieve error code. Because Btrieve doesn't involve the concept of connection handles, there is no corresponding error code. Therefore, you must choose a Btrieve return code that closely matches the severity or context of the message. Because a handle is critical for the application, the Btrieve return code B_UNRECOVERABLE_ERROR was used in the example. You can choose any Btrieve return code provided that the base application is designed to address it.