The Visual Programmer

Joshua Trupin

Joshua Trupin is a software developer specializing in C/C++ and Visual Basic apps for Windows. He can be reached at 75120.657@compuserve.com or geeknet@ix.netcom.com.

Click to open or copy the VISUAL project files.

QI have a 16-bit DLL that has served me well, but now I want to jump to 32-bit Visual Basic® 4.0. I know everyone says you can't call a 16-bit DLL from a 32-bit program, but there has to be some way to do it.

AThere are going to be a lot of problems like this cropping up as people realize their Visual Basic code depends on DLL exports that are now unreachable. There are two ways to get around this problem. The hard way, of course, is to use the supplied Windows® thunk. Unfortunately, it works a bit differently in Windows® 95, Windows NT™, and Win32s®. And who wants to deal with pointer conversion in Visual Basic?

There is a system service that provides automatic interapplication conversion—OLE automation. Instead of worrying about the 16-to-32-bit translation, you can wrap the exported DLL calls with a small 16-bit program written in Visual Basic 4.0. Then you can implement a set of OLE automation exports from the small 16-bit program, matching the DLL exports one-to-one. Since you can go either way with automation, any 16- or 32-bit program can then use the exported DLL functions.

For instance, you might have an old, comfortable library called YELLER.DLL (see Figure 1). It doesn't do that much, but you've been best friends over the years and you're not ready to put it down just yet. Yeller knows two tricks: it can take a number and return an informational string (like "This is number x"), and it can raise a real number to an integer power and return the result.

Figure 1 YELLER.DLL

YELLER.DEF


 ; module-definition file for generic -- used by LINK.EXE

LIBRARY      Yeller
DESCRIPTION  'Old Yeller 16-bit Sample DLL'
EXETYPE      WINDOWS

CODE  PRELOAD FIXED
DATA  PRELOAD SINGLE

HEAPSIZE     2048

EXPORTS      
       WEP        @1 RESIDENTNAME
       GetString  @5
       GetStringA @10
       Powers     @15

YELLER.C


 #include <windows.h>
#include <windowsx.h>
#include <math.h>

//========================================================
// Takes an integer and creates "String number n" from it.

LPSTR _export WINAPI GetString(int i)
{
       LPSTR rval;
       rval = GlobalAllocPtr(GHND, 32);
       wsprintf(rval, "String number %d", i);
       return rval;
}
 
//========================================================
// Prints text into a passed-in string, and returns the
// total number of bytes used.
 
int _export WINAPI GetStringA(LPSTR strg, int i)
{
       return wsprintf(strg, "StringA number %d", i);
}

//========================================================
// Takes two integers and returns the value of the first
// raised to the second.

double _export WINAPI Powers(int x, int y)
{
       static double dbl;
       
       dbl = pow(x, y);
       return dbl;
}

So write your own 16-bit application with one module (BAS) and one class module (CLS) (see Figure 2). The module should contain the Declare functions for the appropriate DLL calls as well as a Sub Main. These Declares worked great in Visual Basic 1, Visual Basic 2, and Visual Basic 3. They'll still work with Visual Basic 4.0—the 16-bit version—and that's all you need to create a bit-independent OLE automation server. The Sub Main can be empty, but it has to be there for the final EXE to run correctly.

Figure 2 VISTHUNK

VISTHUNK.BAS


 Attribute VB_Name = "basVisThunk"
Declare Function OGetString Lib "yeller.dll" Alias "GetString" (ByVal x As _ Integer) As Long ' For LPSTR
Declare Function OGetStringA Lib "yeller.dll" Alias "GetStringA" (ByVal stg As _ String, ByVal x As Integer) As Integer
Declare Function OPowers Lib "yeller.dll" Alias "Powers" (ByVal x As Integer, _  ByVal n As Integer) As Double
Declare Function lstrcpy Lib "kernel" (ByVal lpString1 As String, ByVal _ 
lpString2 As Any) As Long

Sub Main()
End Sub

THUNK.CLS


 VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "Thunk"
Attribute VB_Creatable = True
Attribute VB_Exposed = True


 Function GetString(i As Integer) As String
    Dim l As Long
    Dim y As String * 32
    
    l = OGetString(i)
    n = lstrcpy(y, l)

    GetString = y
End Function


Function GetStringA(str As String, x As Integer) As Integer
    Dim irc As Integer
    irc = OGetStringA(str, x)
    GetStringA = irc
End Function

Function Powers(x As Integer, n As Integer) As Double
    Dim dbl As Double
    dbl = OPowers(x, n)
    Powers = dbl
End Function

Let's look at the Powers call first. Notice that I renamed the function OPowers in the Declare statement in VISTHUNK.BAS. I'm defining a function called Powers for OLE automation, so I can avoid name overlap. In a 16-bit Visual Basic program, you would call OPowers like this:


 Dim dbl as Double
dbl = OPowers(x, n)

To create an OLE wrapper, you do the same thing: define a function in the class module called Powers, whose only purpose is to call the DLL function and return the right value. The new Powers function should have arguments and a return value that match the DLL function. All you have to do is pass the incoming data to the DLL and return whatever the DLL function returns.


 Function Powers(x As Integer, n As Integer) As Double
    Dim dbl As Double
    dbl = OPowers(x, n)
    Powers = dbl
End Function

Functions that return strings can be trickier to use. Since DLL-based functions will, as a rule, return LPSTRs, you must convert them into Basic strings. When an LPSTR return type is indicated, you have to declare it as a Long and do an lstrcpy into a preallocated fixed-length Visual Basic string (see Figure 3). The lstrcpy step adds maddening complexity. Fortunately, it can be masked by your thunking program even as you do the 16-to-32 conversion. If your DLL returns an LPSTR, make your OLE wrapper return a String and do the lstrcpy in the thunking layer.


 Function GetString(i As Integer) As String
    Dim l As Long
    Dim y As String * 32

    l = OGetString(i)
    n = lstrcpy(y, l)


     GetString = y
End Function

Figure 3 Converting Strings


    Dim stg As Long
   Dim y As String * 32 ' Or some other value - demonstration purposes

   ' Call the DLL GetString function. It should return "String number 5"
   stg = OGetString(5)

   ' stg is an LPSTR. Copy it into the pre-allocated VB string.
   n = lstrcpy(y, stg)

   ' Use the pre-allocated string to set lblGetString's Caption.
   lblGetString.Caption = y

If the string is in a parameter, and not a return value, you don't have to deal with this conversion at all.


 FunctionGetStringA(strAsString,xAsInteger)AsInteger
    Dim irc As Integer
    irc = OGetStringA(str, x)
    GetStringA = irc
End Function

You now have three wrapping functions in your middleware layer. Give the program a useful name like VISTHUNK. Give the class module a name like THUNK. Save everything, compile it into an EXE, and run it once to register it. You're now ready to adapt your 32-bit program to use OLE automation instead of calling DLL exports.

You could try to open the 16-bit project in 32-bit Visual Basic 4.0, but when you run it any call to the 16-bit DLL will return "error 48: Error in loading DLL." So ditch the Declares. Just cut them right out of the BAS file and dump the file if you feel like it. Instead, go to the Tools/References menu and select VISTHUNK as a new reference. Then in the declarations section of your program's form, create an instance of the thunking program:


 Dim Obj as New VisThunk.Thunk

Your 16-bit program already has code set up to call the DLL directly. Anywhere one of these references is found, it should be replaced with an equivalent reference to this Obj. If you have a line like


 a = GetStringA(strg, 5)

you just need to change it to:


 a = Obj.GetStringA(strg, 5)

This will call the GetStringA member function within VisThunk.Thunk, which will in turn call GetStringA in the 16-bit DLL. The strg variable will be updated the same way as before.

Figure 4 shows how to update some sample code snippets. With just a few changes, and the magic of OLE automation, your 32-bit program will once again work.

Figure 4 Updating Code

Old


 Sub btnGetString_Click ()
    Dim stg As Long
    Dim y As String * 32
    stg = GetString(5)

    n = lstrcpy(y, stg)
    lblGetString.Caption = y
End Sub

New


 Private Sub btnGetString_Click()
    Dim y As String * 32
    y = Obj.GetString(5)
    lblGetString.Caption = y
End Sub

Old


 Sub btnGetStringA_Click ()
    Dim y As String * 32
    i = GetStringA(y, 14)
    lblGetStringA.Caption = y
End Sub

New


 Private Sub btnGetStringA_Click()
    Dim y As String * 32
    i = Obj.GetStringA(y, 14)
    lblGetStringA.Caption = y
End Sub

Old


 Sub btnPowers_Click ()
    Dim z As Double
    z = Powers(2, 13)
    lblPowers.Caption = z
End Sub

New


 Private Sub btnPowers_Click()
    Dim z As Double
    z = Obj.Powers(2, 13)
    lblPowers.Caption = z
End Sub

QAm I going to have to rewrite all my code if I want to port my application to Microsoft Access for Windows 95 and take advantage of 32-bit processing?

AIf you designed an application in Microsoft Access 2.0, you're actually a bit ahead of the game (at least compared to Visual Basic 3.0 users porting to Visual Basic 4.0). You don't have to worry about how your VBXs will convert to OLE controls, since you're already using them. There's a bunch of interface changes in Microsoft Access 95 that don't make much of a difference to the coder. For instance, many of the Wizards have been overhauled and some menu choices have been moved around for compatibility with other Office for Windows 95 applications.

There are some major improvements in Microsoft Access for Windows 95: Visual Basic for Applications, OLE automation, data access objects (DAO), plus the Microsoft Jet 3.0 engine, which has been cleaned up and 32-bitized. I'll summarize each area.

First of all, Microsoft Access 95 now uses the new Visual Basic for Applications, as do all Office for Windows 95 apps except for Word. Visual Basic for Applications 2.0 has a couple of interesting new constructs in addition to those presented in my article "Visual Basic 4.0 Provides Easy Entry to the Advanced Features of Windows 95," (MSJ, October 1995). For instance, not only can you provide named parameters for a procedure, you can declare certain arguments as optional and provide an open-ended array of parameters in a procedure.

When you call a procedure using named arguments, you can supply them in any order. It makes the calling code easier to read, and lets you omit optional arguments by supplying the ones you need additively instead of subtractively. Let's look at an example.


 Sub AddDBRecord(Lname as String, _
               Optional Fname as String, _
               EmpID as string, Optional DOB as Date, _
               Optional JobLevel as Integer)
                     .
                     .
                     .
End Sub

AddDBRecord requires an employee's last name and ID number. You can also pass in a first name, date of birth, and job level if you have that information handy. Instead of omitting arguments by stacking up commas,


 AddDBRecord "Trupin", "Joshua", "02134ZOOM", ,

you can pass the arguments you want by name, using := instead of =.


 AddDBRecord Lname:="Trupin", EmpID:="02134ZOOM", _
Fname:="Joshua"

In the AddDBRecord code, you can use the IsMissing function to find out whether an Optional parameter was in fact omitted or was just passed in as something blank.

Another useful keyword is ParamArray. You can use it as the last argument in a parameter list and pass any number of variant values to the procedure. If you've ever dived into C/C++, it's similar to the argv[] open-ended list of arguments you can get in a main procedure. The difference is that you're not given an argc, so you have to call UBound on the array to find out how large it is.


 Sub DeleteSomeRecords(tblName as String, _
                     ParamArray intRecDel() as Variant)
                     .
                     .
                     .
       For y = 0 to Ubound(intRecDel())
              ' Delete record number intRecDel(y)
       Next y
                     .
                     .
                     .
End Sub

A ParamArray must always be the last argument in the list. You can't put another argument after a ParamArray and hope that the ParamArray only picks up the arguments up to n–1.


 DeleteSomeRecords "tblJosh", 12, 13, 15, 19, 24, 25

In Microsoft Access 2.0, you could use Create Object calls if you wanted to be a controller. You can now use the New keyword to create an early-bound OLE object in your code. More importantly, Microsoft Access now serves up OLE automation to other applications in a couple of interesting ways. Other programs can use the Access.Application automation type to create an instance of Microsoft Access. (Microsoft Access itself can create another instance of itself, which is useful for app testing.) The Application object exposes a DBEngine property, which is the base object of the DAO hierarchy. If you create an object named Acc in Visual Basic (where Acc is type Access.Application), you can then write


 App.DBEngine

to access data access objects. If you want to control Access-specific objects, you can get to databases with the OpenCurrentDatabase and NewCurrentDatabase functions, both exposed by Access.Application.

When I said that the Jet engine was "cleaned up," I didn't mean that it was sloppy to begin with. What Microsoft has done is consolidate three data types—the recordset, dynaset, and snapshot—into a single improved recordset. The three types were really variations on a theme, and why do you need three different types of result sets? A dynaset-type recordset was an updatable result of a query, possibly containing joined information from multiple tables. A snapshot-type recordset was static; like a dynaset, it could be a single or joined table, but it couldn't be updated. Table-type recordsets contained updatable views of the data in a single table. The old calls are retained for backwards compatibility, though.

As you can imagine, the less a recordset has to do, the faster it is. Joined tables have more overhead than single ones, and it's easier to create a read-only result set than an updatable one.

For example, where you used to call OpenDynaset, the preferred method now is to use an OpenRecordset call with a new second parameter that represents the recordset's type. For instance, you used to open a dynaset like this:


 Dim DS1 As Dynaset
Set DS1 = DB1.OpenDynaset("TableName")

With the new design, you would do this instead:


 Dim DS1 As Recordset
Set DS1 = DB1.OpenRecordset("TableName", dbOpenDynaset)

There's a second new notation that can be used for the same operation:


 Set DS1 = DB1!TableName.OpenRecordset(dbOpenDynaset)

Both these methods will set DS1 to the same dynaset-type recordset. (Actually, all three methods above will still work, but you shouldn't go create new programs using obsolete functions.)

Forms and reports are defined as class modules in Microsoft Access 95, and you know what that means! OK, maybe you don't. Visual Basic for Applications provides class modules as enhanced versions of Basic modules. All procedures and variables defined as Public within a class module become exported methods and properties of the UDO (User-defined Object for the uninitiated). This is how you can easily write OLE automation servers in Visual Basic 4.0. In Microsoft Access, it means you can "inherit" the provided form functionality and extend it in any way you want.

Suppose you have a database-displaying form to which youwanttoaddfilteringtechnology.Intheproject'sclassmodule, you could write a function called SetFilter that takes a letter asinputandsetsthedatabaseFilterbasedonit.


 Public Sub SetFilter(strLetter as String)
    Me.Filter = BuildCriteria("ProductName", dbText, _
                              strLetter)
    Me.FilterOn = True
End Sub

Since the Form is a class module, it can be created from code with a Dim statement, just like any exported OLE automation server. In your code, you can control the filtering with the SetFilter procedure you just wrote.


 Dim frm as New Form_Demo

Sub FilterProducts()
      frm.SetFilter "C"
End Sub

In the same way, you can use Property Let/Get/Set statements to define custom properties. If your form has a property (perhaps the filter character), you can turn it into a form property. If you want to have a property directly set, you can just define it in the definitions section of the module.However,youwilloftenwantadditionalprocessing when a user tries to access or use a property. With Property Let/Get, you can control a property's usage. Property Let is called when a user sets a property; Property Set takes charge when a user tries to retrieve a property. For the filter character property, you could provide these functions:


 Static Property Let FilterChar(chFilterChar as String)
    ' You can use the With statement in the SetFilter 
    ' code as well. This block is identical to 
    ' SetFilter's functionality
    With Me
      .Filter = BuildCriteria("ProductName", dbText, _
                              chFilterChar)
      .FilterOn = True
    End With
End Property

Static Property Get FilterChar()
    FilterChar = Me.Filter
End Property

The FilterProducts routine above can then be changed as follows:


 Dim frm as New Form_Demo

Sub FilterProducts()
       frm.FilterChar = "C"
End Sub

Other improvements in Microsoft Access for Windows 95 include easier screens and dialogs, long filenames, and 32-bit code generation.

Haveaquestionaboutprogrammingin Visual Basic,VisualFoxPro, Access, Office, or stuff like that? Mail it directly to The Visual Programmer, Microsoft Systems Journal, 825 Eighth Avenue, 18th Floor, New York, New York 10019, or send it to MSJ (re: Visual Programmer) via:


CompuServe:

Internet:


Internet:

Joshua Trupin
75120,657

geeknet@ix.netcom.com

Eric Maffei
ericm@microsoft.com


This article is reproduced from Microsoft Systems Journal. Copyright © 1995 by Miller Freeman, Inc. All rights are reserved. No part of this article may be reproduced in any fashion (except in brief quotations used in critical articles and reviews) without the prior consent of Miller Freeman.

To contact Miller Freeman regarding subscription information, call (800) 666-1084 in the U.S., or (303) 447-9330 in all other countries. For other inquiries, call (415) 358-9500.