Calling 16-bit DLLs from Windows 95

Incorporating 16-bit DLLs into 32-bit applications

Steve Sipe

Steve is a developer with GE Fanuc Automation in Charlottesville, Virginia. He can be reached at steve.sipe@cho.ge.com.


Porting 16-bit Windows 3.x applications to 32-bit Windows 95 can sometimes be difficult, especially if the 16-bit apps depend on third-party DLLs. In some cases, the 32-bit, third-party DLLs may not yet be available or, if they are, the expense of replacing these DLLs with 32-bit versions is prohibitive. These are the problems I encountered when converting a Windows 3.x application that used a 16-bit TWAIN scanner DLL to a Windows 95 app.

In this article I'll present a technique for easily incorporating 16-bit DLLs into 32-bit applications. The source code and DLLs (available electronically; see "Availability," page 3) will let you build interfaces between 32-bit applications and 16-bit DLLs using only Visual C++ 2.x and Visual C++ 1.x.

The Problem

Most scanners currently have only 16-bit device drivers that communicate with 16-bit TWAIN DLLs. Unfortunately, 32-bit applications can't directly call a 16-bit DLL. Microsoft suggests two ways to accomplish this:

On one hand, implementing a server .EXE seemed to be the easiest approach. It didn't involve generating, assembling, and linking "thunk scripts" to thunking-layer DLLs. On the other hand, the server .EXE approach had a major drawback--I could not display TWAIN modal dialog boxes in my application and have it automatically disable the application's main window. Since each .EXE has its own task space, making a dialog box in one task appear modal to another task is not a simple process. Implementing a thunking-layer DLL would solve this problem because the DLL would be in the same task as my application's main window. I was hesitant about thunking mainly because of its reputation as a "black art." In spite of this, I began to implement a thunking layer so that my 32-bit application could call 16-bit TWAIN DLL functions directly.

Why is Thunking Necessary?

Windows 95 and Windows NT use flat memory addressing. Pointers exist as 0:32 (flat) addresses. Windows 3.x is based on segmented memory addressing, and pointers exist as 16:16 (segment, offset) addresses. The two types of pointers are not directly interchangeable.

Enter the thunking layer. Windows 95 implements a thunking layer to allow existing 16-bit Windows applications to call 32-bit Windows 95 operating-system code. This thunking layer has several useful functions. One function is the translation of 0:32 pointers to 16:16 pointers, something I definitely needed to accomplish. Another important function is the ability to call 16-bit code from 32-bit applications. The Windows 95-specific thunking solution (known as the "flat thunk") directly supports the thunking layer built into Windows 95. This would enable me to pass pointers across 32-bit to 16-bit boundaries as well as call 16-bit code. The only drawback is that this thunking approach is only available when running on Windows 95. For my application, this was not a concern because our target platform was Windows 95 anyway.

The Adventure Begins

Armed with my Microsoft Developer's CD, Win32 SDK, MASM, and Visual C++, I began to explore the basics of building thunking DLLs. I found a good article on the Developer's CD that gave me an idea of exactly what I wanted to do. It described the steps involved in building a thunking layer:

I started by analyzing the sample program from the Developer's CD. I examined each piece of code to determine which pieces were important for my implementation. Some things became immediately apparent. Building and maintaining my thunking layer involved incorporating some new (and old) tools into my build environment: the thunk compiler, assembler, and the new 16-bit version of the RC compiler (required for version stamping).

I began to implement my first thunking function call. First, I added a prototype in my 32-bit application for the 32-bit side of the thunking function; see Example 1(a). Then, I added the code for MyNew32Func() to the 32-bit thunk DLL and exported it; refer to Example 1(b). Next, I wrote an entry for the 16-bit function call in the thunk script using the C-like language supported by the thunk compiler; see Example 1(c). Finally, as Example 1(d) illustrates, I added the code for MyNew16Func() to the 16-bit DLL and exported it.

I then ran the make files that generated an assembly-language file from the thunk script, assembled it, compiled the DLLs, and linked in the thunk object files.

It quickly became apparent that adding new functions would be a tedious task. I soon began to search for an approach that would yield the desired results with much less effort, preferably one that didn't require using the thunk compiler and assembler when adding new functions.

The Design

One approach for simplification would be to devise some "generic" function that I could call in my 32-bit and 16-bit thunking DLLs. Using one function meant that I would only need to write the thunk script once, compile it, then assemble it. From that point on, I would only have to link to the thunk object because the function definition would not need to change. I formulated a plan. I knew that I could pass variable arguments to C-style functions, but the thunk compiler requires Pascal functions (which have a fixed number of arguments). I also wanted the thunking layer to translate my pointers from 0:32 style pointers to 16:16 pointers.

I decided to build a simple function in the 32-bit DLL that is exported with the C calling convention and then use the C-variable-argument routines to access the parameters. This function would be responsible for copying the variable parameters I specified to fixed parameters and calling my 16-bit thunk function. The next challenge was specifying the types of parameters that I wanted to pass. Were they pointers requiring translation, or just values?

Having done some MFC OLE programming, I was familiar with the proxy-dispatch setup it uses. A proxy specifies an array of parameter types that it passes to InvokeHelper() along with a dispatch ID. The parameter-type array specifies the type of each parameter (see the DTC_xxx values in parm.h for the types that my implementation uses). The dispatch ID corresponds to an entry in the dispatch table of the connected OLE object. This entry specifies the appropriate function to call.

The OLE interface that InvokeHelper() calls also performs what Microsoft describes as "marshaling" and "unmarshaling" of data. This basically means that data is grouped together on the sending end and ungrouped on the receiving end. The Invoke16Func() function that I eventually implemented serves the same purpose as the OLE InvokeHelper() method. It is called by a proxy method and is responsible for grouping variable parameters into fixed parameters so the thunking layer can convert them properly. On the 16-bit side the thunk function ungroups the parameters, places them on the stack frame using inline assembly statements, and looks up the corresponding function (in the dispatch table) and calls it.

The New Process

I now had an easy way to define new 16-bit functions and call them from 32-bit applications--a way that no longer required the tedious iterations of building, compiling, and assembling thunk scripts for each new function.

First, I defined a simple proxy in my 32-bit application; see Example 2(a). I chose to use C++ but could have just as easily used C. This proxy described my parameter types for the marshaling interface, then called the interface. The return code (ulError) contains the return code from the 16-bit function.

In the 16-bit thunk DLL, I added the dispatch table in Example 2(b) and added the code for MyNewFunc(); see Example 2(c).

That's it. I just recompiled the 16-bit thunk DLL and recompiled my 32-bit application. No more thunk compiler, thunk scripts, MASM, or the like. I had a simple way to implement 16-bit function calls in 32-bit code. In fact, I could use the 16-bit header files as a starting point for building my proxies.

A Small Hurdle

Then came my first obstacle. I found that quite a few of the functions in my favorite graphing package need to use floats and doubles, but the thunk compiler doesn't support these data types. After a little research, I found that converting numbers between 32-bit and 16-bit apps was fairly straightforward. The only tricky data type is int. The int type is 32 bits in a 32-bit application and 16 bits in a 16-bit application. I was familiar with this ambiguity because OLE forces you to use short to describe 16-bit integers and long to describe 32-bit integers. It turned out that all the other C data types are the same size on both the 32-bit and 16-bit sides.

Realizing that manually converting numbers would not be a problem, I still had one last hurdle to overcome--passing floating-point values across the thunk boundary. I speculated that it might be possible to build an array of double values and load it with the various double parameters that I would need, then pass a pointer to the array across the thunk boundary. This pointer could then be translated to a proper 16-bit pointer. This works because the values don't really need any translation across the boundary. I can load up all my values and just pass one pointer to the appropriate array. This also turned out to be a more-efficient approach. I could now pass quite a few values using only one fixed parameter.

Some Minor Restrictions

My implementation has a few minor restrictions, mainly because of limitations in the thunk compiler. You can pass a maximum of 11 pointers (values only take a maximum of two pointers: one for doubles and one for all other value types). Of course, you can easily get around this restriction by passing structure pointers. I haven't found any functions yet that require 11 pointers.

Each of the major data types are defined in the header file parm.h. Keep in mind that the only important thing is the size of the data. In other words, an unsigned short is 16 bits and a signed short is 16 bits. The data definition DTC_SHORT defines a 16-bit value; the sign bit does not matter to the Invoke16Func() interface.

What Tools do I Need?

Given the DLLs and samples provided, you should be able to implement your own interfaces to 16-bit DLLs. As long as you have no need to modify the thunk-script behavior, you will only need Microsoft Visual C++ 2.x for the 32-bit side and Visual C++ 1.x for the 16-bit side. I have also included a quick-and-dirty program called "stamp" to update the version stamp of the 16-bit DLL. This eliminates having to run the new 16-bit RC compiler to update the version stamp.

Helpful Hints

There are a few things to keep in mind when implementing your proxies and dispatch methods:

One last thing:The 32-bit DLL can fail to load for various reasons, but usually for reasons related to the 16-bit DLL. Verify that the 16-bit DLL's version stamp was updated. Also, verify that any imported 16-bit DLLs are in the path.

The Sample Code

The PARMTEST sample application (available electronically) is an MFC application that calls a simple 16-bit DLL named "DummyDLL." DummyDLL implements a couple of simple functions. One displays a modal message box; the other draws random shapes into the caller's device context. The file dummypxy.cpp implements the proxy methods for DummyDLL. The dispatch table for the functions is contained in the file parm16ds.c.

Example 1: Implementing a thunking function call. (a) Adding a prototype in a 32-bit application for the 32-bit side of the thunking function; (b) adding and exporting code for MyNew32Func() to the 32-bit thunk DLL; (c) entry for the 16-bit function call in the thunk script; (d) adding and exporting code for MyNew16Func() to the 16-bit DLL.

(a)
void MyNew32Func(HWND hDlg, LPSTR lpszSomeText);

(b)
void MyNew32Func(HWND hDlg, LPSTR lpszSomeText)
{
   // Call the 16 bit thunked function
   MyNew16Func(hDlg,lpszSomeText);
}

(c)
void MyNew16Func(HWND hDlg, LPSTR lpszSomeText)
{
   lpszSomeText = input;
}

(d)
void FAR PASCAL _export MyNew16Func(HWND hDlg, LPSTR lpszSomeText)
{
    // Set a dialogbox field from the 16 bit side
    SetDlgItemText(hDlg,lpszSomeText);
}

Example 2: (a) Defining a proxy in a 32-bit application. (b) Adding the dispatch table in the 16-bit thunk DLL; (c) adding code for MyNewFunc().

(a)
LONG CMyProxy::MyNewFunc (HWND hDlg, LPSTR lpszSomeText)
{
    unsigned long ulError;
    BYTE parms[] = {DTC_HWND,DTC_PTR,DTC_END};
    
    Invoke16Func(0x01,&ulError,hDlg,lpszSomeText);
    
    return((LONG) ulError);
}

(b)
BEGIN_DISPTABLE
    DISP_FUNCTION(0x01,MyNewFunc)
END_DISPTABLE

(c)
LONG FAR PASCAL _export MyNewFunc(HWND hDlg, LPSTR lpszSomeText)
{
    MessageBox(hDlg,lpszSomeText,"Hello from 16 Bits!",MB_OK);
    return(0L);
}