Skip to content

Ramblings: Background and How It Works

Corey Murtagh edited this page Dec 10, 2019 · 1 revision

TL;DR

  • Supporting multiple architectures is a pain.
  • Resource Assembly Loader is fun but awkward.
  • You can't guarantee automatic intialization happens...
  • ...unless you make it happen with something like Fody.

For the less impatient, here's a wee story...

Origins

LAME is one of the original open-source MP3 encoding libraries. It has been around for a long time and it's relatively simple to use while also being quite feature rich. When I was looking for a simple way to do MP3 encoding it was the first thing that jumped out at me.

Initially I grabbed an old copy of libmp3lame.dll and laid down some basic P/Invoke code to use it. This worked pretty well while I was playing so I added some more stuff, made a very basic encoder class and called it good. And since there wasn't much around at the time for MP3 encoding with NAudio I thought "I'll do a nice thing and upload all this so others can use it."

About 5 minutes later it broke.

Of course I hadn't taken into account the fact that the DLL was 32-bit and that it was going to be running in 64-bit mode sometimes - most of the time these days - so I had a bit of a problem. I needed to retarget the correct native DLL, and all the simple ways to do it left me with some borderline unmanagable code. I already had over 100 entry points defined, and I'm selectively lazy.

Handling Architecture Selection

The main problem is that P/Invoke uses attributes to select the native DLL to load. I couldn't adjust those references at runtime, it had to happen at compile. What I needed was an assembly that would compile to x86 and x64 and a way to choose which version to load at runtime.

About that time I was playing with binding assemblies as resources in my project and loading them on demand using AppDomain.AssemblyResolve to hook the assembly resolution chain. I could put the x86 and x64 versions of the DLL wrapper as resources and load whichever one I needed. Shiny new hammer meet nail. Ignore that thread on the side, it's a nail I tell you.

So when your program starts up and uses NAudio.Lame for the first time the Resource Assembly Loader uses the current architecture to figure out which of the two resource-bound libraries it should load. That then determines the name of the native DLL that gets loaded, all hopefully without you knowing that anything is happening.

Initialization Woes

Where this all falls down is when the RAL isn't initialized and you get "Could not load file or assembly" exceptions. I tried a bunch of different ways to hide that initialization so that you could just use the damned thing without having to initialize it by hand. I kept forgetting it myself on pretty much every new project so I knew it was going to be a minor frustration for others.

I found a simple way to do it using a static constructor on the LameMP3FileWriter class. It shouldn't have worked, but it did because the CLR does some slightly strange things during object initialization. It should be loading the libraries as soon as I reference a class that needs them. But something weird happens with static constructors that lets this work.

When you have static members that need to be initialized or you define a static constructor the CLR has to ensure that this all happens before an instance of the class is created, and it can only be done once. To make that happen just right it seems that the CLR treats the static portions of your class as a whole separate class. It loads the dependencies of the static portion and does static initialization before it loads the dependencies of the non-static portion. If the static portion doesn't reference an assembly that the non-static portion does, that assembly isn't loaded until after static initialization completes. This was perfect for my needs. I could initialize the RAL right before it was needed.

Most of the time.

Some times, for no apparent reason, the thing would break. I'd tinker with the code for a bit and it would work again, with no real indication of what I did to break it or how I fixed it.

Them along came .NET Core. I got a few requests for a version that worked with it so I tinkered some more - not having much experience at the time with .NET Standard or .NET Core - and published a pre-release version. It worked for me in a couple of trials so I figured it would work elsewhere. Boy was I wrong.

After a lot of ignoring the problem - and if I'm being honest, hoping someone else would fix it for me - I finally had a real look at what was going on.

It turns out that the CLR's object initialization workflow was a bit hinky and had some undesirable side effects, so the .NET Core team decided to fix the problem. Now we have a much more predictable and stable initialization flow that ensures that all of the required types are loaded before any initializer gets called. It's a good change and I approve... but it breaks my static constructor trick for RAL initialization.

I tried a bunch of other ways to get the damned initializer to run, but in the end I realized that most of the things that worked were essentially manual initialization. Eventually I gave up and exposed the RAL so that .NET Core projects could initialize it, uploaded it as v1.1.0-pre2 and brooded. It wasn't what I wanted. It was an ugly bandaid.

Module Initialization

I dug around a lot and found a reasonable solution to my problem. When the CLR - either .NET Framework or .NET Core - loads an assembly it looks for a global initialization method and executes it before it runs any code in the assembly. Similar to DLLMain in native libraries.

The problem is that C# expressly forbids you from defining global methods, and does not provide any mechanism for doing module initializers at all. Not even an assembly attribute to instruct the compiler to fake it for us. You're just SOL.

Fortunately for me there are some really smart people out there. I aspire to be one of them, but let's face it, I'm not in their league.

The smart people in this case are the guys behind Fody. They've created a set of packages that will operate on your assembly after it is built and make some changes to it. Imagine an assembly line where cars are put together from components and then right at the end of the line the entire car is disassembled, a tiny change is made, then it's all put back together again. That's what Fody does. Seamlessly, just by adding the appropriate NuGet package.

In this case the package is ModuleInit.Fody. It stitches in a global module initializer method that calls a static method that you create.

No more manual initializer calls, no more public RAL. v1.1.0-pre3 is going to handle it all by itself, thank you very much. With Fody I can rip that ugly bandaid off and behold, you can't even see the scar.


Thanks for reading. I hope you have been at least mildly entertained.