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 [now gone] 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.


Added 2023-12-30: The original (2009) source code (by Peter Meyer) can be downloaded in the zip file VB Module as DLL.