Search Results for: "resource

VB and Resource Files (Part 3)

1
Filed under Resource Files, Utilities, VB Feng Shui

In case you've missed the first 2 posts in this series, I'm discussing the concept of using Resouce files with Visual Basic 6.

In the first part, I talked about how to compile arbitrary information into a resource file and reference it from VB.

In Part 2, I discussed a technique for compiling a VersionInfo resource and writing it into your VB executable, thus replacing the incomplete VB provided VersionInfo resource.

In this installment, I wanted to share a little trick for defining the elements of the "Version number" in a resource (RC) file such that you only have to enter the version number once.

If you recall, I presented a sample RC file the last time, looking like this:

1 VERSIONINFO
FILEVERSION 1,2,3,4
PRODUCTVERSION 1,2,3,4
FILEOS 0x4
FILETYPE 0x1 //// 2 for dll, 1 for exe
{
BLOCK "StringFileInfo"
{
BLOCK "000004b0"
{
VALUE "CompanyName", "MyCompany"
VALUE "ProductName", "MyProduct"
VALUE "FileDescription", "MyFileDesc"
VALUE "LegalCopyright", "MyLegalCopyright"
VALUE "LegalTrademarks", "MyLegalTrademark"
VALUE "OriginalFilename", "MyOriginalFilename"
VALUE "ProductVersion", "1.2.3.4"
VALUE "FileVersion", "1.2.3.4"
VALUE "Comments", "MyComments"
VALUE "InternalName", "MyInternalName"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0000 0x04B0
}
}

If you'll notice, there is one thing about this script that no lazy programmer worth a macro recorder would tolerate. The Version Number is duplicated several times, and what's worse, in several different formats.

First, we have the FILEVERSION and PRODUCTVERSION entries, with comma separated numbers.

But then we also have the stringized version of those two elements, that must be specified as period separated elements within quotes.

I don't have an alcohol fueled Code Sync 5000 robot to keep those numbers straight so I figured I'd just use macro substitution. Right. Turns out, the Resource compiler (RC.EXE) is a mighty fickel beast.

What I envisioned was a simple block to define the version number:

#define MAJOR          1
#define MINOR          0
#define REVISION       0
#define BUILD          116

and then use those definitions further down in the file, like so

FILEVERSION MAJOR,MINOR,REVISION,BUILD

and similiarly

VALUE "ProductVersion", "MAJOR.MINOR.REVISION.BUILD"

But obviously, there's a problem; actually, more than just one.

I won't dwell on the details, but after much head scratching (polite euphimism), I ended up with this joyous concoction:

#define COMMAVER(mj,mn,rv,bl)        mj,##mn,##rv,##bl
// Commaver yields x,y,z,a  (necessary for the numeric versions)

#define PERIODVERPT2( mj )           #mj ""
#define PERIODVERPT3( mj,mn,rv,bl )  PERIODVERPT2( mj ) "." PERIODVERPT2( mn ) "." PERIODVERPT2( rv ) "." PERIODVERPT2( bl )
#define PERIODVER                    PERIODVERPT3( MAJOR,MINOR,REVISION,BUILD )
// PeriodVer yields x.y.z.a (necessary for the stringized versions)

// define the two numeric versions
// always make them the same                    
#define FILEVER        COMMAVER(MAJOR,MINOR,REVISION,BUILD)
#define PRODUCTVER     FILEVER

// define the two stringized version numbers, we just make them the same
#define STRFILEVER     PERIODVER
#define STRPRODUCTVER  PERIODVER

Then you can use them like so

....
FILEVERSION FILEVER
PRODUCTVERSION PRODUCTVER
....
VALUE "ProductVersion", STRPRODUCTVER
VALUE "FileVersion", STRFILEVER

Get clever with the #include directive and you can easily keep the version numbers of all the separately compiled VB components of your project synchronized and only have to set the version number in a single place.

If there's a simpler way, I'd love to hear about it. But this works, and it keeps all my version numbers straight.

VB and Resource Files (part 2)

0
Filed under Resource Files, VB Feng Shui

If you've ever looking closely at Windows applications, you know that Windows Version Numbers are composed of 4 parts:

  • Major version
  • Minor version number
  • Revision number
  • Build number

So a version of 4.8.3.9888 would typically mean Major version 4, Minor version 8, Revision 3, Build 9888.

If you've ever looked at VB's Project properties box, though, you've probably noticed the disconnect.

ProjProps

Obviously, VB directly supports the Major and Minor, and appearently, the Revision number.

But, build a VB6 app with unique values in for each number and then view the app's properties via Windows Explorer:

AppProps

In this particular app's case, Major and Minor are 0, and Revision was set to 49. However, according to the Windows Property panel, the Build number is 49. VB internally makes the Build number what you enter as the Revision number in the VB project property UI.

Now, whether this was just a typo on the part of the VB developers or an intentional "feature", I can't say. But it definitely can cause confusion and make it difficult to identify versions of your app out in the field. Then there's the constant explaining of why your app's revision always seems to be 0, but other applications have something there.

In a previous post on VB and resources, I mention the Microsoft Resource Compiler, a utility that can compile resource files into RES files, which can then be compiled into your application by VB.

This combination works wonders if all you want to do is embed icons, bitmaps, strings or arbitrary files into the resources of your app.

And, if you look at the RC documentation, you'd see info on VERSIONINFO statement that is used to define a Version Info Resource.

So, it would stand to reason that if you created a resource file, say like this:

VersionInfoTest.rc

1 VERSIONINFO
FILEVERSION 1,2,3,4
PRODUCTVERSION 1,2,3,4
FILEOS 0x4
FILETYPE 0x1 //// 2 for dll, 1 for exe
{
BLOCK "StringFileInfo"
{
BLOCK "000004b0"
{
VALUE "CompanyName", "MyCompany"
VALUE "ProductName", "MyProduct"
VALUE "FileDescription", "MyFileDesc"
VALUE "LegalCopyright", "MyLegalCopyright"
VALUE "LegalTrademarks", "MyLegalTrademark"
VALUE "OriginalFilename", "MyOriginalFilename"
VALUE "ProductVersion", "1.2.3.4"
VALUE "FileVersion", "1.2.3.4"
VALUE "Comments", "MyComments"
VALUE "InternalName", "MyInternalName"
}
}
BLOCK "VarFileInfo"
{
VALUE "Translation", 0x0000 0x04B0
}
}

Then, all you should have to do is compile the RC file with RC.EXE, load up VB, add the resulting RES file to your project and you'd be done.

Unfortunately, the path to enlightnment is never straightforward, and neither is the process of getting a proper version number into a VB6 executable.

The problem is that VB post-processes the compiled executable in order to put the version information from the Project properties screen into the VERSIONINFO resource. This means that the nice, correct VERSIONINFO resource that you just compiled into the executable get's stomped on by whatever you happen to have in the Project Properties dialog, and that dialog will always reset the Windows Revision number to 0, and use the VB Revision number as the Windows Build number.

What you have to do is post-post-process your exe and put the correct VERSIONINFO resource back in after VB is completely finished with the file.

And the easiest way to do that is with a handy free utility called Resource Hacker. This utility allows you to easily open, view, and even extract all the resources in any EXE or DLL. If you want to just pull all the icons out of a file's resources, there are definitely better ways. But if you really want to poke around the resources in a file, ResHacker is perfect. Plus, it's got a very handy, if not a little arcane, command line interface that will allow you to automate much of the process via MAKE files or batch scripts.

Make sure the RESHACKER.EXE is on your path, then run:

reshacker -addoverwrite "yourexe.exe", "yourexe.exe", "yourexe-VersionInfo.res", versioninfo, 1 , 1033

I'm assuming your compiled application is called yourexe.exe, and that you've compiled an RC file with a VERSIONINFO resource in it to the file yourexe-VersionInfo.res.

Resource hacker will dutifully wipe out the VB created VERSIONINFO resource and replace it with the one compiled from your RC script.

One important note, though. ResHacker will not merge a single resource, and all of the version information is considered a single resource. That means that you need to specify all the pertinent version info properties in your RC file, because everything specified via the VB Project Properties dialog will get replaced.

"But", you say, "the version numbers themselves appear to be replicated 4 times in the RC file! I'm a lazy programmer and the thought of updating 4 copies of the version number just seems, well, wrong."

And you'd be right.

Fortunately, there is a way to convince RC.EXE to allow you to specify the version number for your app only once. However, doing so is, like the concept of using resources in a VB app, more complicated that you would at first imagine.

I'll discuss that in my next post.

Visual Basic 6 and Resource Files

0
Filed under Resource Files, Utilities, VB Feng Shui

Feng Shui is all about the placement of things to better harmonize with their (and consequently your) surroundings.

With VB, there's no better place to examine that than the file footprint of your application.

Ask yourself: You have two apps to evaluate. They perform identically, are configurable in all the same ways and behave exactly the same. One app consists of hundreds of files scattered across dozens of folders. The other's footprint is exactly one file, the EXE itself. Which would you choose?

Granted, that's an extreme case, but the point is, the smaller your app footprint, the better, in almost all circumstances.

And a really nice way to shrink an app footprint is with resources.

If you've never messed around with resource files in VB6, or maybe only tried to use the built in resource editor add-in, you really don't know what you're missing.

Resources can be an immensely handy way to embed everything from pictures to WAV files, to chunks of script, to icons, and just about anything else directly into your application, but in a way that makes them accessible to the outside world, for one-off customizations, translations, or to just keep the file footprint of your app as small as possible.

However, resource files can be notoriously difficult to work with because of the lack of tools available natively with VB6. VB.NET dramatically improves upon the situation, but there's still a lot of VB6 code out there that might benefit from resources.

Generally speaking, you have two "types" of resource files where VB6 is concerned.

  • the resource file the can be "included" in the project and is available to your code while you're running in the IDE
  • Any resources "added" to your applications EXE/DLL/OCX file AFTER you compile it.

Why the distinction?

There's one specific kind of resource that you very much should want to include in your compiled file, but which VB is notoriously lacking about. The Version Info Resource.

VB only allows you to specify the Major, Minor, and Build version numbers (although VB calls the Build number, the "revision" number, mysteriously).
Windows executables, on the other hand, support a Major, Minor, Revision, and Build.

Now, the truth is, VBs support is generally fine for utilities, hobbiest programs and the like. But real, commercial applications really should make use of all four numbers, and that's something that is impossible using VB alone.

Ah, you say, VB does have that "Resource Editor" add-in, just use it! Not quite. You can't create a version info resource in it, and even if you could, VB's compiler replaces any version info element within that resource file with the information from the project properties window.

The solution is relatively simple and still preserves all the great things that resource files in VB can do for you.

The IN-THE-IDE Resource file

For this resource file, you have 2 choices, use the VB Resource Editor Add-In, or create an RC resource script file, and compile it to a RES binary format file that VB expects. I prefer the later, simply because scripting the resource file makes it much easier to include things like big chunks of text (xml, scripts, icons, what-have-you), and you can leave all of those things external to the RES file without having to manually pull them in via the Editor add-in every time they change.

I usually create an RC file with the same name as the VBP file, but with an RC extension

test.RC

// Test Resource Compiler file
// used to automatically compile the RC file into a RES file
// BEFORE the project itself is compiled
// This makes the Resource data available to code while in the IDE
//
//-----------------------------------------------------------------------------------
// Arbitrary Text File Resources
//
//----------------------------------------------------------------------
TEXTRES1   TEXTRES_DEFS PRELOAD DISCARDABLE TextRes1.txt
TEXTRES2  TEXTRES_DEFS PRELOAD DISCARDABLE TextRes2.txt

//-----------------------------------------------------------------------------------
// Bitmap Resources
//-----------------------------------------------------------------------------------
IMG_MAIN  BITMAP PRELOAD  DISCARDABLE ".\test.bmp"

Note that test.bmp is just some random bitmap file, and that TextRes1.txt and TextRes2.txt are arbitrary text files.

Then, you can access those resources via a little bit of VB code

To get the text file resources

Dim a() As Byte
dim buf$
a() = LoadResData("TEXTRES1", "TEXTRES_DEFS")
buf$ = StrConv(a, vbUnicode) 'need to convert the raw ansi text file content to UNICODE to make VB happy

Or to load the bitmap

Set form.Picture = LoadResPicture("IMG_MAIN", vbResBitmap)

Icons, and string tables are a little more difficult, esp. with respect to XP and VISTA format icons, so I'll worry about them later.

You can compile the RC file using a command line similiar to the following:

rc.exe /r /fo "test.res" "test.rc"

Make sure the RC.EXE resource compiler is on your path for it to run right. The RC.EXE file itself should be somewhere in the VB6 installation folder under Program Files. For a muhc more thorough explanation of the Resource Compiler, check here. Microsoft has some very good RC information here also.

Alternatively, you can put this command in a BAT file, and execute it quietly:

cmd /Q /c rc.ex /r /fo "test.res" "test.rc"

One caveat. If you decide to use the RC file, be aware that VB loads the compiled RES file when you load the project. So if you make a change to one of the component resource files (say, an ICO or BMP file), you'll need to exit VB, recompile the RC file to a RES file, and then RELOAD the project in VB. Since RES files don't change all that much once initially created, this isn't a huge problem.

Another note: You can only include one resource file in a single VBP project, so it has to contain all the resources the app will need (except, of course, for the Version Info Resource).

And a final note: You can easily add a resource file to a project by
1) compiling the RC file to a RES file
2) Opening your VBP project
3) dragging the RES file from Explorer to the VB Project Window.

For next time, handling the Version Info Resource....

The Disambiguator (A KeePass Plugin)

0
Filed under Uncategorized

The TLDR;

For the impatient crowd :)

  • Download The Disambiguator KeePass plugin here.
  • Unzip it and place the PLGX file in the KeePass "Plugins" folder, just like any other plugin.
  • Restart KeePass. You should see a "Compiling Plugins" notice and then KeePass should start normally.
  • In KeePass, click "Tools", "Plugins" and verify that The Disambiguator is now in the list of loaded plugins.
  • Edit the AutoType entry for a credential set that has an ambiguous Window Title.
  • add "{exe:nameOfExecutable}" (without doublequotes) at the end of the WindowTitle. Replace nameOfExecutable with the filename of the Executable program file that the Target Window belongs to. In the case of Quicken, for instance, that would look like {exe:qw.exe}

Now open your target application and use AutoType as you normally would. If everything is set right, KeePass will now automatically select the appropriate credentials for the target window using both the Window Title and the application executable.

Introduction

I've used the password manager KeePass for a very long time. Excellent, simple, clean install, low resource usage and fast.

Editing a Credential Set in KeePass

The AutoType support, while possibly not quite as simple as some full commercial packages, is quite flexible and has never let me down.

Well, almost never.

KeePass's AutoType feature works by allowing the user to enter one or more "Window Titles", which it then compares against the target window when you user presses the AutoType hotkey.

When only one configured Window Title matches the Window Title of the target window, KeePass automatically can choose that single set of credentials, and perform the autotype. It's almost like magic!

Trouble Ahead

But, what happens when there are 2 or more credential sets that have the same Window Title?

For instance, both Quicken and Exodus request the user to login via a login screen with the Window Title "Enter Password".

If you're "responsible" with you passwords, and we all are as KeePass users (right?!), then you're not using the same credentials (username and password) for two different applications. This means you likely have two different sets of credentials stored in KeePass: one for Quicken and one for Exodus. And you've setup AutoType for both.

But there's the rub. Both sets of credentials have AutoType setup to key on the Window Title of "Enter Password", as they must.

The KeePass Credential Chooser

Unfortunately, when you have, say, the Quicken Login window displayed and press the KeePass AutoType hotkey (normally Ctr-Alt-A), KeePass discovers 2 sets of credentials (and possibly more) that match. Because it can't magically decide which to use, it pops up the Credential Chooser dialog so that you, the user, can make the choice.

This is all fine and good, but it can be a bit of a pain, and it forces the user to make a choice when the choice should be automatic.

Enter The Disambiguator

The problem here, of course, is disambiguation. Using the Window Title as the only "disambiguating" factor is ok most of the time, but, as the above example shows, it's not always quite enough to completely identify a single specific credential set to use for autotype.

Alex Vallat has a very nice plugin for KeePass called WebAutoType which specifically allows users to configure matching URLs for autotype entries. This will almost always provide enough uniqueness to properly match a single credential set for Web Pages. But unfortunately, it does nothing for normal desktop apps like Quicken or Exodus.

That's where The Disambiguator comes in.

to be continued

SED and AWK for the VB Guy

0
Filed under Utilities

Generally speaking, when I need to do a little file manipulation, I usually fire up .net, whip up a little VB.net command line app to do the trick and off I go.

However, a few nights ago, I needed to do some manipulation on a largish (30+meg) xml file. The manipulation itself was fairly simple:

  1. Find a tag in the file
  2. Insert the contents of other files into the target file, right before the tag

However, it was late, and I was feeling a bit lazy, so I googled it.

What I got was almost all the first page results pointing me to SED or AWK.

What’s that?

SED is short of Stream EDitor. Essentially, it’s an app for running a text file through a set of regular expressions and outputting the results.

AWK is short for Aho, Weinberger and Kernighan, the names of the three programmers who originally came up with it. It’s actually a language for processing text. But, any more, it generally refers to the command line application to applying that language to a input file and generating output from it.

Not big on UNIX

Now, I’ve been around long enough to know what SED and AWK are, but I’ve really never actually used them. However, with all these search results pointing that direction, I had to poke around a little more.

You can grab a version of SED for Windows here:

http://gnuwin32.sourceforge.net/packages/sed.htm

And AWK (or GAWK, the gnu version of AWK, get it<g> ) here:

http://gnuwin32.sourceforge.net/packages/gawk.htm

Those pages have tons of excellent resources, as well as examples, all the docs you’d ever want to read, etc.

And these two apps have been around for so long, that, well, a quick Google search will turn up an example of just about anything you’d need to do with them, so I’m not going to muddy up search results any more than to say that they are really handy tools, especially if you know a little bit about regular expressions.

A Windows Observation

However, I would point out one fairly minor nit that I ran into, at least with the above two ports that I tried.

Both work just fine, but SED I found a tad more troublesome to install. The main problem was that it relies on several external DLLs. You can see these dependencies using DependencyWalker:

image

These files need to be in the same folder as the SED.EXE, and they’re all available at the above link. I guess my feeling is that for such a singular tool, these kinds of dependencies should be compiled in. At one point, many many moons ago, it made at least a little sense to reduce your app diskspace requirements by relying on shared dlls and such. But these days, no one cares if an app like this is 150k vs 500k with all the dependencies compiled in.

AWK (or GAWK), on the other hand, has NO dependencies. None. I copied it to my TOOLS folder, which is on my PATH, and viola! Worked right off. Truly an 0-hassle installation.

They both work very similarly, though SED relies mostly on regular expressions, whereas AWK certainly can be used in conjunction with only regular expressions, but also has the full AWK language behind it to boot.

Speed

One note about speed. There’s nothing to note!

Both of these apps were so fast, even against a 33mb input file, that I didn’t even notice they took any time at all. Running them against this file took about the same time as to actually copy the file.

Granted, my needs were simple, and I’m sure more complex expressions would slow things down. But still. That was refreshing.

And that thing I needed it for?

Removing a singular tag from a large XML file automatically:

awk "!/<\/tag\x3E>/" File1.xml >output.xml

Most of the weird look is from:

  • Having to escape the “/” with a “\”
  • Can’t use a “>” in a batch file command line, because it’s interpreted as a “pipe into an output file” command, which I’ve done at the end of the command with “>output.xml”, so I have to escape it as “\x3E”

I suspect I’ll be using it considerably more in my future!

BookmarkSave Addin for VB6

79
Filed under VB6
EDIT: I've finally gotten around to publishing the source for this addin on CodePlex. https://bookmarksavevb6.codeplex.com EDIT 7/25/2012: I've just updated the addin (see the ZIP download below) to correct a problem that can occur if you have several projects loaded in a group, and have files of the same name loaded in each project, AND you set bookmarks or breakpoints in those files. In that case, the breakpoints/bookmarks can end up being restored to the wrong file when you load the group of projects. This won't cause any problems in your code, but your bookmarks and breakpoints won't be restored properly, and you might receive messages saying that "a breakpoint can't be set on that line", or something similar.  Just download the latest version, unzip it and copy the dll over your existing dll. If first blogged about this addin back here. Essentially, the idea is to solve a long standing pet-peeve of mine with the VB6 IDE; the fact that it doesn’t save your bookmark or breakpoint locations from one run to the next. Ugh! That functionality wouldn’t have taken more than about 30 minutes for someone to implement on the VB team, but, alas, no one did, and I’ve spent far more time than I should have manually restoring breakpoints ever since. If, like me, you’ve worked in VB.net for any amount of time, and you now find yourself, from time to time, having to load up your trusty copy of VB6 to do some maintenance work, you, almost certainly also like me, sorely lament that missing functionality. But no more! After quite a bit of teeth-gnashing, along with some very helpful testing comments from Sam (Thank You!), I think it’s about time to correct that long-suffered oversight! Download the Addin here: BookmarkSave Addin ZIP The Zip file contains one DLL. Just extract it where-ever you want, and run REGSVR32 on it (just like any other COM dll). NOTE: You will have to have the .net framework 4.0 installed, as this addin is compiled in .net against that version of the framework. Why, you may ask? Well, frankly, VS2010 is much nicer to code in than VB6, and I was actually quite curious whether a VB6 Addin that performed a real, useful, function could be written in VB.net and act, more or less, just like an addin that had been written in VB6. Personally, with this, I think the answer is a pretty deafening “Oh Yeah!”… DISCLAIMER: As with anything else around these parts, your mileage may vary (the normal “It works on my machine”). I’ve tested it in conjunction with a few of the addins I use (CodeSmart and MZTools, as well as the Mouse Wheel fix, and resource editor) and have had only one minor issue. If CodeSmart is loaded  as well as BookmarkSave, CodeSmart causes the VB6 IDE to crash when you unload it, but only if you ran your application in the VB6 IDE. If this scares you, don’t download or install this addin. I worked around a number of CodeSmart “peculiarities” concerning the way they happen to subclass VB IDE code windows, but so far, the source of this problem eludes me. The good news is that other than the Crash dialog, there doesn’t appear to be any other anomalies (your project saves fine, runs, etc). My suspicion is that during the Addin UNLOAD phase of the VB6 shutdown process, a pointer isn’t being released quite right.

Using the Addin

Well, it can’t get much simpler. Once you’ve registered it, load up VB6 and check the Addins Manager to make sure it’s loaded. If it is, load up a project of your choice, set a few breakpoints and bookmarks, and close the project. Reopen it in VB6 and you should see all your breakpoints and bookmarks wondrously reset just like they were before! There aren’t really any settings or options to speak up. Bookmarks are saved into an XML file called {projectname}.BM in the same folder as your project’s VBP file. Breakpoints are saved into a {projectname}.BP file in the same place. Naturally, you should probably not check those files into version control.

New Hotkeys

Sadly, VB6 doesn't provide many convenient ways of navigating bookmarks and breakpoints, so this addin adds a few. Ctrl-K - Toggle a bookmark at the current cursor location Alt-Left or Right - Navigate to the next or previous bookmark Alt-Up or Down - Navigate to the next or previous breakpoint These aren't configurable at this time.

A few interesting bits

None of this is necessary to actually use the Addin, but I’ll mention it for the curious: First, I make use of my DLLExport utility to actually export the DLLRegisterServer and DLLUnregisterServer functions, so that the DLL can self register with COM, just like a VB6-authored DLL can. With all the other great COM support in .net, this is a really glaring omission. I wrote about that technique here. Next, I use my “GenerateLineMap” utility to strip line number info from the PDB file and embed it into the DLL. This information is then used during error handling to annotate any stack trace with real source code line numbers, without actually including the PDB as a separate file. This is a technique I’ve worked on for quite some time, and talked about here, here, and here. Still need to get that article written up.

Gaining Access to a Running Instance of Windows Media Player In VB.net

7
Filed under Code Garage, VB Feng Shui, WindowsMediaPlayer

Over the past few weeks, I’ve been working on a little screensaver side project to run in an old radio cabinet that I’ve converted into a touchscreen jukebox. Details of that project will be coming, but for now, I thought I’d share some interesting software bits I’ve discovered.

The screensaver that I’ve working on is intended to:

1) Recognize a Media Player that’s running on the computer when the screensaver loads up.

2) Connect to and monitor that media player for any changes in what’s it playing. Particularly, I want to know Artist name, album name and track name.

3) Hit a few search engines for some images related to what’s playing and display them in a nice “Ken Burns Pan and Zoom” style animation.

Well, long story short, WPF made short work of the image animations, and connecting to two of my favorite media players, J River Media Center and Album Player, was pretty trivial.

But, within just a few days of posting the first version, someone asked if it worked, or could work, with Windows Media Player (and, as it turns out, Windows Media Center, which is just a nicer shell over WMP).

Why Not?

My first thought was, sure! All I have to do it be able to see what’s currently playing and when it changes. Shouldn’t be too tough, right?

Well, after quite of bit of digging, it turns out that Windows Media Player (WMP), is far more gracious about hosting “plugins” that it is about being remotely attached to. There are several plugins available for WMP that write out info on the current playing media to an XML file, or the registry, or a web service, or whatever. But that requires "my application’s user” to install some other application to make things work. Not cool. At least, not for me.

Plan two. Most MS Office apps register themselves with the Running Object Table (The ROT). Other programs can query the ROT and retrieve objects from it that they’re interested in connected to. You often see the VB GetObject() function used for this purpose.

But WMP doesn’t register itself with the ROT, so that’s not an option.

On to Plan C.

WMP Remoting

However, as luck would have it, MS did do something about this type of situation. They call it “Media Player Remoting”. However, it’s just about the least documented concept I’ve come across yet. There’s just very little info about exactly how to set up this “remoting” or what it’s capable of.

Eventually,though, I did come across mention of a demo project written by Jonathan Dibble, of Microsoft no less, that illustrates the technique in C#. There’s a thread here that contains links to the original code, though that page appears to be in Japanese.

Looking further, I found several variations of Dibble’s project, some vastly more involved and complex than others.

I grabbed the simpler version and started hacking!

The Conversion

Converting Mr. Dibble’s code was fairly straightforward. He did a pretty fair job in commenting it and breaking things down nicely. As usual, one of my favorite Web Resources, DeveloperFusion’s CodeConverter, got a workout, and did a fine job on most of the conversion gruntwork.

But when the dust cleared, it didn’t work.

After a lot of stepping with the debugger, it turns out that Jonathan’s handling of  IOleClientSite.GetContainer isn’t quite right. His original code threw a “Not implemented” exception that would cause a crash for me every single time.

The function itself isn’t particularly useful for what I needed to do, and after reading up on the documentation, I felt certain that there’s really wasn’t anything that “needed” to be done in that function. But, it did get called by WMP, and something other than throwing an exception had to be done there.

Then, I realized that a number of other OLE-centric functions that Jonathan had implemented had a return value of HRESULT, and simply returned a E_NOTIMPL value.

So, I changed up the definition of GetContainer and had it returning an E_NOTIMPL, and presto! It works!

Since Jonathan’s demo project appears to be at least 4 years old, I’m not sure whether I may have indeed worked that way at one point, or was a bug, or, quite possibly, was something I didn’t get quite right in the conversion in the first place. Regardless, this version works, so bob’s your uncle.

How to Use It

For anyone not interested in the details, I’ll dive right in to how you actually use this technique.

First off, you’ll need to add a reference to WMP.DLL. This file should be in your Windows\system32 folder if a Windows Media Player is installed. Once added, you’ll have a WMPLib reference in your References tab:

image

Next, copy the WMPRemote.vb file into your own project.

Finally, though this isn’t strictly necessary, you may want to alter your project’s AssemblyInfo.vb file and set the CLSAttribute to false.

....
<Assembly: AssemblyCopyright("blah")> 
<Assembly: AssemblyTrademark("blah")> 
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'THE BELOW LINE YOU MIGHT ADD OR CHANGE TO "FALSE"
<Assembly: CLSCompliant(False)> 

'The following GUID is for the ID of the typelib if this project is exposed to COM
<Assembly: Guid("12333333-33454-1233-1234-123451234512")> 
.....

The main class to work with is WMPRemote. It has two static properties; IsWindowMediaPlayerLoaded, and ActivePlayer.
These are static properties, so you access them using the WMPRemote class, like so:

If WMPRemote.IsWindowsMediaPlayerLoaded Then
   Dim Player = WMPRemote.ActivePlayer
End If

At that point, if WMP is loaded, you’ll have a reference to the full WMPlib.WindowsMediaPlayer object in the Player variable.

From there, you can do whatever you need to.

Query properties:

Debug.print Player.playState 

Attach to events:

AddHandler Player.CurrentItemChange, AddressOf CurrentItemChange
AddHandler Player.PlayStateChange, AddressOf PlayStateChange

Or whatever else is necessary.

How It Works

WMPRemote

Since this class is the main access point, I’ll start here. There are only 2 static functions defined here:

IsWindowsMediaPlayerLoaded

This function simply uses the .net Processes object to query for any processes named WMPlayer. If there are any, it returns TRUE, if not, FALSE. Obviously, it could be wrong, but that’s not terribly likely.

ActivePlayer

If things have already been initialized, this function just returns whatever it’s already retrieved for the Active WindowsMediaPlayer object.

If not, it checks if WMP appears to be loaded and, if so, creates and shows an instance of the internal frmWMPRemote form. During the load of this form, it’s immediately hidden so the user will never see it.

The only purpose of frmWMPRemote, is to host the WindowsMediaPlayer ActiveX Control. This all happens during the form Load event:

Me.Opacity = 0
Me.ShowInTaskbar = False
_InternalPlayer = New WMPRemoteAx
_InternalPlayer.Dock = System.Windows.Forms.DockStyle.Fill
Me.Controls.Add(_InternalPlayer)
Me.Hide()

Note that it actually is creating an instance of the WMPRemoteAx control, and then siting it on the form.

WMPRemoteAx

This class is based on the AxHost control, and is what allows an ActiveX COM-based control to exist on a .net WinForms form.

Once created as actually sited on a control (or WinForms Form, in this case), the AttachInterfaces method is called by the .net runtime to connect this host up with whatever COM ActiveX Control it will be hosting. I’ve told this control to host the WindowsMediaPlayer ActiveX control by setting the GUID in the constructor:

MyBase.New("6bf52a52-394a-11d3-b153-00c04f79faa6")

AttachInterfaces connects up the ActiveX control with a clientsite as necessary, but more importantly, it exposes the internal OCX instance by casting it as an instance of WindowsMediaPlayer:

Dim oleObject As IOleObject = TryCast(Me.GetOcx(), IOleObject)

Dim a = TryCast(Me, IOleClientSite)
oleObject.SetClientSite(a)

_RemotePlayer = DirectCast(Me.GetOcx(), WMPLib.WindowsMediaPlayer)

At this point, COM calls several other interfaces that have been implemented by WMPRemoteAx, including:

  • IOleServiceProvider
  • IOleClientSite

    Most of the methods on these interfaces need not actually do anything. They are required for more sophisticated integrations. However, IOleServiceProvider_QueryService does have a very specific purpose.

    Remoting

    Remember that this entire situation is made possible by something WMP calls “Remoting”. Turns out, this callback method is how our control communicates to WMP that we are, in fact, setting up a remoting situation.

    If riid = New Guid("cbb92747-741f-44fe-ab5b-f1a48f3b2a59") Then
        Dim iwmp As IWMPRemoteMediaServices = New RemoteHostInfo()
        Return Marshal.GetComInterfaceForObject(iwmp, GetType(IWMPRemoteMediaServices))
    End If
    When WMP calls this function with the given riid as above, our control has to respond by returning an object of type IWMPRemoteMediaServices (in this case implemented by the RemoteHostInfo object in the project).  That object has a few properties that WMP queries for some basic information, but really, the fact that our control (WMPRemoteAx) has responded by returning an IWMPRemoteMediaServices is the main thing that sets up the remoting connection.

    The OLE Interfaces and Enums

    The rest of WMPRemote.vb is made up of several COM Enums and interfaces that are necessary to bring all this together, but that aren’t readily defined in VB.net. None of that code should normally be altered in any way because it’s actually defining interfaces and values that have already been defined in COM, but that we need defined for use in a .net application. For instance:

    <ComImport(), ComVisible(True), Guid("00000118-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
    Public Interface IOleClientSite

    This is the start of the definition of the IOleClientSite interface.  That interface is a “well defined” interface with a specific GUID (the value of the GUID attribute), and a specific interface type. Changing our .net definition of that interface would likely break everything.

    Where’s the code?

    Right here. This particular version is in VS2010 with .net 4.0 framework, but I don’t imagine there'd be any problems converting back to VS2008, or possibly even VS2005. I suspect you will need at least .net framework 2.0 though, but I haven’t tested this.

    Also, check out a few of my other projects in the Code Garage.

    And finally, let me know what you think!

  • 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.

    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!

    “Unspecified Error” in XAML under WP7

    1
    Filed under Troubleshooting, Windows Phone 7

    Ran into a very strange error today that took me a bit to figure out.

    Essentially, I was templating a standard ol’ Silverlight Button (I wanted it to have a bit of a gradient and a nice rounded border in this instance).

    I was mainly messing around with things, so I was defining the control template inside the button control itself, instead of the more general purpose way of defining a Style.

    <Button Name="btnTest" Content="Caption" Click="btnTest_Click" HorizontalAlignment="Stretch" >
        <Button.Template>
            <ControlTemplate>
                    <Border BorderBrush="White" BorderThickness="3" CornerRadius="10" Margin="{StaticResource PhoneMargin}" >
                        <Border.Background>
                        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                            <GradientStop Color="#FFCDCDE5" Offset="0" />
                            <GradientStop Color="#FF389940" Offset="0.25" />
                            <GradientStop Color="#FF008d00" Offset="0.314" />
                            <GradientStop Color="#FF002200" Offset="1" />
                        </LinearGradientBrush>
                    </Border.Background>
                    <TextBlock Text="{TemplateBinding Content}" HorizontalAlignment="Stretch" TextAlignment="Center" FontFamily="Segoe WP" FontSize="{StaticResource PhoneFontSizeExtraLarge}" Margin="0,6,0,18"/>
                </Border>
            </ControlTemplate>
        </Button.Template>
    </Button>

    Nothing special, but when I ran the program and navigated onto the page containing this button….

    image

    Ugh, Just about the most general error message possible.

    Fortunately, I’d checked things in just a little earlier so I had a record of what had changed, the main thing being that I’d added the use of the TemplateBinding Content feature.

    The idea there is to be able to customize exactly what UI element is used to display the “Content” property of the Button control itself. As you can see, I set the button’s Content to “Caption” and then bind to that in a TextBlock.

    Simple stuff.

    But it crashed.

    If I took out the TemplateBinding reference, everything worked just fine.

    After a lot of headscratching, I eventually realized that in this case, I did NOT provide a TargetType argument to ControlTemplate. I guess I was thinking that sense the ControlTemplate was being defined within an actual control instance, it wasn’t necessary. Lo, I was but mistaken!

    Once I’d specified the TargetType for the ControlTemplate element, everything was back on track.

    <Button.Template>
        <ControlTemplate TargetType="Button">

    It truly is the tiny things in XAML that’ll get you!