Dynamic Plugins: Using the AppDomain Class to Load and Unload Code (
Page 1 of 2 )
Learn the right way to play in the sandbox. Jon Shemitz shows you how to load and call code in a temporary
AppDomain, without affecting the original
AppDomain. You'll also learn about the
marshalling mechanism, which cop
I was recently asked to do some plugin license tests in an AppDomain, so that unlicensed code could be unloaded and not available (and not incur royalties). So, I wrote code like this:
AppDomain Sandbox = AppDomain.CreateDomain("Sandbox");
try
{
Assembly Suspect = Sandbox.Load(LibraryName);
// test the library
}
finally
{
AppDomain.Unload(Sandbox);
}
It took me maybe four hours from F1 to tested, and I thought I was a stud.
But there's a lesson here about doing testing correctly. What I tested was that I could do what I expected with the Suspect assembly: call GetExportedTypes to find the types that were assignment-compatible with our plugin interface (i.e. that supported the interface) and then running license tests on them. I didn't test that the assemblies I'd loaded were actually unloaded.
Fortunately, I caught this long before QA, so no harm was done, but calling code in another application domain and getting back results is a bit more complicated than the above. Getting it right helped me understand the AppDomain.Load and MarshalByRefObject documentation a bit better, too.
In this article, I show how to load and call code in a temporary AppDomain, without affecting the original AppDomain. This involves a shared assembly, somewhat like the shared assembly in the standard plugin architecture. I also explain a bit about the marshalling mechanism, which copies data between application domains, and briefly discuss the differences between serialized copies and remote proxies.
Shared Assembly
In the standard plugin architecture, you load plugins into the default AppDomain, where they remain until your application terminates. You have a shared library, with interfaces that define both any services that the host offers the plugins, and the handshaking that a plugin must do to be hosted. The shared library may also include standard implementations of various base types, utility code, and the like. As in Figure 1, both the application and all the plugins have a compile time reference to (and, hence, a run-time dependence on) the same library assembly.
Figure 1: The standard plugin architecture
Your plugin assemblies provide classes that support the plugin interface(s) defined in the shared library. When you create an instance of a class defined in a plugin assembly, you typically cast it to a plugin interface from the shared library, so that subsequent calls are early bound, through the interface method table. You share types, and can pass values back and forth between assemblies without translation.
Making calls between application domains also depends on a small, shared assembly, but the calls are much less direct. As in Figure 2, your application has a reference to a small shim assembly that exports a remotable type, one that descends from System.MarshalByRefObject. After you create an AppDomain as above...
AppDomain Sandbox = AppDomain.CreateDomain("Sandbox");
...you use the new domain's CreateInstanceAndUnwrap method to load the shim assembly into the new domain; create an instance of an object (defined in the shim assembly) in the new domain; and then create a proxy object in the original domain. When you call a method of a proxy object in the original domain, any arguments are marshaled to the base object's domain and any result is marshaled back.
Figure 2: The AppDomain architecture
Data can only be marshaled across AppDomain boundaries if it is either remotable or serializable. Remotable data is an instance of a class that descends from System.MarshalByRefObject. As with the main shim object, it exists "for real" in the secondary application domain, and as a proxy in the original application domain. Again, when you call a proxy method, any arguments are marshaled to the real object's domain and any result is marshaled back.
Serializable data is either a system primitive type (number, boolean, or character) or a data structure (a class or a struct) that is marked with the [Serializable] attribute and is defined in an assembly shared by both application domains. (This includes strings and arrays.)
Serializable data is serialized in one
AppDomain and deserialized in another. This produces an independent copy, not a proxy; when you call a method on a copied object, your call operates on the copied state, and does not cross application domain boundaries.
(Note that .NET 1.1 does not check that data is [Serializable] at compile-time. You can compile code that tries to copy non-serializable code, but you will get an exception when it runs.)
An Example
Stripping the attached sample code to its bare bones:
AppDomain Temporary = AppDomain.CreateDomain("Temporary");
try
{
Gateway Proxy =
(Gateway) Temporary.CreateInstanceAndUnwrap("Shim", "Shim.Gateway");
Match M = Proxy.LoadAndMatch("Plugin.dll",
"Though the tough cough and hiccough, plough them through");
}
finally
{
AppDomain.Unload(Temporary);
}
I create a new AppDomain, then call the CreateInstanceAndUnwrap method from the original AppDomain. This loads the Shim assembly into the Temporary application domain, and then creates the Gateway class via the namespace-qualified name Shim.Gateway. The next statement calls the Gateway proxy in the original AppDomain. The proxy call marshals the two parameters across the application domain boundary; calls the "real" method in the new AppDomain; and then marshals the result back to the original AppDomain.