Category Archives: .NET

Running Visual Studio as Admin Without the UAC prompts

0
Filed under .NET, Visual Studio

For several projects I work on, I need to load Visual Studio (2022 Enterprise edition) in “admin” mode.

That’s easy enough to do, but I always have to click through the UAC prompts, which is tiresome.

Turns out there’s a (relatively) easy way around this.

I stumbled across this article by Lowell Heddings on how to “Create Administrator Mode Shortcuts without UAC prompts” accidentally which clued me in that this was even possible. I highly recommend checking it out.

I’ve embellished the technique slightly because I found that VS would end up coming up behind all the other open windows, which is even worse than the UAC prompts.

I’ll summarize the process below:

  1. Use Task Scheduler to create a new task
  2. Give the task a short name (I called mine “Visual Studio 2022”, set it to Run only when user is logged in
  3. Use YOUR user
  4. Check Run With Highest Privileges
  5. Configure for Win 10/11 whatever works for you
  6. On the Actions tab
  7. Create new Start a program action
  8. Specify full path name to devenv (Visual Studio), no args; “C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\devenv.exe”
  9. Get nircmd from here http://nircmd.nirsoft.net/win.html (put it on your PATH)
  10. Add another Start a program action
  11. Set this action to “nircmd win focus process devenv.exe”
  12. On the Settings tab, make sure that the combobox near the bottom says “Run a new Instance in parallel” and not “Do not start a new instance”. That way, you can still launch multiple instances of Visual Studio if you need to.
  13. Save the Scheduled Task
  14. Create a new shortcut on the desktop
  15. For the shortcut path use “C:\Windows\System32\schtasks.exe /run /tn “{your task name from step 2 above}”

And that’s it.

If everything’s kosher, dblclicking on that shortcut will launch VS in Admin mode, in the foreground, with no UAC prompting.

0
Filed under .NET, SQL, Visual Studio

Command Line MSSQL

SQL on the command line!

There’s a number of tools out there for connecting to an MSSQL Database and running queries from the command line, but a colleague recently pointed out a new one I hadn’t seen before that actually works really really well, especially with Windows Terminal and Cmder.

First, make sure you’ve got Windows Terminal and Cmder installed. They are truly the bees knees when it comes to working in the command line under Windows!

Next, install mssql-cli. This app is actually a python script, so you’ll also need Python if you don’t already have it installed. Don’t fret, though. The link contains instructions on getting Python going, and once that’s done, installing mssql-cli is a single pip command:

python -m pip install mssql-cli

To test it, just open a command prompt and type:

mssql-cli

Now, that alone is nice, but if you’re like me, you have several test databases you connect to on a regular basis and entering credentials is troublesome at best.

Not to worry. There’s a batch file for that!

Now, it’s not the simplest in the world even though it is a single liner. So here goes:

wt -d c:\users\dhiggins\desktop cmd /k "c:\apps\cmdr\vendor\init.bat cd %CD% && mssql-cli -U {dbloginusername} -P {password} -d {dbname} -S {dbservername}"

Let’s break that down:

  • wt – starts Windows Terminal
  • -d – followed by the folder you’d like to start terminal in. Not strictly required, but it’s a nice add. I just set it to my desktop
  • cmd – This is used to get Cmder started in a tab in Windows Terminal
  • /k – tells cmd to execute the following quoted command and stay loaded
  • “c:/apps/cmdr/vendor/init.bat – this get Cmder started and the shell all initialized. Note that the path to your installed copy of Cmdr may be different from the “apps/cmdr” that I have here.
  • cd %CD% – Gets Cmder switched to the folder that this bat file is located in
  • && mssql-cli – Actually starts mssql-cli! The whole point of this exercise.
  • -U {dbloginusername} – Provider the UserName you use to log into your db server here
  • -P {password} – provide the database user password here
  • -d {dbname} – provide the database name here
  • -S {dbservername}” – And finally provide the database server name here. I’m just connecting up to the locally installed instance of SQL Server.

Save that as a BAT file and dblclick it to launch directly into a Cmder tab inside Windows Terminal connected to the DB of your choice. Perfection!

One big benefit from using Cmder, at least from what I can tell, is that it automatically supports horizontal scrolling of query result sets.

Notice that those right pointing arrows!

Just use <left><right> arrow keys to scroll the grid left and right as you page through results.

If you don’t use Cmder as your shell, scrolling won’t work like that unless you install something else called PyPager. Which I didn’t do.

Visual Studio Bonus!

Now, all this is well and good, but as they say on late, late night TV: Wait! There’s more!

I spend a lot of time in Visual Studio, so any way to stay there tends to be a positive for me, and one of the most recent additions to VS is built-in Terminal support.

Works a treat and even works with Cmder, so you get all that great Cmder goodness right inside Visual Studio.

But, you can create as many “Terminal Configurations” as you want, so here’s a shot of a few that I have, including one running Cmder directly and another starting the MSSQL-CLI directly to a specific database.

Easy and quick direct access to a specific db, Right inside VS!

Visual Basic 11 beta is out

3
Filed under .NET, Rants, VB Feng Shui

vs2010_iconlibrary

OK, It’s been a few weeks, so this is likely not “new” news, but still.

Looks like MS isn’t backing off on VB one bit, and that’s a good thing. Heck, as I understand it, there’s even a project in skunkworks build VB in VB! I’ve always said the mark of a complete language is when it’s actually written in itself.

But I digress. Back to VB 11. The VB Team website has a nice rundown of some of the high points of the release here.

 

 

 

 

To summarize:

  1. Async support. Meh. VB (actually any .net language) has always had this. The Async stuff definitely makes it easier, so I won’t complain too much. Personally, I’ve kind of gotten into the Javascript notion of lambdas for callbacks in support of async stuff. You can do that in VB, but lambda’s, being limited to single lines) haven’t been all that useful for it, up till now with….
  2. Multi-line Lambdas. That’s right, you can finally declare a Function()….. lambda that spans multiple lines.
  3. Iterators. Which basically means a Yield keyword that can fall out of an iteration loop to allow the iterating code to perform it’s work.
  4. Caller info. This is a big one for me. I’ve written more than my share of error logging/trapping/handling frameworks over the years, starting with VB3, on through VB6 and then into .net. Unfortunately, this feature has nothing to do with that!
    Caller info allows a function to easily and straightforwardly retrieve the Name, Line number or FilePath, of the calling function. While the Line number and Calling path are of dubious value, the caller name is incredibly powerful for things like front ending field accessors in data access libraries. Say you have a small function that retrieves a value from a VB, via a name. With this feature, you can easily create properties of an object that are named that name and then pass the property name down to the accessor function to retrieve the value.
    This has been doable for ever in .net as well (via the call stack), but was not straightforward and was susceptible to corruption if you used code obfuscators. No word on whether this feature will play better with obfuscators at this point, though.
  5. Call View Hierarchy. Looks like a very nice feature, but I’ll have to play with it more before making any concrete observations.
  6. Global Namespace. Another meh. On some projects, I could see this being handy. But it’s nothing to get too excited over. A very incremental improvement.

 

What’s missing

  1. Unsafe code. At least, I’ve seen no mention of it.
  2. Property-level scope. They added auto properties in VB10. Property-level scope seems like the next natural step for that. And by this, I mean:
    Public Property Name() as string
         Private _Name as string      <—– Property level scope
         Get()
                Etc
          End Get()
          Set ()
                Etc
          End Set
    End property

    EDIT: The suggestion that I posted to MS’s suggestion board ended up getting a comment from Lucian Wischik. You can read his full comments here.
    Property Scope Variables
  3. More Dynamic functionality, though you could argue that you can actually do quite a lot of dynamic stuff now in .net languages.

It’s good to see the language getting very close to parity with C#, and the news of a forthcoming VB.net coded compiler is even more exciting.

Authoring VB6 IDE Addins in VB.net

2
Filed under .NET, Utilities, VB6

imageOk, Ok, I know what you’re asking yourself right now.

WTF would you want to do this?

But bare with me.

Realistically speaking, VB6 has a very limited shelf life. I have plenty of VB6 apps that run just fine under Win7 (even 64 bit), but Under Win8? Who knows.

But, the truth is, there are plenty of businesses that rely on WinXP and VB6 apps today, and those businesses aren’t converting those apps to .net (or anything else) particularly quickly.

As it happens, I’m currently doing some maintenance and upgrade work on a VB6 application, and, while it IS a 10+ year old IDE, it does have it niceties and not-so-niceties.

CodeSmart, MZTools, CodeHelper, and other add-ins can certainly help, but one thing I’ve definitely missed now that I’ve worked with VS2005+ is persistent bookmarks and breakpoints.

That fact that VB6 didn’t save bookmark and breakpoint locations is a real shame.

Now, people have asked about this capability for years. I found posts going back to 2001 asking for this function, but the only utility that even came close was the open source CodeDawg project. And while I still have the source here, I can no longer even locate a link for it on the web. No matter, really, because it never did work particularly well, would miss breakpoints or bookmarks that you set sometimes, and just generally had a terribly interface.

But it was a valiant attempt!

Fast forward to today, and I’m back in VB6, working in a large and not particularly straightforward codebase, and I find myself really needing bookmarks and breakpoints that I can set and come back to the next day or whenever.

But .net provides so much simpler coding paradigms for many things now (generic collections, and direct support for subclassing anyone!) that after a few days working up a prototype add-in in VB6, I couldn’t stand it and had to see if it was possible to put something together in .net.

That’s right. A VB6 addin written in VB.net 2010!

It’s not quite finished yet, so I’ll hold off on presenting the full monty for now, but it most certainly is doable, and actually makes for a quite nice development and debugging experience.

First, fire up VS2010 and create a DLL Library project.

In the project  properties, you’ll need to make sure to check the “Make assembly COM visible”

image

Next, On the compile tab, you need to set “Register for COM Interop”

image

You’ll also need to add a reference to the VBIDE.DLL file:image

You can usually find the file under the COM tab on the Add Reference screen:

image

Finally, create a new CLASS, call it Connect, and paste this:

<ComClass("96861C1E-73A0-46E2-9993-AE66D2BC6A91", "1AEA0235-959D-4424-8231-8EBB9B9C85FE")> _
<ProgId("BookmarkSave.Connect")> _
Public Class Connect
    Implements VBIDE.IDTExtensibility

    Private _App As Application

    Public Sub OnAddInsUpdate(ByRef custom As System.Array) Implements VBIDE.IDTExtensibility.OnAddInsUpdate
        _App.OnAddInsUpdate(custom)
    End Sub

    Public Sub OnConnection(VBInst As Object, ConnectMode As VBIDE.vbext_ConnectMode, AddInInst As VBIDE.AddIn, ByRef custom As System.Array) Implements VBIDE.IDTExtensibility.OnConnection
        Try
            If _App Is Nothing Then
                _App = New Application
            End If
            _App.OnConnection(VBInst, ConnectMode, custom)
        Catch ex As Exception
            ex.Show("Unable to start addin.")
        End Try
    End Sub

    Public Sub OnDisconnection(RemoveMode As VBIDE.vbext_DisconnectMode, ByRef custom As System.Array) Implements VBIDE.IDTExtensibility.OnDisconnection
        _App.OnDisconnect(RemoveMode, custom)
    End Sub

    Public Sub OnStartupComplete(ByRef custom As System.Array) Implements VBIDE.IDTExtensibility.OnStartupComplete
        _App.OnStartupComplete(custom)
    End Sub
End Class

Now to explain a few things.

The two GUIDs you see in the first COMCLASS line, you’ll need to generate your own unique id’s for them.

Obviously, the ProgID will also need to be changed (the general convention is {addin-name.Connect}).

The Application object is my actual core addin application. I’ve intentionally kept all the addin’s code OUT of the Connect class, to keep it simple, and because this class MUST be made visible to COM so that VB6 itself can instantiate the CONNECT object (and thus kickstart your addin). Usually, the less “stuff” you have to expose to COM in a .net assembly, the better, and this represents the bare minimum.

To make debugging seamless, be sure to set the Debug Start Action to “Start External Program” and point it at your VB6.EXE file:

image

You’ll also need to make sure your addin is registered with VB6, so it will know to load it along with any other addins. You do that by creating a key like the “BookmarkSave.Connect” key below:

image

Be sure to change the “BookmarkSave.Connect” to whatever ProgID you’ve decided on.

Give it a whirl

With all that in place, you should be able to RUN your addin from VS2010, VB6 should start and your addin CONNECT object’s OnConnection method should be called.

Now it’s time to pull out my Duran Duran and SoundGarden discs and get to coding some good ol’ fashioned pseudo-OO VB6 goodness!

Determining Whether you’re in DesignMode in a Windows Phone 7 Project

1
Filed under .NET, Windows Phone 7

When you’re building usercontrols in a Windows Phone 7 project, it’s often necessary to know whether the code is running in Design Mode (i.e. being invoked by the Visual Studio IDE or by Expression Blend), or whether you’re actually running in the real program.

It’s a trivial matter, really, but it can be hard to remember exactly how to do it when I need it, so I wrapped it in an IsInDesigner function:

    Public Function IsInDesigner() As Boolean
        Return System.ComponentModel.DesignerProperties.IsInDesignTool
    End Function

Direct integration of ILMerge with VB.net Projects

0
Filed under .NET, Error Handling, MSBuild, Troubleshooting

I’ve been working on a few utility type applications lately (more on them later, once they’re firmed up), but one thing I’ve found incredibly useful for certain projects is ILMerge.

If you’re not familiar with it, ILMerge is a utility that essentially can combine two or more .net assemblies into a single assembly, and reducing an application’s footprint is something I’m particularly passionate about!

In the past, I’ve always created postbuild steps that included executing ILMerge as just a command line post build process, but doing it this way has always been problematic. To start, debugging in the IDE tends to be compromised at best, and not possible at worst. Plus it just never felt clean.

In researching the problem, I ran across three blog posts that have provided more than enough info to get me past the Post Build process and actually integrate ILMerge directly into the build process itself, and one very intriguing alternative.

First, Scott Hanselman wrote about his experiences using ILMerge to merge a VB.net assembly into an otherwise C# project back in 2007. A great article and a fantastic first step.

Then, I came across Daniel Fortunov’s  article about integrating ILMerge into the VS build process. His example is also very good and a little cleaner, I’d say than Scott’s approach, but both articles are definitely worth checking out if you find yourself needing this kind of thing.

Lastly, Jeffrey Richter wrote a terrifically short but incredibly eye-popping article about a technique to load .net assemblies dynamically from binary resources baked into a single assembly at compile time. Very simple and clean. A great technique to have in your toolbox if the need ever arises.

For my part, I’ve used the ILMerge technique to merge Mono.Cecil.dll (a C# library from the mono project for directly altering .net assemblies) into a VB.net utility that can repackage portions of an assembly’s PDB file directly into the assembly’s EXE or DLL file, to supply line numbers during the rendering of an exception’s stack trace without actually have to provide a PDB (and all the additional metadata about your assembly) to your client. A fantastic debugging technique and one that I’ve been working with (off and on) for several years now. I’ll write about it in more detail later.

Self-Registering COM .net Assemblies

21
Filed under .NET, Utilities

One of the big benefits of the .net system is the fact that it doesn’t rely on COM and the registry to find referenced assemblies. At it’s simplest, the .net loader (which is responsible for resolving external assembly references) looks in the same folder as the assembly that contains the reference, and if the referenced assembly is there, it’s loaded. Done.

Why on earth COM couldn’t have been this way from the beginning is beyond me (ok, actually it’s not, there were plenty of, at least at the time, what appeared to be good arguments for putting all that registration stuff in the registry, but that’s a whole different posting).

At any rate, the new style .net assembly resolution rules work great for .net assemblies, but not with .net assemblies that expose COM objects. And I’m still seeing plenty of projects where COM integration is a primary component. For various reasons, the code for these projects needs to be in .net, so that has left me having to deal with .net COM interop on more than one occasion.

UPDATE: 10/15/2011

I just discovered the <ComRegisterFunction> and <ComUnregisterFunction> attributes, which, initially, I thought might automatically do everything this entire post is about. Doh!

However, that’s not the case. These two attributes can be attached to functions that will be called by REGASM.EXE during dll registration or unregistration. But they do not create the standard dll entry points "DllRegisterServer” and “DllUnregisterServer”. Hence, even if you use these two attributes, you would still have a COM .net assembly that would have to be registered via RegAsm.

Fly in the Ointment

In all honesty, .net COM interop is not too bad. If you know how to use interfaces, the <COMCLASS> attribute, and a few other particular attributes, it’s not much more difficult to create COM components in .net (C# or VB) than it was to create them in VB6.

Except for one thing.

Registration.

COM Components pretty much have to be registered in the registry (ok there’s registry free COM, but that’s another article as well and it’s not often used). For registering COM dlls, the tool of choice has long been REGSVR32.exe. Unfortunately, self registering COM dlls require 2 C-Style entry points, called DLLRegisterServer and DLLUnregisterServer for RegSvr32 to work it’s magic and register or unregister the COM dll. You can see these entry points (and typically 2 others, DllCanUnloadNow and DllGetClassObject) if you open up just about any COM dll in DependencyViewer:

image

Registering a COM .net Assembly

After a little poking around on Google, I came across the RegistrationServices object. This object provides everything you need to register and unregister a COM .net assembly.

Here’s a snippet to actually register a .net assembly that exposed COM objects:

Dim asm = Assembly.LoadFile(AssemblyName)
Dim regAsm = New RegistrationServices()
Dim bResult = regAsm.RegisterAssembly(asm, AssemblyRegistrationFlags.SetCodeBase)

The Unregistration process is very similar.

The Typical Situation

The most common time that a COM dll is registered or unregistered is at installation or uninstallation time. The Windows Installer contains plenty of functionality to register and unregister COM dlls, and .net assemblies, so generally speaking, you often don’t need to even worry whether you COM .net assemblies can be self registered or not, because Windows Installer can automatically handle them.

However, if you want to distribute a dll with no install (maybe an xcopy deployment scenario, or any other situation where an full blown install would be more work than it’s worth), the scene isn’t so pretty. For your COM .net assembly to be registered properly, it will need to have REGASM run on it. REGASM is the .net version of REGSVR32.exe. Unfortunately, REGSVR32 is on the path of every Windows machine out there, so it can be run from anywhere on your system.

REGASM, however, is not. And finding it is often no picknick.

The Ideal Solution

In a perfect world, the Register for COM Interop option in the Visual Studio project options screen, would automatically create the required DLLRegisterServer and DLLUnRegisterServer entry points for you, just like VB6 did. After all, all that’s really necessary for that functions is a call to the RegistrationServices object as detailed above, pointing to the currently running assembly! It’s a trivial bit of code.

But alas, that minor detail was left out.

The Work Around (or ILAsm to the Rescue)

At this point, I knew how to register and unregister COM assemblies. I knew how RegSvr32 functions internally (all it does is perform a LoadLibrary on the given DLL file, attempt to retrieve the procaddress of the DllRegisterServer or DllUnregisterServer functions, call them if they exist, and fail with an error if they don’t).

The only thing missing was how to expose those two functions from a .net assembly as a standard C-style dll entrypoint.

It turns out that neither C# nor VB, at this point, can flag functions to be exported as entry points.

But…. msil (Microsoft intermediate language, what any .net app is initially compiled into) can.

And doing so is quite common knowledge. I’ve actually written about it before, most recently here.

That post mentions the original source for the DLLExport idea and provides a background and code, so I won’t replicate that here. Suffice it to say that essentially, what tools like this do is take a compiled. .net assembly, in which certain methods have been marked with a particular attribute, disassemble the assembly, tweak the resulting IL code slightly to expose those attributed functions as real entry points and then use ILASM to reassemble the tweaked code back into a normal assembly, only now with the entry points exposed.

So, to create a self-registering COM .net assembly, all that’s really necessary is to include this DLLRegister class in your project (along with the DLLExportAttribute class mentioned in the link above):

Imports System.Runtime.InteropServices
Imports System.Reflection

''' <summary>
''' Provides Self-Registration functions for COM exposed .net assemblies
''' </summary>
''' <remarks></remarks>
Public Class DllRegisterFunctions
    Public Const S_OK As Integer = 0
    Public Const SELFREG_E_TYPELIB = &H80040200
    Public Const SELFREG_E_CLASS = &H80040201


    <DllExport.DllExport("DllRegisterServer", CallingConvention.Cdecl)> _
    Public Shared Function DllRegisterServer() As Integer
        Try
            Dim asm = Assembly.LoadFile(Assembly.GetExecutingAssembly.Location)
            Dim regAsm = New RegistrationServices()
            Dim bResult = regAsm.RegisterAssembly(asm, AssemblyRegistrationFlags.SetCodeBase)
            Return S_OK
        Catch ex As Exception
            Return SELFREG_E_TYPELIB
        End Try
    End Function


    <DllExport.DllExport("DllUnregisterServer", CallingConvention.Cdecl)> _
    Public Shared Function DllUnregisterServer() As Integer
        Try
            Dim asm = Assembly.LoadFile(Assembly.GetExecutingAssembly.Location)
            Dim regAsm = New RegistrationServices()
            Dim bResult = regAsm.UnregisterAssembly(asm)
            Return S_OK
        Catch ex As Exception
            Return SELFREG_E_TYPELIB
        End Try
    End Function
End Class

compile the dll, run DLLExport utility on the dll to expose these marked functions, and presto, done!

But wait, There’s More!

This is all well and good, and it definitely works, but using a post-compile process like this is certainly not ideal. If you’ve signed or strong-named your dll during the compile process, that information will be lost during the recompile. Worse, your debugging PDB will not follow through, so debugging the post-processed dll can be tricky as well.

It’d be great is .net had a linker, and we could just link a precompiled OBJ file in that already contained the DllRegister and Unregister functions and export points. Unfortunately, .net doesn’t have a linking process.

A Dead End

However, there is a very nifty utility called ILMerge that CAN merge two or more .net assemblies into one. “Perfect!” I thought, but a quick test resulted in an error from ILMerge indicating that “one or more assemblies contains unmanaged code”. I can only guess that ILMerge doesn’t support exported entry points like this, at least not yet.

I’m Not Dead Yet!

A few days passed, and I was searching for a solution to a completely unrelated issue when I happened upon a page describing Microsoft’s Side-by-Side functionality. Something about the SxS extensions they use all over the place gave me an idea.

If all that the RegistrationServices object needs to register a COM .net dll is the filename, why not create a stub registration dll that contains DllRegisterServer and DllUnregisterServer functions that actually register a side by side dll that contains the actual  COM objects.

So, say you have a dll called MyDll.dll.

Under this scenario, you’d actually put your code into a dll called MyDllSxS.dll (side by side, get it <g>) and then just make a copy of this self-register handling dll and call it MyDll.dll.

When someone runs regsvr32 mydll.dll, the DllRegisterServer function in the stub actually registers the MyDllSxS.dll.

The stub then, is exactly the same for any self-registering COM .net assembly and can just be copied side-by-side as much as necessary.

Granted, this approach does increase the overall application footprint (since there are additional dlls to distribute), but because the extra files are very small (16k or so) and all exactly the same, I don’t see this as a huge issue.

Still Not Perfect

Unfortunately, even though the above approach works great, is simple, and doesn’t require peculiar compilation steps on your codebase, it does still mean you do have a second dll to distribute. And, realistically, if you’re going to do that, why not instead create a little EXE than gets distributed with your app and does the exact same thing as the DllRegisterServer and DllUnregisterServer functions?

Well, honestly, I don’t have a good answer for that. In truth, RegAsm does a little more than just registering and unregistering COM .net assemblies. But for general usage in the field, those two functions are all you really normally need, and building a quick console app to do just that is trivial given the above code.

At then end of the day, for those projects that need COM integration, I’ll probably just include the DLLExportAttribute directly in the project, include the DllRegisterFunctions class to supply the necessary entry points,  and finally run DLLExport on the compiled DLL in the Release mode build of the project to actual export those entry points.

It’s not ideal, but:

  • It works
  • It doesn’t require any  more DLLs than you’d otherwise include in the project
  • It’s not terribly onerous to implement
  • The resulting dlls register and unregister just as a savvy user would expect them to, via regsvr32.

Exposing C-Style Entry Points in a .net Assembly (revisited)

8
Filed under .NET, Code Garage

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:

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!

Determining Whether You’re Running on the Windows Phone Emulator or Not

0
Filed under .NET, Windows Phone 7

This is a tiny little quick-tip for Windows Phone 7

I came up with a tiny little function to make it easy to check whether you’re running under the Windows Phone Emulator this afternoon.

    Public Function IsEmulator() As Boolean
        Select Case Microsoft.Devices.Environment.DeviceType
            Case Microsoft.Devices.DeviceType.Device
                Return False
            Case Microsoft.Devices.DeviceType.Emulator
                Return True
            Case Else
                '---- just a fall back case
                Return True
        End Select
    End Function

It’s trivial really, I know, but I often wrap these kinds of utility functions in an easy-to-remember function name, because remembering “Microsoft.Devices.Environment.DeviceType” is a lot more difficult than “IsEmulator” <g>.

Showing a Bing Map in Windows Phone 7.1 With Directions

0
Filed under .NET, Windows Phone 7

image

For a Windows Phone 7 app I’ve been playing with, one last element I wanted to include was a “directions” function. Basically, I just wanted to pop up a Bing Map with a start and end point and directions, much like you’d get if you went directly to the maps application on the phone and manually entered the start and end point.

The API for this is pretty straightforward, at least for WP7.1 (Mango).

Essentially, you create a BingMapsDirectionsTask, supply a start and end point and invoke Show.

Dim bmt = New BingMapsDirectionsTask()
bmt.Start = New LabeledMapLocation(StartName, New GeoCoordinate)
bmt.End = New LabeledMapLocation(EndName, New GeoCoordinate)
bmt.Show()

The problem was that New GeoCoordinate element. For the BingMapsDirectionsTask, you have to supply two LabeledMapLocation objects, one for the start and one for the end point.

No big deal, but according to the docs, you need to supply a Name for the labeled location, and a coordinate (essentially, just a Lat/Lon). This means that if all you have is an address, supplying the lat/lon becomes an issue of geocoding, and as far as I can tell, there’s no built-in service on the phone to geocode addresses.

I was able to turn up several good examples of using the BingMapGeoCodeService online, and they all worked fine (once you go through the requisite step of obtaining an AppID from www.bingmapsportal.com).

But, in the process of making that work, I discovered something interesting.

When creating the LabeledMapLocation objects for the start and end points, if you supply an address for the name, and nothing  for the GeoCoordinate, the BingMapsDirectionsTask will automatically geocode the address!

The Disclaimer

I’m not sure whether this is intentional behavior, as the available docs for BingMapsDirectionsTask are pretty sparse, and it could be that this won’t make it into the final 7.1 release.

But, it sure makes interacting with BingMaps for simple direction functionality ridiculously easy.

The final code, then, would look like this:

Dim bmt = New BingMapsDirectionsTask()
bmt.Start = New LabeledMapLocation("12 Main St, Dallas, Tx 76011", Nothing)
bmt.End = New LabeledMapLocation("500 Oak Lawn Ave, Dallas, Tx, 76011", Nothing)
bmt.Show()

Granted, using the BingMapGeoCodeService, you’ll have a lot more flexibility, including the ability to specify the level of confidence at which to filter geocoding hits, and you can actually retrieve multiple hit values and present them to the user in whatever fashion you want.

Let me know how it works for you!