Following on from my earlier post about Assembly resolution and CLR versions, I have an observation to share about the .NET Just-In-Time compiler and the resolution of Assembly references. It's a fairly simple observation, but it came as a surprise to me.
DISCLAIMER: This is just an observation. I can't find any real concrete specification of the behaviour I'm describing. My supporting documents are this PowerPoint presentation, and this doctoral thesis. If you have better references, or consider this either blindingly obvious or blindingly wrong, please post in the comments.
I used to think that if "Assembly A" references another assembly, "Assembly B", then Assembly B is loaded (or at least validated) when Assembly A is loaded. If Assembly B is missing, Assembly A won't run. Am I right?
It appears I am not right. The rule is that Assembly B will not normally be resolved (ie checked for existence and loaded) until the JIT compiler encounters some code which references a type belonging to that assembly. You could delete Assembly B entirely and Assembly A would work perfectly, providing it didn't touch Assembly B's types in any of the codepaths which you take.
I put together a simple pair of C# projects which demonstrate this behaviour. You can download them here.
The projects build two assemblies, ResolutionTest.exe and MissingAssembly.dll (such creative names!)
MissingAssembly contains one class, MissingClass. ResolutionTest contains a static Program class and another class, MainClass. Here is the code for MainClass:
(This is the executive summary version. The actual source has some other attributes and comments about inlining. I'll get to that in a minute.)
Here's what happens when it runs:
No great surprises. A MainClass object is instantiated. The constructor calls the SubMethod() method, which then creates a new object of type MissingClass from the MissingAssembly.
Now, suppose we delete MissingAssembly:
An exception is raised because it can't find the MissingAssembly object. However, the MainClass() constructor still executes. The FileNotFoundException occurs when the call to SubMethod() is made. The JITter has examined SubMethod() and said "I need a MissingClass which is held in assembly MissingAssembly. I'd better go find that...", and then failed to find it.
Why is this worth knowing?
Because (I think) it means that you can "cheat" when binding assemblies. Previously I'd used late binding via Reflection (like this) when I didn't know if something might or might not be installed on the end user's machine. As far as I can tell, this approach is not wholly necessary - If the "third party" type references are all encapsulated nicely away from the "main" code, it should be possible to just use exception handling to react if the types aren't available at JIT-time.
It's also worth knowing because even if you call Assembly.Load() at runtime without any problems, it doesn't mean all the references have resolved. The loaded assembly could still break when you go to use it.
Some things to remember:
- This isn't something that you can should use every day. You can dig yourself a big hole of shoddy design by doing this. In my case, I just needed a clean way to disable some functionality and fail cleanly when optional assemblies weren't available.
- The assemblies still have to be available when the source is compiled to IL. You can't compile IL which references types or assemblies that do not exist.
- This knowledge can help you optimize your code, by controlling when extra loading and JITting is performed.
- There is a chance that the JITter will inline lightweight method calls. If MainClass.SubMethod() is inlined into the constructor, then it's possible that the constructor will fail. I originally tried to avoid this by attaching MethodImplOptions.NoInlining to SubMethod(). However, in the end I couldn't make it inline the code under any circumstances. It's possible that the JITter notices the unloaded type inside the method, and decides not to inline for that reason.
- It's a bit of a hack, but you could dynamically link to one of several available assemblies at runtime, by going through a series of alternative wrapper classes (each referencing different assemblies) and finding the one which loads successfully. If all of the wrappers implemented a common interface, you could then use the components interchangeably with acceptable performance. A more generalised CLR-supported model of this is described in the doctoral thesis I linked to above (he demonstrates dynamically choosing an alternative to Windows.Forms for GUI display when running Rotor on OS X or Linux.)
- This probably doesn't work if you've used ngen.exe, for obvious reasons.