Creating a Windows DLL with Visual Basic

36
Published on Windows DevCenter (http://www.windowsdevcenter.com/ ) See this if you're having trouble printing code examples Creating a Windows DLL with Visual Basic by Ron Petrusha 04/26/2005 As the first rapid application development language, Visual Basic attracted attention for its elegant graphical interface and overall ease of use, which allowed a relatively inexperienced programmer to accomplish in minutes what often took days for advanced programmers using languages like C and C++. As a result, Visual Basic drew millions of new programmers, many of whom might never have even considered programming had it not been for the language's simplicity. Because of this simplicity, and because Visual Basic was attracting a following that the proponents of other languages could only dream of, non-Visual Basic programmers (who were really green with envy) counterattacked by pointing to the inexperience of most Visual Basic programmers and to the problems that stem from Visual Basic's design goal of shielding the developer from the complexities of the underlying operating system. To bolster their contention that Visual Basic is underpowered and underdeveloped, critics liked to point to the many things "real" programmers do that Visual Basic programmers cannot. Perhaps the most common limitation that critics continually point to is Visual Basic's inability to create a standard Windows dynamic link library (DLL). Certainly it's true that out of the box, Visual Basic doesn't allow you to create a Windows DLL in the same way that you can create other project types, like a Standard EXE or an ActiveX DLL. In this article, we'll go exploring to see how Visual Basic generates its executables. In the process, we'll discover that with a little bit of extra work, we can in fact create Windows DLLs with Visual Basic. What Is a Windows Dynamic Link Library? A dynamic link library (DLL) is a library of functions and procedures that can be called from an application or another DLL. Using a library in this way has two major functions: It permits the sharing of code. The same DLL can be used by many other DLLs and applications. The Win32 API, for instance, is implemented as a series of Windows DLLs. Moreover, as long as multiple processes load the same DLL at the same base address, they can share the code in the DLL. In other words, a single DLL in memory can be accessed by multiple processes. It allows for component-based and modular development, which makes development and the upgrade process easier. Ordinarily, when a static library is used in application development, the library's modules must be linked into the finished application. With dynamic linking, the modules reside in a separate DLL file that is loaded dynamically, either when the application loads or when its member functions are needed.

description

This document describe how to create/develop a standard windows dll in vb6

Transcript of Creating a Windows DLL with Visual Basic

Page 1: Creating a Windows DLL with Visual Basic

Published on Windows DevCenter (http://www.windowsdevcenter.com/) See this if you're having trouble printing code examples

Creating a Windows DLL with Visual Basicby Ron Petrusha04/26/2005

As the first rapid application development language, Visual Basic attracted attention for its elegant graphicalinterface and overall ease of use, which allowed a relatively inexperienced programmer to accomplish in minuteswhat often took days for advanced programmers using languages like C and C++. As a result, Visual Basicdrew millions of new programmers, many of whom might never have even considered programming had it notbeen for the language's simplicity. Because of this simplicity, and because Visual Basic was attracting a followingthat the proponents of other languages could only dream of, non-Visual Basic programmers (who were reallygreen with envy) counterattacked by pointing to the inexperience of most Visual Basic programmers and to theproblems that stem from Visual Basic's design goal of shielding the developer from the complexities of theunderlying operating system. To bolster their contention that Visual Basic is underpowered and underdeveloped,critics liked to point to the many things "real" programmers do that Visual Basic programmers cannot. Perhapsthe most common limitation that critics continually point to is Visual Basic's inability to create a standardWindows dynamic link library (DLL).

Certainly it's true that out of the box, Visual Basic doesn't allow you to create a Windows DLL in the same waythat you can create other project types, like a Standard EXE or an ActiveX DLL. In this article, we'll goexploring to see how Visual Basic generates its executables. In the process, we'll discover that with a little bit ofextra work, we can in fact create Windows DLLs with Visual Basic.

What Is a Windows Dynamic Link Library?

A dynamic link library (DLL) is a library of functions and procedures that can be called from an application oranother DLL. Using a library in this way has two major functions:

It permits the sharing of code. The same DLL can be used by many other DLLs and applications. TheWin32 API, for instance, is implemented as a series of Windows DLLs. Moreover, as long as multipleprocesses load the same DLL at the same base address, they can share the code in the DLL. In otherwords, a single DLL in memory can be accessed by multiple processes.It allows for component-based and modular development, which makes development and the upgradeprocess easier.

Ordinarily, when a static library is used in application development, the library's modules must be linked into thefinished application. With dynamic linking, the modules reside in a separate DLL file that is loaded dynamically,either when the application loads or when its member functions are needed.

Page 2: Creating a Windows DLL with Visual Basic

A dynamic link library may include internal functions, which can be called only from within the DLL. Its mainpurpose, however, is to provide exported functions--that is, functions that reside in a module of the DLL and canbe called from other DLLs and applications. Frequently, a definition (.def) file is used in C/C++ projects to list aDLL's exports.

A DLL also includes an optional entry point, which is called when a process or thread loads or unloads the DLL.Windows calls this entry point when a process loads and unloads the DLL. It also calls the entry point when theprocess creates or terminates a thread. That allows the DLL to perform any per-process and per-applicationinitialization and cleanup. The syntax of this entry point, which must use the standard-call calling convention (usedby default in Visual Basic), is:

Public Function DllMain(hinstDLL As Long, fdwReason As Long, lpwReserved As Long) As Boolean

Its parameters are:

hInstDLL, a Long containing the instance handle of the DLL. This is the same as the DLL's module handle.

fdwReason, a constant indicating why the entry point has been called. Possible values are:

DLL_PROCESS_ATTACH (1)

A process is loading the DLL. Any per-process initialization should be performed.DLL_THREAD_ATTACH (2)

The process is spawning a new thread. Any per-thread initialization should be performed.DLL_THREAD_DETACH (3)

A thread is exiting. Any per-thread cleanup should be performed.DLL_PROCESS_DETACH (0)

A process is detaching the DLL, or the process is exiting. Any per-process cleanup should be performed.lpvReserved

A Long that provides more information about DLL_PROCESS_ATTACH and DLL_PROCESS_DETACH. (It isunused if fdwReason is DLL_THREAD_ATTACH or DLL_THREAD_DETACH.) If fdwReason isDLL_PROCESS_ATTACH, it is Nothing for libraries loaded dynamically using functions like LoadLibraryand GetProcAddress, and it is not Nothing for libraries loaded statically by providing library stubs atcompile time. If fdwReason is DLL_PROCESS_DETACH, it is Nothing if the call has resulted from a call tothe Win32 FreeLibrary function, and it is not Nothing if the entry point is called during processtermination.

The return value of the function is meaningful only if fdwReason is DLL_PROCESS_ATTACH. If initializationsucceeds, the function should return True; otherwise, it should return False. Note that because the function isan entry point called by Windows, the values of the arguments passed to the function are provided by Windows.Also, the entry point is not called when a thread is terminated using the Win32 TerminateThread function, noris it called when a process is terminated using the Win32 TerminateProcess function.

The DLL Code

In attempting to develop a Windows DLL, we'll create a very simple library of math functions. The following isthe DLL's code, which we'll store in a code module (a .bas file) named MathLib:

Page 3: Creating a Windows DLL with Visual Basic

the DLL's code, which we'll store in a code module (a .bas file) named MathLib:

Option Explicit

Public Const DLL_PROCESS_DETACH = 0Public Const DLL_PROCESS_ATTACH = 1Public Const DLL_THREAD_ATTACH = 2Public Const DLL_THREAD_DETACH = 3

Public Function DllMain(hInst As Long, fdwReason As Long, lpvReserved As Long) As Boolean Select Case fdwReason Case DLL_PROCESS_DETACH ' No per-process cleanup needed Case DLL_PROCESS_ATTACH DllMain = True Case DLL_THREAD_ATTACH ' No per-thread initialization needed Case DLL_THREAD_DETACH ' No per-thread cleanup needed End SelectEnd Function

Public Function Increment(var As Integer) As Integer If Not IsNumeric(var) Then Err.Raise 5 Increment = var + 1End Function

Public Function Decrement(var As Integer) As Integer If Not IsNumeric(var) Then Err.Raise 5 Decrement = var - 1End Function

Public Function Square(var As Long) As Long If Not IsNumeric(var) Then Err.Raise 5 Square = var ^ 2End Function

Several characteristics about the code are worth mentioning. The first is that although it includes a DllMainprocedure, no per-process or per-thread initialization needs to be performed. So DllMain simply returns Trueif it is called with the fdwReason argument set to DLL_PROCESS_ATTACH.

Second, the point of providing a Windows DLL is to allow other languages to call it. To ensure interoperability,we want to confine ourselves to language features that the Win32 API supports, so that our DLL can be calledfrom as many development environments and platforms as possible. We could have made each of our three mathfunctions more flexible, for example, by defining both the incoming argument and the return value as Variants.That would have allowed the function to determine the data type it should interpret the incoming data as, inaddition to the data type it should return. But the Variant is a data type defined by COM, Microsoft'sComponent Object Model, and is not a data type Win32 API recognizes. So instead, the code uses standardWin32 API data types.

We'll also need a test program to tell us whether our Windows DLL is working properly. For that purpose, we

Page 4: Creating a Windows DLL with Visual Basic

can create a Standard EXE project with one form and one code module. The code module simply consists of theDeclare statements that define the functions found in the DLL:

Public Declare Function Increment Lib "MathLib.dll" (var As Integer) As Integer

Public Declare Function Decrement Lib "MathLib.dll" (var As Integer) As Integer

Public Declare Function Square Lib "MathLib.dll" (var As Long) As Long

Rather than simply specifying the name of the DLL in the Lib clause, you also should add the full path to thedirectory that contains the DLL.

The form's code performs the calls to the DLL functions:

Option Explicit

Dim incr As IntegerDim decr As IntegerDim sqr As Long

Private Sub cmdDecrement_Click() decr = Increment(decr) cmdDecrement.Caption = "x = " & CStr(decr)End Sub

Private Sub cmdIncrement_Click() incr = Increment(incr) cmdIncrement.Caption = "x = " & CStr(incr)End Sub

Private Sub cmdSquare_Click() sqr = Square(srr) cmdSquare.Caption = "x = " & CStr(sqr)End Sub

Private Sub Form_Load() incr = 1 decr = 100 sqr = 2End Sub

The ActiveX DLL Project Type

Let's begin by creating an ActiveX DLL project and seeing what happens if we try to call it as if it were astandard Windows DLL. When you create an ActiveX DLL project, Visual Basic automatically adds a classmodule (a .cls file) to it. You can rename this if you want, but don't include any code. Instead, add a codemodule (a .bas file) to the project, add the DLL's code, and then compile the DLL. When you run the DLL testapplication, the error message dialog shown in Figure 1 appears. The error message indicates that although theDLL was found, the specific called function (Increment) was not.

Page 5: Creating a Windows DLL with Visual Basic

Figure 1. Error when accessing an ActiveX DLL as a Windows DLL

The most likely cause of this error is that the function is not actually exported by the DLL. We can use theDumpBin utility to examine a DLL's exports by using the syntax

Dumpbin <path and name of dll> /exports

If we run DumpBin using this syntax, we see the following output:

Microsoft (R) COFF Binary File Dumper Version 6.00.8447Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file mathlib.dll

File Type: DLL

Section contains the following exports for MathLib.dll

0 characteristics 41B9E52C time date stamp Fri Dec 10 10:04:28 2004 0.00 version 1 ordinal base 4 number of functions 4 number of names

ordinal hint RVA name

1 0 0000192E DllCanUnloadNow 2 1 00001902 DllGetClassObject 3 2 00001918 DllRegisterServer 4 3 000018EC DllUnregisterServer

Summary

1000 .data 1000 .reloc 1000 .rsrc 1000 .text

Our DLL exports four functions, all of which are utility functions that support COM. Clearly we need to exportDllMain and our three math functions. But how? Visual Basic does not appear to allow you to export DLL

Page 6: Creating a Windows DLL with Visual Basic

functions from ActiveX DLLs, thus effectively preventing you from using Visual Basic to create a standardWindows DLL.

This difficulty, however, is not insurmountable. When we select the File -> Make <filename>.dll menu option tocreate an ActiveX DLL, it appears that Visual Basic is seamlessly taking our source code and outputting anActiveX DLL. But if we examine the subdirectory in which Visual Basic was installed, it appears that the processis not quite so seamless. Along with VB6.EXE, the Visual Basic executable that defines the Visual Basicenvironment, we can also find C2.EXE and LINK.EXE, which are a compiler and a linker, respectively. Theirpresence in this directory suggests that VB6.EXE itself does not handle the generation of a DLL file, but that atsome point in the compilation process, it calls these programs.

We can find out how Visual Basic is using the compiler and linker more precisely by renaming them and creatingwrapper executables named C2 and LINK that in turn call the real compiler and linker. The following is thesource code for a new version of a console-mode C2.EXE that calls the "real" C2 compiler, which we'verenamed C2comp.exe:

Public Sub Main()

On Error Resume Next

Dim strCmd As String, strPath As String Dim oFS As New Scripting.FileSystemObject Dim ts As TextStream

strCmd = Command strPath = App.Path Set ts = oFS.CreateTextFile(strPath & "\c2log.txt") ts.WriteLine "Beginning execution at " & Date & " " & Time() ts.WriteBlankLines 1 ts.WriteLine "Command line parameters to c2 call:" ts.WriteLine " " & strCmd ts.WriteBlankLines 1 ts.WriteLine "Calling C2 compiler" Shell "c2comp.exe " & strCmd If Err.Number <> 0 Then ts.WriteLine "Error in calling C2 compiler..." End If ts.WriteBlankLines 1 ts.WriteLine "Returned from c2 compiler call" ts.Close End Sub

The process of compiling an ActiveX DLL produces the following output in our log file:

Beginning execution at 12/10/2004 12:44:22 PM

Page 7: Creating a Windows DLL with Visual Basic

Command line parameters to c2 call: -il "C:\DOCUME~1\Ron\LOCALS~1\Temp\VB277103" -f "C:\VB Projects\ MathLib\MathMod.bas" -W 3 -Gy -G5 -Gs4096 -dos -Zl -Fo"C:\ VB Projects\MathLib\MathMod.OBJ" -QIfdiv -ML -basic

Calling C2 compiler

Returned from c2 compiler call

These are fairly standard command-line arguments to produce object files that in turn are supplied to the linker.That means that to determine how to produce a Windows DLL, we'll have to intercept the call to the linker sothat we can see what arguments Visual Basic passes to it. The following code does that:

Public Sub Main()

On Error Resume Next

Dim strCmd As String, strPath As String Dim oFS As New Scripting.FileSystemObject Dim ts As TextStream

strCmd = Command strPath = App.Path Set ts = oFS.CreateTextFile(strPath & "\lnklog.txt") ts.WriteLine "Beginning execution at " & Date & " " & Time() ts.WriteBlankLines 1 ts.WriteLine "Command line parameters to LINK call:" ts.WriteLine " " & strCmd ts.WriteBlankLines 1 ts.WriteLine "Calling LINK linker" Shell "linklnk.exe " & strCmd If Err.Number <> 0 Then ts.WriteLine "Error in calling linker..." Err.Clear End If ts.WriteBlankLines 1 ts.WriteLine "Returned from linker call" ts.Close End Sub

It requires that we rename the linker LinkLnk.exe and name our link wrapper Link.exe.

When we attempt to compile an ActiveX DLL project, our linker log file contains the following output:

Beginning execution at 12/11/2004 12:44:33 PM

Command line parameters to LINK call:

Page 8: Creating a Windows DLL with Visual Basic

Command line parameters to LINK call: "C:\Program Files\Microsoft Visual Studio\VB98\Class1.OBJ" "C:\Program Files\Microsoft Visual Studio\VB98\Project1.OBJ" "C:\Program Files\Microsoft Visual Studio\VB98\VBAEXE6.LIB" /ENTRY:__vbaS /OUT:"C:\Program Files\Microsoft Visual Studio\VB98\Project1.dll" /BASE:0x11000000 /SUBSYSTEM:WINDOWS,4.0 /VERSION:1.0 /DLL /INCREMENTAL:NO /OPT:REF /MERGE:.rdata=.text /IGNORE:4078

Calling LINK linker

Returned from linker call

If we compare these command-line arguments with the syntax required to link the object files for a DLL usingeither C or C++, an omission becomes immediately apparent. Although the /DLL switch is supplied to create astandard DLL, there is no /DEF switch to define a module definition (.def) file that lists the functions exported bythe DLL. (If we were programming in C or C++, we could use statements within our code to define our exports.Visual Basic doesn't support this, however, making the .def file the sole means of defining a library's exports.)Moreover, if we examine the files generated for an ActiveX DLL project by the Visual Basic environment, we'llalso find that Visual Basic itself has not generated a .def file.

Creating the Windows DLL

So, after examining an ActiveX DLL's export table, intercepting Visual Basic's call to the compiler, interceptingVisual Basic's call to the linker, and comparing the arguments passed to the linker with those required by aC/C++ compiler to generate a Windows DLL, we've finally identified why we aren't able to successfully create aWindows DLL with Visual Basic. And fortunately, we can work around that restriction. We should be able tocreate a standard Windows DLL if we do the following:

1. Create a .def file for our project. We can specify our exported functions in the .def file in several ways,but it's best to keep it simple:

NAME MathLibLIBRARY MathModDESCRIPTION "Add-on Library of Mathematical Routines"EXPORTS DllMain @1 Increment @2 Decrement @3 Square @4

The NAME statement defines the name of the DLL. The LIBRARY statement must either precede the list ofexported functions or appear on the same line as the first function. The .def file should also list the ordinalposition of each exported function preceded by an @ symbol.

2. Decide how we want to intercept the call to the linker. Two major techniques are available to do this:

Patching the Import Address Table (IAT), which requires that we build a Visual Basic add-in thatmodifies the IAT in order to intercept particular calls by Visual Basic to the Win32 API. Although

Page 9: Creating a Windows DLL with Visual Basic

it's certainly the most elegant method, its complexity makes it a worthy subject for a separatearticle.

Building a proxy linker that intercepts the call to the real linker, modifies the command-linearguments to be passed to the linker, and then calls the linker with the correct command-linearguments. This is the approach we used to discover what arguments Visual Basic was passing tothe compiler and linker, and it's the approach we'll adopt to create a Windows DLL.

In building our proxy linker, we want a sufficiently flexible design so that we can generate other kinds offiles, if need be.

3. Modify the arguments to the linker to add the /DEF switch along with the path and filename of our .def file.To do this, you must create a Visual Basic Standard EXE project, add a reference to the MicrosoftScripting Runtime Library, remove the form from the project, and add a code module. The source codefor the proxy linker is as follows:

Option Explicit

Public Sub Main()

Dim SpecialLink As Boolean, fCPL As Boolean, fResource As Boolean Dim intPos As Integer Dim strCmd As String Dim strPath As String Dim strFileContents As String Dim strDefFile As String, strResFile As String Dim oFS As New Scripting.FileSystemObject Dim fld As Folder Dim fil As File Dim ts As TextStream, tsDef As TextStream

strCmd = Command Set ts = oFS.CreateTextFile(App.Path & "\lnklog.txt") ts.WriteLine "Beginning execution at " & Date & " " & Time() ts.WriteBlankLines 1 ts.WriteLine "Command line arguments to LINK call:" ts.WriteBlankLines 1 ts.WriteLine " " & strCmd ts.WriteBlankLines 2 ' Determine if .DEF file exists ' ' Extract path from first .obj argument intPos = InStr(1, strCmd, ".OBJ", vbTextCompare) strPath = Mid(strCmd, 2, intPos + 2) intPos = InStrRev(strPath, "\") strPath = Left(strPath, intPos - 1) ' Open folder Set fld = oFS.GetFolder(strPath) ' Get files in folder

Page 10: Creating a Windows DLL with Visual Basic

' Get files in folder

For Each fil In fld.Files If UCase(oFS.GetExtensionName(fil)) = "DEF" Then strDefFile = fil SpecialLink = True End If If UCase(oFS.GetExtensionName(fil)) = "RES" Then strResFile = fil fResource = True End If If SpecialLink And fResource Then Exit For Next ' Change command line arguments if flag set If SpecialLink Then ' Determine contents of .DEF file Set tsDef = oFS.OpenTextFile(strDefFile) strFileContents = tsDef.ReadAll If InStr(1, strFileContents, "CplApplet", vbTextCompare) > 0 Then fCPL = True End If ' Add module definition before /DLL switch intPos = InStr(1, strCmd, "/DLL", vbTextCompare) If intPos > 0 Then strCmd = Left(strCmd, intPos - 1) & _ " /DEF:" & Chr(34) & strDefFile & Chr(34) & " " & _ Mid(strCmd, intPos) End If ' Include .RES file if one exists If fResource Then intPos = InStr(1, strCmd, "/ENTRY", vbTextCompare) strCmd = Left(strCmd, intPos - 1) & Chr(34) & strResFile & _ Chr(34) & " " & Mid(strCmd, intPos) End If ' If Control Panel applet, change "DLL" extension to "CPL" If fCPL Then strCmd = Replace(strCmd, ".dll", ".cpl", 1, , vbTextCompare) End If ' Write linker options to output file ts.WriteLine "Command line arguments after modification:" ts.WriteBlankLines 1 ts.WriteLine " " & strCmd ts.WriteBlankLines 2 End If ts.WriteLine "Calling LINK.EXE linker" Shell "linklnk.exe " & strCmd If Err.Number <> 0 Then ts.WriteLine "Error in calling linker..." Err.Clear End If ts.WriteBlankLines 1 ts.WriteLine "Returned from linker call" ts.Close

Page 11: Creating a Windows DLL with Visual Basic

ts.Close

End Sub

This proxy linker modifies only the command-line arguments passed to the linker if a .def file is present inthe directory that contains the Visual Basic project; otherwise it simply passes the command-linearguments on to the linker unchanged. If a .def file is present, it adds a /DEF switch to the command line.It also determines whether any resource files are to be added to the linked file list. Finally, it examines theexport table to determine if a function named CplApplet is present; if it is, it changes the output file'sextension from .dll to .cpl.

4. To install the proxy linker, rename the original Visual Basic linker LinkLnk.exe, copy the proxy linker tothe Visual Basic directory, and name it Link.exe.

Once we create our proxy linker, we can reload our MathLib project and compile it into a DLL by selecting theMake MathLib.exe option from the File menu.

Testing the DLL

Once we create our Windows DLL, the final step is to test it to make sure that it works. To do this, create anew Standard EXE project (let's call it MathLibTest) and add a code module. To make sure that code in ourproject can access the functions exported by the DLL, we use the standard Visual Basic Declare statement.We declare our three exported math routines in the code module as follows:

Option Explicit

Public Declare Function Increment Lib "C:\VBProjects\MathLib\mathlib.dll" ( _ value As Integer) As Integer Public Declare Function Decrement Lib "C:\VBProjects\MathLib\mathlib.dll" ( _ value As Integer) As Integer Public Declare Function Square Lib "C:\VBProjects\MathLib\mathlib.dll" ( _ value As Long) As Long

We can then use the following code in the form module to call the routines in the DLL:

Option Explicit

Private Sub cmdDecrement_Click() txtDecrement.Text = Decrement(CInt(txtDecrement.Text))End Sub

Private Sub cmdIncrement_Click() txtIncrement.Text = Increment(CInt(txtIncrement.Text))End Sub

Private Sub cmdSquare_Click() txtSquare.Text = Square(CLng(txtSquare.Text))End Sub

Private Sub Form_Load()

Page 12: Creating a Windows DLL with Visual Basic

txtIncrement.Text = 0 txtDecrement.Text = 100 txtSquare.Text = 2End Sub

When we call each of the MathLib functions, the application window might appear as it does in Figure 2,confirming that the calls to the MathLib routines work as expected.

Figure 2: Testing calls to MathLib.dll

Ron Petrusha is the author and coauthor of many books, including "VBScript in a Nutshell."

Return to the Windows DevCenter.

Copyright © 2009 O'Reilly Media, Inc.