I’ve written about exposing Entry Points from a .net dll before, most recently
vbfengshui.com/normal-ol-dlls-from-vb-net/">here.
But I recently came across another take on the subject that was so clean, I thought I’d record it
and a link here, just to be complete.
First, a little background. Occasionally, I’ve found I have need to expose st
andard c-style entry points from a .net dll. This usually centers around integrating with some application, typically plugins, where the hosting app attempts to use LoadLibrary on your dll
and then resolve a specific entrypoint via GetProcAddress.
I’ve done this kind of thing in a variety of ways, C or C++ wrappers, ASM, etc, but .net doesn’t provide any way to expose entry points.
Or does it?
In reality, neither C# or
VB.net allow such functionality, but MSIL (Microsoft Intermediate Language)
does. In fact, this has been around for so long, there’s actually several different approaches to implementing this functionality out on the web.
The two I know about are:
I had some troubles with Selvin’s version, but Mr. Giesecke’s version resolved the issue nicely at the time.
However, after looking over the 3’rd option, I have to say I like it a little more. I should point out, though, that I’ve only used Mr. Giesecke’s approach with .net 3.5,
and the other approach with .net 4.0, so keep that in mind.
Essentially, the way these utilities work is to use ILDASM to disassembly a compiled .net assembly, they then read the resulting IL file, tweak it in a few specific ways,
and finally use ILASM to reassemble the project.
One important note here: ILASM.exe actually
comes with the .net runtime
and as such, it’s already on your computer if you have the .net runtime installed.
On the other h
and, ILDASM comes with the .net framework SDK, which is NOT part of the framework runtime. You’ll need to download
and install the SDK in order to have ILDASM available. You can get the 2.0 SDK
here.
On to the Code
Mr. Giesecke’s utility is well documented
and I won’t reproduce that here.
The source code for the other utility I mentioned was posted by the author in a forum thread. It’s C#,
and well, this is
VBFengShui, plus I wanted to ferret through it
and underst
and what was going on a little more than I had in the past, so converting it to
VB seemed like a good idea. Plus I cleaned up a few minor nits here
and there to boot.
The final program is listed below. It’s fully contained in a single class. It’s long, but not that long.
vb">Imports System.Text
Imports System.IO
Imports System.Reflection
Imports Microsoft.Win32
Imports System.ComponentModel
Imports System.Runtime.InteropServices
Imports System.Runtime.CompilerServices
Namespace DllExport
'
' Export native 64bit method from .NET assembly
' =============================================
' Adapted from code found here
' http://social.msdn.microsoft.com/Forums/en-US/clr/thread/8648ff5e-c599-42e4-b873-6b91205a5c93/
'
' More info
' http://msdn.microsoft.com/en-us/library/ww9a897z.aspx
' http://stackoverflow.com/questions/2378730/register-a-c-vb-net-com-dll-programatically
'
'
' ==================================
' Comments from the original project
' ==================================
' It is well known fact that .NET assembly could be tweaked to export native method,
' similar way how normal DLLs do it. There is good description and tool from Selvin
' for 32bit native function.
'
' My problem was how to do it for 64bits. Here you go.
'
' 1) you ILDAsm your assembly into il code.
' 2) Edit the IL and change header to look like this:
'
' For 32bit:
' .corflags 0x00000002
' .vtfixup [1] int32 fromunmanaged at VT_01
' .data VT_01 = int32[1]
'
' For 64bit
' .corflags 0x00000008
' .vtfixup [1] int64 fromunmanaged at VT_01
' .data VT_01 = int64[1]
'
' 3) Header of your exported method would look similar to this. This is same for 32bit version.
' .vtentry 1 : 1
' .export [1] as Java_net_sf_jni4net_Bridge_initDotNet
'
' 4) You ILAsm the file back into DLL. For x64 you use /x64 flag.
'
' 5) Update: It looks like none of the .vtfixup, .data or .vtentry changes are required any more to make this work.
' This simplifies the parser quite a lot. We only need to change .corflags and modify the method signature
'
' Usage requires a build step which includes this
' if "$(OutDir)"=="bin\Debug\" (set EXPORTARGS=/debug /name32:" x86" /name64:" x64") ELSE (set EXPORTARGS=/name32:" x86" /name64:" x64")
' "$(SolutionDir)Utilities\DllExport.exe" %EXPORTARGS% /input:"$(TargetPath)"
'
' You can, of course, choose not to build the x86 or x64 versions by leaving out the
' applicable /name: tag
'
''' <summary>
''' Class to export attributed functions as standard cdecl functions
''' </summary>
Class DLLExport
#Region "Enums"
Private Enum Platform
x86
x64
End Enum
#End Region
#Region "Fields"
Private rInputFile As String
Private rOutputFile As String
Private rDebugOn As Boolean
Private rVerboseOn As Boolean
Private rLines As New List(Of String)()
Private rExportIdx As Integer
Private rX86Suffix As String
Private rX64Suffix As String
Private rExportX86 As Boolean
Private rExportX64 As Boolean
#End Region
#Region " EntryPoint"
Public Shared Sub Main(args As String())
Dim dllexport = New DLLExport
#If DEBUG Then
'---- these are a few Debugging command lines
'string[] testargs = {"", "/debug", "/name32:\"-x86\"", "/name64:\"-x64\"", "/input:\"..\\..\\..\\DLLRegister\\bin\\release\\DllRegisterRaw.dll\"", "/output:\"..\\..\\..\\DLLRegister\\bin\\release\\DllRegister.dll\"" };
'Dim testargs As String() = {"", "/debug", "/name32:""-x86""", "/input:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxS.dll"""}
args = "|/debug|/input:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxSRaw.dll""|/output:""..\..\..\DllRegisterSxS\bin\x86\Debug\DllRegisterSxS.dll""".Split("|")
#End If
Dim cdir As String = ""
Dim r As Integer = 1
Try
cdir = System.IO.Directory.GetCurrentDirectory()
r = dllexport.Execute(args)
Catch ex As Exception
Console.WriteLine("")
Console.WriteLine(String.Format("Unable to process file: \r\n{0}", ex.ToString))
Finally
System.IO.Directory.SetCurrentDirectory(cdir)
End Try
'---- return an application exit code
Environment.ExitCode = r
End Sub
#End Region
#Region "Initialization"
''' <summary>
''' Constructor
''' </summary>
Public Sub New()
'Nothing special
End Sub
#End Region
#Region "Properties"
''' <summary>
''' Get just the file name without extension
''' </summary>
Private ReadOnly Property FileName() As String
Get
Return Path.GetFileNameWithoutExtension(rInputFile)
End Get
End Property
''' <summary>
''' Get the folder that contains the file
''' </summary>
Private ReadOnly Property FileFolder() As String
Get
Return Path.GetDirectoryName(rInputFile)
End Get
End Property
''' <summary>
''' Get the path to the disassembler
''' </summary>
Private ReadOnly Property DisassemblerPath() As String
Get
Dim registryPath = "SOFTWARE\Microsoft\Microsoft SDKs\Windows"
Dim registryValue = "CurrentInstallFolder"
Dim key = If(Registry.LocalMachine.OpenSubKey(registryPath), Registry.CurrentUser.OpenSubKey(registryPath))
If key Is Nothing Then
Throw New Exception("Cannot locate ildasm.exe.")
End If
Dim SDKPath = TryCast(key.GetValue(registryValue), String)
If SDKPath Is Nothing Then
Throw New Exception("Cannot locate ildasm.exe.")
End If
SDKPath = Path.Combine(SDKPath, "Bin\ildasm.exe")
If Not File.Exists(SDKPath) Then
Throw New Exception("Cannot locate ildasm.exe.")
End If
Return SDKPath
End Get
End Property
''' <summary>
''' Get the path to the assembler
''' </summary>
Private ReadOnly Property AssemblerPath() As String
Get
Dim version = Environment.Version.Major.ToString() & "." & Environment.Version.Minor.ToString() & "." & Environment.Version.Build.ToString()
Dim ILASMPath = Environment.ExpandEnvironmentVariables("%SystemRoot%\Microsoft.NET\Framework\v" & version & "\ilasm.exe")
If Not File.Exists(ILASMPath) Then
Throw New Exception("Cannot locate ilasm.exe.")
End If
Return ILASMPath
End Get
End Property
#End Region
#Region "Public Methods"
''' <summary>
''' Run the conversion
''' </summary>
''' <returns>An integer used as the DOS return value (0-success, 1 failed)</returns>
Public Function Execute(inargs As String()) As Integer
Console.WriteLine("DLLExport Tool v{0}", My.Application.Info.Version.ToString)
Console.WriteLine("Utility to create old-style dll entry points in .net assemblies")
Console.WriteLine("")
If ProcessArguments(inargs) Then
' Show usage
Console.WriteLine("usage: DllExport.exe assembly [/Release|/Debug] [/Verbose] [/Out:new_assembly] [/name32:32bitsuffix] [/name64:64bitsuffix] ")
Console.WriteLine("")
Console.WriteLine("If neither name32 or name64 is specified, only a 32bit output will be generated.")
Return 1
End If
If Not File.Exists(rInputFile) Then
Throw New Exception("The input file does not exist: '" & rInputFile & "'")
End If
WriteInfo("DllExport Tool")
WriteInfo(String.Format("Debug: {0}", rDebugOn))
WriteInfo(String.Format("Input: '{0}'", rInputFile))
WriteInfo(String.Format("Output: '{0}'", rOutputFile))
Console.WriteLine("")
Disassemble()
ReadLines()
'---- for debugging, backup the original il
'SaveLines(@"C:\Temp\DllExport\Disassembled Original.il");
ParseAllDllExport()
'---- 32-bit
If rExportX86 Then
FixCorFlags(Platform.x86)
'---- for debugging, back up the tweaked il
'SaveLines(@"C:\Temp\DllExport\Disassembled x86.il");
Assemble(Platform.x86)
End If
'---- 64-bit
If rExportX64 Then
FixCorFlags(Platform.x64)
'---- for debugging, back up the tweaked il
'SaveLines(@"C:\Temp\DllExport\Disassembled x64.il");
Assemble(Platform.x64)
End If
Dim exportCount As Integer = rExportIdx - 1
Console.WriteLine("DllExport: Exported " & exportCount & (If(exportCount = 1, " function", " functions")))
Console.WriteLine()
Return 0
End Function
#End Region
#Region "Private, Protected Methods"
''' <summary>
''' Parse the arguments
''' </summary>
Private Function ProcessArguments(inargs As String()) As Boolean
rDebugOn = False
rVerboseOn = False
rInputFile = Nothing
rOutputFile = Nothing
rX86Suffix = Nothing
rX64Suffix = Nothing
rExportX86 = False
rExportX64 = False
'---- mainly for testing to allow swapping out command line args programmatically
Dim args As String()
If inargs Is Nothing Then
args = Environment.GetCommandLineArgs()
Else
args = inargs
End If
'---- parse each command line arg
For idx = 1 To args.Length - 1
Dim argLower = args(idx).ToLower()
If argLower.StartsWith("/name32:") Then
rExportX86 = True
rX86Suffix = args(idx).Substring(8).Trim("""".ToCharArray())
ElseIf argLower.StartsWith("/name64:") Then
rExportX64 = True
rX64Suffix = args(idx).Substring(8).Trim("""".ToCharArray())
ElseIf argLower = "/debug" Then
rDebugOn = True
ElseIf argLower = "/verbose" Then
rVerboseOn = True
ElseIf argLower.StartsWith("/input:") Then
rInputFile = args(idx).Substring(7).Trim("""".ToCharArray())
ElseIf argLower.StartsWith("/output:") Then
rOutputFile = args(idx).Substring(8).Trim("""".ToCharArray())
End If
Next
'---- if neither x86 or x64, then assume x86
If Not rExportX86 AndAlso Not rExportX64 Then
rExportX86 = True
End If
If rInputFile = String.Empty OrElse rInputFile Is Nothing Then
Throw New Exception("You must provide a filename to process.")
Else
If Not File.Exists(rInputFile) OrElse Me.FileFolder = String.Empty Then
'---- if there's no folder for inputfile, assume the current folder
rInputFile = Path.Combine(Directory.GetCurrentDirectory(), rInputFile)
If Not File.Exists(rInputFile) Then
'---- still can't find the input file, bail
Throw New Exception(String.Format("The input file does not exist: '{0}'", rInputFile))
End If
End If
'---- if no output specified, use the same as input
If String.IsNullOrEmpty(rOutputFile) Then
rOutputFile = rInputFile
End If
'---- return true on failure, false on success
Return String.IsNullOrEmpty(rInputFile)
End If
End Function
''' <summary>
''' Disassemble the input file
''' </summary>
Private Sub Disassemble()
rExportIdx = 1
System.IO.Directory.SetCurrentDirectory(Me.FileFolder)
Dim proc As New Process()
' Must specify the /caverbal switch in order to get the custom attribute
' values as text and not as binary blobs
Dim arguments As String = String.Format("/nobar{1}/out:""{0}.il"" ""{0}.dll""", Me.FileName, " /linenum /caverbal ")
WriteInfo("Disassemble file with arguments '" & arguments & "'")
Dim info As New ProcessStartInfo(Me.DisassemblerPath, arguments)
info.UseShellExecute = False
info.CreateNoWindow = False
info.RedirectStandardOutput = True
proc.StartInfo = info
Try
proc.Start()
Catch e As Win32Exception
Dim handled As Boolean = False
If e.NativeErrorCode = 3 Then
' try to check wow64 program files
Dim fn As String = info.FileName
If fn.Substring(1, 16).ToLower() = ":\program files\" Then
info.FileName = fn.Insert(16, " (x86)")
handled = True
proc.Start()
End If
End If
If Not handled Then
Throw (e)
End If
End Try
proc.WaitForExit()
If proc.ExitCode <> 0 Then
WriteError(proc.StandardOutput.ReadToEnd())
Throw New Exception("Could not Disassemble: Error code '" & proc.ExitCode & "'")
End If
End Sub
''' <summary>
''' Read all the lines from the disassembled IL file
''' </summary>
Private Sub ReadLines()
rLines.Clear()
If String.IsNullOrEmpty(rInputFile) Then
Throw New Exception("The input file could not be found")
End If
Dim ilFile As String = Me.FileName & ".il"
If Not File.Exists(ilFile) Then
Throw New Exception("The disassembled IL file could not be found")
End If
Dim sr As StreamReader = File.OpenText(ilFile)
While Not sr.EndOfStream
Dim line As String = sr.ReadLine()
rLines.Add(line)
End While
sr.Close()
sr.Dispose()
End Sub
''' <summary>
''' Save the current lines to the specified file
''' </summary>
Private Sub SaveLines(fileName As String)
Try
Dim folder = Path.GetDirectoryName(fileName)
If Not Directory.Exists(folder) Then
Directory.CreateDirectory(folder)
End If
Dim fileStream = File.CreateText(fileName)
For Each line As String In rLines
fileStream.WriteLine(line)
Next
fileStream.Close()
Catch
End Try
End Sub
''' <summary>
''' Fix the Cor flags
''' </summary>
Private Sub FixCorFlags(platform__1 As Platform)
For idx As Integer = 0 To rLines.Count - 1
If rLines(idx).StartsWith(".corflags") Then
Select Case platform__1
Case Platform.x86
rLines(idx) = ".corflags 0x00000002 // 32BITREQUIRED"
Exit Select
Case Platform.x64
rLines(idx) = ".corflags 0x00000008 // 64BITREQUIRED"
Exit Select
End Select
Exit For
End If
Next
End Sub
''' <summary>
''' Parse all DllExport entries
''' </summary>
Private Sub ParseAllDllExport()
Dim dllExportIdx As Integer = FindAttributeLine(-1, -1)
While dllExportIdx >= 0
ParseDllExport(dllExportIdx)
dllExportIdx = FindAttributeLine(dllExportIdx + 1, -1)
End While
End Sub
''' <summary>
''' Parse the DllExport entry
''' </summary>
''' <param name="dllExportIdx"></param>
Private Sub ParseDllExport(dllExportIdx As Integer)
Dim exportNameIdx As Integer = FindLineContains("string('", True, dllExportIdx, dllExportIdx + 5)
Dim calConvIdx As Integer = FindLineContains("int32(", True, dllExportIdx, dllExportIdx + 5)
Dim exportName As String = Nothing
Dim startIdx As Integer = 0
Dim endIdx As Integer = 0
If calConvIdx < 0 Then
Throw New Exception("Could not find Calling Convention for line " & dllExportIdx.ToString())
End If
If exportNameIdx >= 0 Then
startIdx = rLines(exportNameIdx).IndexOf("('")
endIdx = rLines(exportNameIdx).IndexOf("')")
If startIdx >= 0 AndAlso endIdx >= 0 Then
exportName = rLines(exportNameIdx).Substring(startIdx + 2, endIdx - startIdx - 2)
End If
End If
startIdx = rLines(calConvIdx).IndexOf("int32(")
endIdx = rLines(calConvIdx).IndexOf(")")
If startIdx < 0 OrElse endIdx < 0 Then
Throw New Exception("Could not find Calling Convention for line " & dllExportIdx.ToString())
End If
Dim calConvText As String = rLines(calConvIdx).Substring(startIdx + 6, endIdx - startIdx - 6)
Dim calConvValue As Integer = 0
If Not Integer.TryParse(calConvText, calConvValue) Then
Throw New Exception("Could not parse Calling Convention for line " & dllExportIdx.ToString())
End If
Dim callConv As CallingConvention = CType(calConvValue, CallingConvention)
Dim endDllExport As Integer = FindLineContains("}", True, calConvIdx, calConvIdx + 10)
If endDllExport < 0 Then
Throw New Exception("Could not find end of Calling Convention for line " & dllExportIdx.ToString())
End If
' Remove the DllExport lines
While endDllExport >= dllExportIdx
rLines.RemoveAt(System.Math.Max(System.Threading.Interlocked.Decrement(endDllExport), endDllExport + 1))
End While
Dim insertIdx As Integer = FindLineStartsWith(".maxstack", True, dllExportIdx, dllExportIdx + 20)
If insertIdx < 0 Then
Throw New Exception("Could not find '.maxstack' insert location for line " & dllExportIdx.ToString())
End If
Dim tabs As Integer = rLines(insertIdx).IndexOf(".")
Dim exportText As String = TabString(tabs) & ".export [" & (System.Math.Max(System.Threading.Interlocked.Increment(rExportIdx), rExportIdx - 1)).ToString() & "]"
If Not String.IsNullOrEmpty(exportName) Then
exportText += " as " & exportName
End If
rLines.Insert(insertIdx, exportText)
Dim methodName As String = UpdateMethodCalConv(FindLineStartsWith(".method", False, insertIdx - 1, -1), callConv)
If Not String.IsNullOrEmpty(methodName) Then
If Not String.IsNullOrEmpty(exportName) Then
Console.WriteLine("Exported '" & methodName & "' as '" & exportName & "'")
Else
Console.WriteLine("Exported '" & methodName & "'")
End If
End If
End Sub
''' <summary>
''' Update the method's calling convention
''' </summary>
''' <param name="methodIdx"></param>
''' <param name="callConv"></param>
Private Function UpdateMethodCalConv(methodIdx As Integer, callConv As CallingConvention) As String
If methodIdx < 0 OrElse FindLineStartsWith(".method", True, methodIdx, methodIdx) <> methodIdx Then
Throw New Exception("Invalid method index: " & methodIdx.ToString())
End If
Dim endIdx As Integer = FindLineStartsWith("{", True, methodIdx, -1)
If endIdx < 0 Then
Throw New Exception("Could not find method open brace location for line " & methodIdx.ToString())
End If
endIdx -= 1
Dim insertLine As Integer = -1
Dim insertCol As Integer = -1
Dim methodName As String = Nothing
For idx As Integer = methodIdx To endIdx
Dim marshalIdx As Integer = rLines(idx).IndexOf("marshal(")
If marshalIdx >= 0 Then
' Must be inserted before the "marshal(" entry
insertLine = idx
insertCol = marshalIdx
Exit For
Else
Dim openBraceIdx As Integer = rLines(idx).IndexOf("("c)
While openBraceIdx >= 0 AndAlso insertLine < 0 AndAlso insertCol < 0
Dim spaceIdx As Integer = rLines(idx).LastIndexOf(" "c, openBraceIdx)
If spaceIdx >= 0 Then
Dim findMethodName As String = rLines(idx).Substring(spaceIdx + 1, openBraceIdx - spaceIdx - 1)
' The method name is anything but "marshal"
If findMethodName <> "marshal" Then
insertLine = idx
insertCol = spaceIdx + 1
methodName = findMethodName
Exit While
End If
openBraceIdx = rLines(idx).IndexOf("("c, openBraceIdx + 1)
End If
End While
End If
If methodIdx >= 0 AndAlso insertCol >= 0 Then
Exit For
End If
Next
If insertLine < 0 OrElse insertCol < 0 Then
Throw New Exception("Could not find method name for line " & methodIdx.ToString())
End If
Dim leftText As String = rLines(insertLine).Substring(0, insertCol)
Dim rightText As String = rLines(insertLine).Substring(insertCol)
Dim callConvText As String = "modopt([mscorlib]"
Select Case callConv
Case System.Runtime.InteropServices.CallingConvention.Cdecl
callConvText += GetType(CallConvCdecl).FullName & ") "
Exit Select
Case System.Runtime.InteropServices.CallingConvention.FastCall
callConvText += GetType(CallConvFastcall).FullName & ") "
Exit Select
Case System.Runtime.InteropServices.CallingConvention.StdCall
callConvText += GetType(CallConvStdcall).FullName & ") "
Exit Select
Case System.Runtime.InteropServices.CallingConvention.ThisCall
callConvText += GetType(CallConvThiscall).FullName & ") "
Exit Select
Case System.Runtime.InteropServices.CallingConvention.Winapi
callConvText += GetType(CallConvStdcall).FullName & ") "
Exit Select
Case Else
Throw New Exception("Invalid calling convention specified: '" & callConv.ToString() & "'")
End Select
rLines(insertLine) = leftText & callConvText & rightText
Return methodName
End Function
''' <summary>
''' Assemble the destination file
''' </summary>
Private Sub Assemble(platform__1 As Platform)
Dim sw As StreamWriter = File.CreateText(Me.FileName & ".il")
For Each line As String In rLines
sw.WriteLine(line)
Next
sw.Close()
sw.Dispose()
Dim resFile As String = Me.FileName & ".res"
Dim res As String = """" & resFile & """"
If File.Exists(resFile) Then
res = " /resource=" & res
Else
res = ""
End If
Dim proc As New Process()
Dim extension As String = Path.GetExtension(rInputFile)
Dim outFile As String = Path.GetFileNameWithoutExtension(rOutputFile)
Select Case platform__1
Case Platform.x86
If Not String.IsNullOrEmpty(rX86Suffix) Then
outFile += rX86Suffix
End If
Case Platform.x64
If Not String.IsNullOrEmpty(rX64Suffix) Then
outFile += rX64Suffix
End If
End Select
If extension = String.Empty Then
extension = ".dll"
End If
outFile += extension
Dim argOptions As String = "/nologo /quiet /DLL"
Dim argIl As String = """" & Me.FileName & ".il"""
Dim argOut As String = "/out:""" & outFile & """"
If rDebugOn Then
argOptions += " /debug /pdb"
Else
argOptions += " /optimize"
End If
If platform__1 = Platform.x64 Then
argOptions += " /x64"
End If
Dim arguments As String = argOptions & " " & argIl & " " & res & " " & argOut
WriteInfo(String.Format("Compiling file with arguments '{0}", arguments))
Dim info As New ProcessStartInfo(Me.AssemblerPath, arguments)
info.UseShellExecute = False
info.CreateNoWindow = False
info.RedirectStandardOutput = True
proc.StartInfo = info
proc.Start()
proc.WaitForExit()
WriteInfo(proc.StandardOutput.ReadToEnd())
If proc.ExitCode <> 0 Then
Throw New Exception(String.Format("Could not assemble: Error code '{0}'", proc.ExitCode))
End If
End Sub
''' <summary>
''' Find the next line that starts with the specified text, ignoring leading whitespace
''' </summary>
''' <param name="findText"></param>
''' <param name="startIdx"></param>
''' <param name="endIdx"></param>
''' <returns></returns>
Private Function FindLineStartsWith(findText As String, forward As Boolean, startIdx As Integer, endIdx As Integer) As Integer
If forward Then
If startIdx < 0 Then
startIdx = 0
End If
If endIdx < 0 Then
endIdx = rLines.Count - 1
Else
endIdx = Math.Min(endIdx, rLines.Count - 1)
End If
For idx As Integer = startIdx To endIdx
If rLines(idx).Contains(findText) AndAlso rLines(idx).Trim().StartsWith(findText) Then
Return idx
End If
Next
Else
If startIdx < 0 Then
startIdx = rLines.Count - 1
End If
If endIdx < 0 Then
endIdx = 0
End If
For idx As Integer = startIdx To endIdx Step -1
If rLines(idx).Contains(findText) AndAlso rLines(idx).Trim().StartsWith(findText) Then
Return idx
End If
Next
End If
Return -1
End Function
''' <summary>
''' Find the next Attribute line
''' </summary>
''' <param name="startIdx"></param>
''' <param name="endIdx"></param>
''' <returns></returns>
Private Function FindAttributeLine(startIdx As Integer, endIdx As Integer) As Integer
If startIdx < 0 Then
startIdx = 0
End If
If endIdx < 0 Then
endIdx = rLines.Count - 1
Else
endIdx = Math.Min(endIdx, rLines.Count - 1)
End If
For idx As Integer = startIdx To endIdx
If rLines(idx).Contains("DllExportAttribute::.ctor") AndAlso rLines(idx).Trim().StartsWith(".custom instance void ") Then
Return idx
End If
Next
Return -1
End Function
''' <summary>
''' Find the line that contains the specified text
''' </summary>
''' <param name="findText"></param>
''' <param name="startIdx"></param>
''' <param name="endIdx"></param>
''' <returns></returns>
Private Function FindLineContains(findText As String, forward As Boolean, startIdx As Integer, endIdx As Integer) As Integer
If forward Then
If startIdx < 0 Then
startIdx = 0
End If
If endIdx < 0 Then
endIdx = rLines.Count - 1
Else
endIdx = Math.Min(endIdx, rLines.Count - 1)
End If
For idx As Integer = startIdx To endIdx - 1
If rLines(idx).Contains(findText) Then
Return idx
End If
Next
Else
If startIdx < 0 Then
startIdx = rLines.Count - 1
End If
If endIdx < 0 Then
endIdx = 0
End If
For idx As Integer = startIdx To endIdx Step -1
If rLines(idx).Contains(findText) Then
Return idx
End If
Next
End If
Return -1
End Function
''' <summary>
''' Get a string padded with the number of spaces
''' </summary>
''' <param name="tabCount"></param>
''' <returns></returns>
Private Function TabString(tabCount As Integer) As String
If tabCount <= 0 Then Return String.Empty
Dim sb As New StringBuilder()
sb.Append(" "c, tabCount)
Return sb.ToString()
End Function
''' <summary>
''' Write an informational message
''' </summary>
''' <param name="info"></param>
Private Sub WriteInfo(info As String)
If rVerboseOn Then
Console.WriteLine(info)
End If
End Sub
''' <summary>
''' Write an informational message
''' </summary>
Private Sub WriteError(msg As String)
Console.WriteLine(msg)
End Sub
#End Region
End Class
End Namespace
There’s really nothing tricky or earth-shattering here. Mainly calls to ILDASM
and ILASM,
and quite a lot of string parsing (looking for the DLLExportAttribute markers
and replacing them with the applicable IL code).
The DLLExport Marker Attribute
As for that DLLExportAttribute, its definition is much simpler:
vb">Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
Namespace DllExport
''' <summary>
''' Attribute added to a static method to export it
''' </summary>
<AttributeUsage(AttributeTargets.Method)> _
Public Class DllExportAttribute
Inherits Attribute
''' <summary>
''' Constructor 1
''' </summary>
''' <param name="exportName"></param>
Public Sub New(exportName As String)
Me.New(exportName, System.Runtime.InteropServices.CallingConvention.StdCall)
End Sub
''' <summary>
''' Constructor 2
''' </summary>
''' <param name="exportName"></param>
''' <param name="callingConvention"></param>
Public Sub New(exportName As String, callingConvention As CallingConvention)
_ExportName = exportName
_CallingConvention = callingConvention
End Sub
Private _ExportName As String
''' <summary>
''' Get the export name, or null to use the method name
''' </summary>
Public ReadOnly Property ExportName() As String
Get
Return _ExportName
End Get
End Property
''' <summary>
''' Get the calling convention
''' </summary>
Public ReadOnly Property CallingConvention() As String
Get
Select Case _CallingConvention
Case System.Runtime.InteropServices.CallingConvention.Cdecl
Return GetType(CallConvCdecl).FullName
Case System.Runtime.InteropServices.CallingConvention.FastCall
Return GetType(CallConvFastcall).FullName
Case System.Runtime.InteropServices.CallingConvention.StdCall
Return GetType(CallConvStdcall).FullName
Case System.Runtime.InteropServices.CallingConvention.ThisCall
Return GetType(CallConvThiscall).FullName
Case System.Runtime.InteropServices.CallingConvention.Winapi
Return GetType(CallConvStdcall).FullName
Case Else
Return ""
End Select
End Get
End Property
Private _CallingConvention As CallingConvention
End Class
End Namespace
What Next?
So, now that we can easily expose entry points from a .net assembly, what can we do with that?
For starters, as my previous post mentioned, I’ve built a .net plugin for the MAME front end called
Mala. It’s still in very early stages,
and is not yet available, but it definitely works.
Even more interesting, I’ve experimented with creating self-registering COM .net assemblies. But that will have to wait for another posting.
For the full project,
and a pre-compiled DLLExport.exe file, grab this
vbfengshui.com/content/other/DLLExport.zip">zip.
And definitely let me know how you use it!