Dynamically load a DLL from a runtime specified path
When running VB.Net or C# code, often it’s useful to call native code. One way is using PInvoke. (for other ways, see How fast is interop code?)
For example, you can call GetWindowText to get the title of a window. To get it’s managed signature, you can use this code from https://www.pinvoke.net/default.aspx/user32/GetWindowText.html
<DllImport("User32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Shared Function GetWindowText(ByVal hwnd As IntPtr, _
ByVal lpString As StringBuilder, _
ByVal cch As Integer) As Integer
End Function
This code works because "User32.dll" is a well known system Dll and can be found along the PATH. However, many times the Dll you want may not be on the path. Sometimes it's in a known location relative to where your code is executing (like a subdirectory).
The string name of the Dll specified in the DllImport can include a path name, like "c:\windows\system32\user32.dll", but it must be constant at compile time. This limitation contradicts the name for Dll: "Dynamic Link Library"
However, sometimes a DLL may not live in a known location on a disk, but you still want to call it. Perhaps the location can be specified at runtime.
A simple way to work around this problem is to explicitly call LoadLibrary (which accepts a full path) first. Then the call will work.
Run the sample code below.
Start Visual Studio. File->New->Project->VB ->Windows Application.
Double click the form and replace the code with the sample below.
The sample creates two buttons on a form and tries to invoke GetWindowText from a specified DLL.
The 1st button invokes the one in the Windows System32 directory.
The 2nd button copies the system one to a different folder and a different name ("User32Copy.dll"), then calls it.
See also:
<Sample Code>
Imports System.Runtime.InteropServices
Imports System.IO
Imports System.Text
Public Class Form1
Dim WithEvents b32 As New Button
Dim WithEvents b32Copy As New Button
Dim TargDir = IO.Path.GetTempPath
Dim SysDir = Path.Combine(My.Application.GetEnvironmentVariable("windir"), "System32") ' C:\Windows\System32
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Me.Text = "Form1 " + DateTime.Now.ToLongTimeString
b32.Text = "User32"
Me.Controls.Add(b32)
b32Copy.Text = "User32Copy"
b32Copy.Top = 30
Me.Controls.Add(b32Copy)
End Sub
Sub btn32_click() Handles b32.Click
CallGetWindowText(New Wrap32, Path.Combine(SysDir, Wrap32.cDllName))
End Sub
Sub btn32Copy_click() Handles b32Copy.Click
Dim cFullName = Path.Combine(TargDir, Wrap32Copy.cDllName) ' D:\user32Copy.dll
If Not File.Exists(cFullName) Then
File.Copy(Path.Combine(SysDir, Wrap32.cDllName), cFullName)
End If
Using ldr = New DynamicDllLoader(cFullName)
CallGetWindowText(New Wrap32Copy, cFullName)
End Using
End Sub
Sub CallGetWindowText(ByVal wrap As Object, ByVal cFullName As String)
Dim sbStr As New StringBuilder(255)
wrap.GetWindowText(Me.Handle.ToInt32, sbStr, sbStr.Capacity)
Dim ParentWind = Me.Handle.ToInt32 'sets the owner of the Msgbox. also try Me.Handle.ToInt32 and 0 to observe Alt-Tab behavior
MessageBox.Show("From " + cFullName + vbCrLf + "Form caption = " + sbStr.ToString, "From " + cFullName, 0)
End Sub
End Class
Friend Class Wrap32
Friend Const cDllName = "user32.dll"
<DllImport(cDllName, SetLastError:=True, CharSet:=CharSet.Auto)> _
Shared Function GetWindowText(ByVal hwnd As IntPtr, _
ByVal lpString As StringBuilder, _
ByVal cch As Integer) As Integer
End Function
End Class
Friend Class Wrap32Copy
Friend Const cDllName = "user32Copy.dll"
<DllImport(cDllName, SetLastError:=True, CharSet:=CharSet.Auto)> _
Shared Function GetWindowText(ByVal hwnd As IntPtr, _
ByVal lpString As StringBuilder, _
ByVal cch As Integer) As Integer
End Function
End Class
Friend Class DynamicDllLoader
Implements IDisposable
<DllImport("kernel32.dll", _
CallingConvention:=CallingConvention.Winapi, _
CharSet:=CharSet.Auto, _
EntryPoint:="LoadLibrary", _
PreserveSig:=True)> _
Friend Shared Function LoadLibrary(<[In]()> ByVal DllPath As String) As Integer
End Function
<DllImport("kernel32.dll", _
CallingConvention:=CallingConvention.Winapi, _
EntryPoint:="FreeLibrary", _
PreserveSig:=True)> _
Friend Shared Function FreeLibrary(<[In]()> ByVal Handle As Integer) As Integer
End Function
Private _HandleDll As Integer
Sub New(ByVal FullPath As String, Optional ByVal fChangeDir As Boolean = False)
'must change folder to loc of MSVBIDE so VSAssert and MSVBIDEui can be found
Dim OrigCurdir = System.IO.Directory.GetCurrentDirectory
If fChangeDir Then
System.IO.Directory.SetCurrentDirectory(Path.GetDirectoryName(FullPath))
End If
Try
If IO.File.Exists(FullPath) Then
_HandleDll = LoadLibrary(FullPath)
If _HandleDll = 0 Then
Throw New FileNotFoundException("couldn't load " + FullPath)
End If
Else
Throw New FileNotFoundException(FullPath)
End If
Catch ex As Exception
Finally
If fChangeDir Then
Directory.SetCurrentDirectory(OrigCurdir)
End If
End Try
End Sub
Sub UnLoadDll()
If _HandleDll Then
FreeLibrary(_HandleDll)
_HandleDll = 0
End If
End Sub
Private disposedValue As Boolean = False ' To detect redundant calls
' IDisposable
Protected Overridable Sub Dispose(ByVal disposing As Boolean)
If Not Me.disposedValue Then
If disposing Then
' TODO: free other state (managed objects).
End If
UnLoadDll()
' TODO: free your own state (unmanaged objects).
' TODO: set large fields to null.
End If
Me.disposedValue = True
End Sub
#Region " IDisposable Support "
' This code added by Visual Basic to correctly implement the disposable pattern.
Public Sub Dispose() Implements IDisposable.Dispose
' Do not change this code. Put cleanup code in Dispose(ByVal disposing As Boolean) above.
Dispose(True)
GC.SuppressFinalize(Me)
End Sub
#End Region
End Class
</Sample Code>