I’ve written about exposing Entry Points from a .net dll before, most recently 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 standard 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:
- Robert Giesecke’s DLLExport utility and MSBuild target
- Selvin’s CodeProject entry called ExportDLL
- A very small C# console app from a poster on this forum thread
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 hand, 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 understand 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.
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:
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 zip.
And definitely let me know how you use it!
8 Comments
I am extremely inspired together with your writing abilities as neatly as with the format to your weblog. Is that this a paid subject matter or did you modify it yourself? Either way keep up the excellent quality writing, it is uncommon to see a nice blog like this one today..
Thanks for the comment. The blog layout is detailed in the footer, just scroll all the way down. I modified a freely available layout for WordPress.
Hi Darin, love the tool.
One issue I found, you miss the first arg passed on line 272
For idx = 1 To args.Length – 1
should be
For idx = 0 To args.Length – 1
Hi Joe. At first, I thought “How the heck could I have missed that one!”, but after doing a little digging, I believe the code is correct. Check out this doc on the GetCommandLineArgs func.
http://msdn.microsoft.com/en-us/library/system.environment.getcommandlineargs.aspx
That 0 argument will be the name of the application that is running, which I don’t care about for the purposes of parsing the command line.
Thanks for the comment, though, and I’m glad it’s been useful!
Hi Darin,
Yes my mistake, it only happens in Debug with the args string on line 92.
Doh! Yes, you are definitely correct about that. I completely missed it. I’ll get the listing fixed up.
Thanks!
I was suggested this web site via my cousin. I’m now not certain whether this submit is written by way of him as no one else understand such unique about my trouble.
You are amazing! Thank you!