Dynamic Plugins: Using the AppDomain Class to Load and Unload Code - ' What' (
Page 2 of 2 )
's Wrong With That?">
As with my original code, this code seems to work; the LoadAndMatch method returns a valid (System.Text.RegularExpressions) Match object. You can examine its properties, call its methods, and so on. However, if you run the sample code, you'll see that the call to LoadAndMatch loads RegexAssembly12_0 into the default AppDomain. Why? Well, Plugin.dll — which is not loaded into the default AppDomain — uses a compiled regular expression, and the returned Match object contains a reference to that compiled regular expression. When the marshaller copies the returned Match object, it finds a value of a type defined in the compiled regex assembly, so it has to load the compiled regex assembly into the default AppDomain before it can deserialize the reference to the compiled regular expression.
To keep code isolated in an AppDomain, you have to pay attention to where each bit of data is defined. So long as every datum is defined in an assembly that's already shared between your application domains, you can copy data from one domain to another. However, deserializing data defined in an unloaded assembly will load that assembly. While this is generally desirable behavior, it's not what you want when you are using an AppDomain to isolate some untrusted code!
Thus, the attached sample code goes on to create a second temporary AppDomain, using the same Shim but loading a different dll. The code in the OtherPlugin.dll region returns a LightWeightMatch instance which contains only copies of a Match object's Success, Index, Length, and Value properties. Since the LightWeightMatch class doesn't refer to any values defined outside of the system libraries, the marshaller can recreate the LightWeightMatch state without having to load any code into the original AppDomain. When you run the demo app, you can see that calling...
LightWeightMatch M =
Proxy.LoadAndLightWeightMatch("OtherPlugin.dll",
"Though the tough cough and hiccough, plough them through");
...and examining the LightWeightMatch doesn't load any new code into the default AppDomain. When the demo executes AppDomain.Unload(Temporary2), both OtherPlugin.dll and all the assemblies it loaded are unloaded.
Finally
It may be obvious now what was wrong with my original code: the Assembly class is serializable but not remotable. The Sandbox.Load call does indeed load an assembly into the new AppDomain, but deserializing the returned Assembly also loads the assembly into the original AppDomain. (As per the SDK documentation, this can cause assembly mis-match issues if your default AppDomain and your new AppDomain have different assembly search paths.) As in the AssemblyMarshalling project, calling GetExportedTypes on the Assembly copy only increases the "pollution" of the original assembly, loading any assemblies that the loaded assembly needs.
Using application domains right isn't all that much harder than using application domains wrong. What is awkward is that the standard deserialization behavior can "leak" code from one AppDomain to another. So, pay attention to what you return from
your "isolated" AppDomain, and be sure to check AppDomain.CurrentDomain.GetAssemblies() before you sign off on your code.
Jon Shemitz is a consultant and an author. You may contact him at www.midnightbeach.com. This article will appear, in different form, in his forthcoming book, .NET 2.0 For Delphi Programmers (Apress, 2005).
Ask Jon about this topic in the DevSource Forum.