How to convert a VB module to a DLL
By Peter Meyer
This article provides a step-by-step tutorial showing how to convert a Visual Basic module (containing functions) to a Windows dynamic link library (DLL), so that those functions can be called by other programs.
Introduction: In 2005 Ron Petrusha published an article in which he said that Visual Basic, with its "elegant graphical interface and overall ease of use ... [and which allows] a relatively inexperienced programmer to accomplish in minutes what often took days for advanced programmers using languages like C and C++", had been attacked by (envious) non-VB programmers for its inability to compile the code for a VB module to a standard Windows DLL. His article explained that this could in fact be done, and explained how. All credit for the solution to this problem is due to Ron Petrusha, and this article is mostly a presentation of a modified form of the method which he discovered.
We describe here a bare-bones VB application which contains a VB module which contains the code for a single function, and we show step-by-step how to convert that module to a DLL and to obtain a VB application which calls that function in the DLL (rather than in the VB module). The method is easily extended to VB modules containing multiple functions and procedures.
First create a project in a folder \vbm2dll for a VB executable called Caller. (The project can be located anywhere but if so then the pathnames to certain files must be changed in some of the code given below.) The form should have the following controls:
and the code should be thus:
Private Sub Form_load() txbValuePassed = "abc" End Sub Private Sub cmdCall_Click() txbValueReturned = FunctionCalled(txbValuePassed) End Sub
Now add a module called Module1 to the project, and add to it the following code:
Public Function FunctionCalled _ (ByVal strValuePassed As String) As String FunctionCalled = strValuePassed End Function
Compile the project to Caller.exe to make sure that all is in order. Run the application and click on the "Call function" button (passing various values) to ensure that it works OK.
Now create an ActiveX DLL project. This creates a class module, Class1.cls, which can be ignored. Save this project in \vbm2dll as Called.vbp.
Add a new module to this project and save it as Module2.bas. Copy the code (given above) from Module1.bas to Module2.bas and add a declaration of a function DllMain() so that the code is as below (do not include "Option Explicit").
Public Function DllMain(hInst As Long, _ fdwReason As Long, lpvReserved As Long) As Boolean DllMain = (fdwReason = DLL_PROCESS_ATTACH) End Function Public Function FunctionCalled _ (ByVal strValuePassed As String) As String FunctionCalled = strValuePassed End Function
For an explanation of an earlier form of the function DllMain() see the article by Ron Petrusha.
Compile the project to Called.dll to make sure that all is in order.
Now we modify the Caller project so that the application does not call the function FunctionCalled() in the VB module in the project but rather calls the function in the DLL Called.dll.
Remove the code in the Module1.bas module either by removing the module from the project or as follows:
#If False Then Public Function FunctionCalled _ (ByVal strValuePassed As String) As String FunctionCalled = strValuePassed End Function #Endif
Now add a declaration for the function FunctionCalled() to the code for the form in the Caller project thus:
Private Declare Function FunctionCalled _ Lib "c:\vbm2dll\Called.dll" _ (ByVal strValuePassed As String) As String Private Sub Form_load() txbValuePassed = "abc" End Sub Private Sub cmdCall_Click() txbValueReturned = FunctionCalled(txbValuePassed) End Sub
The Caller program is now set up to call FunctionCalled() in Called.dll.
Compile the project to Caller.exe to make sure that all is in order. But now when we run the application and click on the "Call function" button we get an error message: "Can't find DLL entry point FunctionCalled in c:\vbm2dll\Called.dll"
The reason is that the DLL is not exporting the function FunctionCalled(), so this function is not visible to the Caller program. Ron Petrusha says that "Visual Basic does not appear to allow you to export DLL functions from ActiveX DLLs, thus effectively preventing you from using Visual Basic to create a standard Windows DLL. This difficulty, however, is not insurmountable." He proceeds to show how it can be overcome, namely, by creating a proxy linker called by Visual Basic and which in turn calls the original linker to create a DLL which exports the required function (or functions, if the VB module contains code for more than one).
In order to create a DLL which exports functions, a parameter must be passed to the original linker to specify a DEF file stating the functions to be exported. So for Called.dll create a Called.def file (in \vbm2dll) as follows:
NAME Called LIBRARY Called DESCRIPTION "Additional functions to be exported" EXPORTS DllMain @1 FunctionCalled @2
These functions are "additional" because several functions are automatically exported.
If the module to be compiled as a DLL has several functions, say, FunctionA(), FunctionB() and so on, then the DEF file should be like this:
NAME Called LIBRARY Called DESCRIPTION "Additional functions to be exported" EXPORTS DllMain @1 FunctionA @2 FunctionB @3 FunctionC @4
Ron Petrusha gives the code for a proxy linker. Here we provide an alternative proxy linker. The principle of operation in each case is the same: When the ActiveX DLL project (containing the VB module) is compiled, Visual Basic calls the VB compiler, which calls the VB linker. To obtain a DLL which exports the functions in the VB module, so that they can be called from other programs, the call to the VB linker must be intercepted by a program (the proxy linker) which has the same name as the original VB linker. The proxy linker then modifies the command line arguments appropriately, and calls the original VB linker under a different name.
To make the proxy linker, create a new standard EXE project. This creates a form Form1.frm. Remove this from the project. Under "Project | References" check "Microsoft Scripting Runtime", then save the project as ProxyLinker.vbp (either in \vbm2dll or elsewhere).
Add a new module called Module3.bas to this project (to distinguish it from Module1.bas and Module2.bas, in case this project is created in \vbm2dll), and add this code to it:
Option Explicit Public Sub Main() Dim blnCallLinker As Boolean Dim intPos As Integer Dim intPos2 As Integer Dim strCmd As String Dim strDLLfilepath As String Dim strDEFfilepath As String Dim oFS As New Scripting.FileSystemObject Dim oFS2 As New Scripting.FileSystemObject Dim ts As TextStream strCmd = Command Set ts = oFS.CreateTextFile(App.Path + "\proxy_linker_log.txt") ts.WriteLine "Execution of proxy linker begun at " + CStr(Date) + " " + CStr(Time()) ts.WriteBlankLines 1 ts.WriteLine "Command line arguments to LINK call:" ts.WriteBlankLines 1 ts.WriteLine strCmd ts.WriteBlankLines 1 blnCallLinker = False intPos = InStr(1, UCase(strCmd), "/DLL") If intPos = 0 Then ts.WriteLine "Not creating a DLL. No change to command line arguments." ts.WriteBlankLines 1 blnCallLinker = True Else intPos = InStr(1, UCase(strCmd), "/OUT") If intPos = 0 Then ts.WriteLine "No /OUT argument found." ts.WriteBlankLines 1 Else intPos2 = InStr(intPos + 6, strCmd, Chr(34)) If intPos2 = 0 Then ts.WriteLine "Cannot get DLL filepath." ts.WriteBlankLines 1 Else strDLLfilepath = Mid(strCmd, intPos + 6, intPos2 - intPos - 6) ts.WriteLine "DLL filepath is " + strDLLfilepath strDEFfilepath = Left(strDLLfilepath, Len(strDLLfilepath) - 3) + "def" ts.WriteLine "DEF filepath is " + strDEFfilepath ts.WriteBlankLines 1 'Check that DEF file exists. 'Set oFS2 = CreateObject("Scripting.FileSystemObject") If Not oFS2.FileExists(strDEFfilepath) Then ts.WriteLine "DEF file " + strDEFfilepath + " not found." ts.WriteBlankLines 1 Else 'Add module definition before /DLL switch intPos = InStr(1, strCmd, "/DLL") strCmd = Left(strCmd, intPos - 1) _ + " /DEF:" + Chr(34) + strDEFfilepath + Chr(34) + " " _ + Mid(strCmd, intPos) ts.WriteLine "Command line arguments after modification:" ts.WriteBlankLines 1 ts.WriteLine strCmd ts.WriteBlankLines 1 blnCallLinker = True End If End If End If End If If blnCallLinker Then Shell "LINK-ORIG.EXE " + strCmd If Err.Number = 0 Then ts.WriteLine "Call to original linker was successful." Else ts.WriteLine "Call to original linker produced an error." Err.Clear End If ts.WriteBlankLines 1 End If ts.WriteLine "Recommended: restore the original linker as LINK.EXE." ts.Close End Sub
Compile this project as link.exe.
Locate the original VB linker, also called LINK.EXE. (We here use upper and lower case names to distinguish the original linker from the proxy linker.) For Version 6 of Visual Basic it is normally to be found in the folder C:\Program Files\Microsoft Visual Studio\VB98. Make a backup of this file (important!), say LINK-BAK.EXE. Rename LINK.EXE as LINK-ORIG.EXE. Copy the proxy linker, link.exe, to the folder holding the original VB linker.
Open the project for Called and compile it to produce a DLL with the name called.dll. As noted above, the VB compiler calls link.exe (the proxy linker). This checks to see that the DEF file called.def is present in \vbm2dll, adds a command line argument specifying this file, and calls the VB linker LINK.EXE (renamed as LINK-ORIG.EXE), which produces the DLL.
Note that for this method to work, the DEF file must have the same name (apart from the file extension) as the DLL to be created, and must be present in the same folder as the VB module (the folder where the DLL is to be created).
The proxy linker produces a log file called proxy_linker_log.txt in the same folder as link.exe. After Called.dll has been compiled this should be inspected to ensure that there is no error message.
Now either (a) run the compiled application Caller.exe or (b) open the product for Caller and run the program. When the "Call function" button is clicked, there is no error message as previously, and the value passed is returned and displayed, thereby showing that the function FunctionCalled() in the DLL is being called successfully.
The proxy linker, link.exe, can be used to compile ordinary VB executable programs (such as link.exe) but when it is not needed (for converting VB modules to DLLs) it is advisable to replace it with the VB linker (which has been saved as both LINK-BAK.EXE and as LINK-ORIG.EXE) under the original name LINK.EXE. VB compilations will then call this program directly rather than indirectly via the proxy linker.
Ron Petrusha's original explanation of his method is at "Creating a Windows DLL with Visual Basic", which may be found here.
Hermetic Systems Home Page