The other day, Sergey Vlasov asked me how to deal with the absence of the Managed DirectX assemblies on the target computer. It’s a great question, because – as more and more people work with Managed DirectX – applications are going to have to be prepared to run on machines that may have older versions of DirectX installed. Since Managed DirectX is new with DirectX 9.0 – and since it’s possible to install even DirectX 9 without the managed pieces – the absence of, say, Microsoft.DirectX.Direct3D.dll is a real possibility.
And this isn’t just a problem for DirectX assemblies. In general, it should be possible for applications to gracefully degrade when faced with a missing component. There are many definitions of “graceful degradation” – whether that’s throwing up a dialog box, emailing tech support automatically or downloading and installing the missing pieces depends on the application. But regardless, you have to know how to recover when your application tries to load an assembly and fails.
There are generally two ways to load an assembly: explicitly and implicitly. An explicit load happens when you call Assembly.Load or Assembly.LoadFrom, passing in the name of an assembly you’d like to load. Trapping an error in this case is fairly straightforward, since the call to Assembly.Load orLoadFrom will throw an exception if the assembly you’re looking for can’t be loaded. The only tricky part is passing the right name to Assembly.Load – you have to include the version and public key token if you want to load something that’s in the GAC. That’s a discussion for another day, though: let’s stick to dealing with load failure for the moment.
But you probably hardly ever do an explicit load. Most loads are implicit loads – they just sort of happen.
Let’s start with a definition: an implicit load is when an assembly is loaded automatically because of a dependency. For example, if you add a reference to System.Xml.dll because your program uses the XPath functionality located there, this assembly will automatically be loaded by the runtime – you don’t have to call Assembly.Load or Assembly.LoadFrom. Which is nice, but sort of begs the question: what if the assembly I’m looking for isn’t there?
As it turns out, what happens is exactly the same thing: an exception is generated at the point where the runtime tries to load the assembly, but fails for whatever reason. And we can still catch that exception and deal with it. The trick is knowing when the load occurs.
The CLR tends to do things in a lazy manner. For example, you probably know that the JIT compiler compiles code just before a method is called for the first time (hence Just In Time: JIT). If a function is never called, the JIT compiler never compiles that method. Assembly loading works like this, too: assemblies that your program requires are never loaded until they are needed for the first time. Then and only then does the runtime go off and try to find the thing.
As it turns out, the first time that we need an assembly is when a method that makes use of a type in that assembly is JIT compiled. For example, the CLR won’t load System.Xml.dll until your code calls a method that makes use of something in System.Xml.dll, like the XPathExpression class. It’s the JIT compilation of your method that triggers the load of System.Xml.dll.
This makes perfect sense, when you think about it. In order for the CLR to successfully compile a method that contains a call to (for example) “new XmlDocument()”, it has to know what an XmlDocument object looks like. In order to know that, it has to load System.Xml.dll, where the XmlDocument class lives. So the JIT triggers the load.
What this means for us is that if we want to deal with the failure of the CLR to implicitly load an assembly, our try/catch block had better be outside the method that actually uses stuff from that assembly. This looks a little weird, but again, makes sense when you think about why:
using System; using System.Reflection ; public class App { public static void Main () { try { Initialize(); } catch (Exception e) { Console.WriteLine ("I caught an exception: {0}", e.ToString ()); } } public static void Initialize() { ClassFromAnotherAssembly c = new ClassFromAnotherAssembly (); c.Foo (); } }
Notice how we have the catch block outside the method that’s triggering the load. This is because the Initialize method will never actually be called if the other assembly can’t be loaded: the JIT compiler encounters an error while trying to load the assembly containing ClassFromAnotherAssembly, and so the Initialize method can never even be compiled, let alone called.
There’s another trick we can play. If we happen to have special knowledge of where an assembly is, even if the CLR fails to find it in the places it usually looks, there’s a hook we can use to take over finding the assembly ourselves. That hook is the AssemblyResolve event of the System.AppDomain class. This event will be called whenever an assembly load fails, giving us a chance to find and load the assembly ourselves. How you do that is up to you, but one thing to note is that Assembly.LoadFrom has an overload that takes a byte array...so you could do something like store the assembly in your application as a resource and load it from there.
Here’s what the code looks like to add a handler to the AssemblyResolve event – everything else is still the same:
public static void Main () { AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler ( AssemblyLoadFailedHandler ); Initialize(); } private static Assembly AssemblyLoadFailedHandler ( object sender, ResolveEventArgs e) { if ( e.Name == " SomeSpecialCasedAssembly ") { // Do some magic if it's an assembly we know about return DownloadAssemblyFromInternet (); } else { // If we return null, it just throws the exception that // would normally be thrown when an assembly fails // to load return null ; } }
Of course, when you do this, you are bypassing all the versioning magic that the CLR provides for you, so think hard before deciding you really want to take this route. It might be better to simply catch the error and throw a dialog box up to the user than to try to get this clever.