Customizing the .NET Common Object Runtime - Part 2
Building Custom AppDomain Managers
Dan Buskirk
June 11, 2007
Overview of AppDomains
In
the first part of this series, we created a small custom host for the
.NET CLR. Our goal is to include custom loading of AppDomains as a part of the
host's function, but first we must take a little time to review the
fundamentals of the AppDomain itself.
I doubt that anyone would like to return to the days of Windows 3.1, when any
errant application would crash the entire operating environment. In 32-bit
Windows, applications run in their own separate process, and the operating
system prevents an action in one process from inadvertently stomping on
something in another. An AppDomain is very much an extension of this notion. As
several processes run within the operating system, the CLR controls one or more
AppDomains within a single process. An AppDomain is the unit of isolation with
the CLR just as the process is the unit of isolation within the OS. The .NET
runtime takes this further, however. AppDomains are not just about memory
access, they are also the unit of isolation for security as well. The most
common application of multiple AppDomains within a single process is the
ASP.NET engine. Individual web applications run in separate AppDomains. As you
know, the web application is the unit of security within ASP.NET; this is easy
for the CLR to enforce via AppDomains. Our goal is similar; we wish to isolate
unpredicatble add-ins from each other and from the application itself.
Another very valuable characteristic of AppDomains is that they can be unloaded.
Once loaded, the CLR itself can never be unloaded, except by shutting down the
process itself. Neither can individual assemblies be unloaded. Therefore, if we
are loading a large assembly and wish to unload it to recover memory, the only
alternative is to load that assembly into a separate AppDomain. Once we are
finished with the code, the AppDomain, and the managed code within it, can be
unloaded.
When the default host is asked to load managed .NET code into the CLR, an
AppDomain, not surprisingly called the default AppDomain, is created.
The host creates an AppDomainManager, which loads security information, which
then loads and runs the managed code. Additional AppDomains can be created
entirely within managed code. Each one gets an instance of the AppDomainManager
which then loads more security information and more managed code.
Before we continue the development of our custom host, we will build two small
applications to illustrate the creation of AppDomains in managed code. The
first will simply load and run some user code in a new AppDomain. The second
will create a managed AppDomainManager to provide more fine-grained control
over this process.
Creating an Application that Compiles and Runs User C# Code
Much of the available sample code that illustrates th creation of new AppDomains
illustrates how to load code from an assembly file into that new AppDomain.
This, of course, is the practical task most developers will need to perform.
For illustrative purposes, and perhaps a little fun, we will take a slightly
different tack. Our application will accept a C# script from the user, compile
it, and then run it in a new AppDomain.
Using CodeDom to Compile C# Code at Runtime
The .NET Framework has always provided mechanisms for compiling VB and C# code
at runtime. Indeed, code written for this purpose in versions 1.0 and 1.1 will
still continue to work in .NET 2.0. However, Microsoft is in the process of
redesigning some of the key classes in the CodeDom namespace, so we will make a
short detour to review them.
In earlier versions, to get a compiler you would declare a variable of type ICodeCompiler,
instantiate an object of VBCodeProvider or CSharpCodeProvider
class, and then call the CreateCompiler method of the
appropriate object. The process is more direct with .NET 2.0. We simply call
the new CompileAssemblyFromSource method of the CSharpCodeProvider
class and we are all set. In the example code which follows, we will accept a
string as input, then wrap that string in a class and function declaration.
After this "on-the-fly" class is compiled, we will load its assembly and
execute our method. The net effect, from the user's perspective, will be the
execution of his or her code snippet immediately. If you are using the Visual
Studio, create a Class Libbrary project called "AppDomainTestLibrary" and
define the following class:
1 using
System;
2 using
System.Text;
3 using
System.Windows.Forms;
4 using
System.Collections;
5 using
System.Collections.Generic;
6 using
Microsoft.CSharp;
7 using
System.CodeDom.Compiler;
8 using
System.Reflection;
9
10
11 public
class CompileInvoke
12 {
13
public void RunIt(string
CodeSnippet)
14
{
15
16
// (1)
17
CSharpCodeProvider cscp = new
CSharpCodeProvider();
18
CompilerParameters cp = new
CompilerParameters();
19
20
// (2) We are goind to let users
create message boxes if they like
21
cp.ReferencedAssemblies.Add("System.Windows.Forms.dll");
22
cp.GenerateInMemory = true;
// false means create an output file
23
24
// (3) We can save intermediate files
if we like
25
//cp.TempFiles = new
TempFileCollection(".", true);
26
27
28
//List<string> al = new
List<string>();
29
30
// (4) our source code will be in one
big string
31
StringBuilder sb =
new StringBuilder();
32
33
sb.Append("using System;\n");
34
sb.Append("using
System.Windows.Forms;\n");
35
sb.Append("public class hola\n");
36
sb.Append("{\n");
37
sb.Append("public void f()\n");
38
sb.Append("{\n");
39
sb.Append(CodeSnippet);
40
sb.Append("\n}\n}\n");
41
42
// one array element is like one
source file
43
string[] theCode = {
sb.ToString() };
44
45
46
// (5) Where the action is
47
CompilerResults cr = cscp.CompileAssemblyFromSource(cp,
theCode);
48
if (cr.Errors.Count == 0)
49
{
50
Assembly
a = cr.CompiledAssembly;
51
Type HolaType;
52
HolaType = a.GetType("hola");
53
Object it;
54
it = Activator.CreateInstance(HolaType);
55
MethodInfo
mi;
56
mi = HolaType.GetMethod("f");
57
try
58
{
59
mi.Invoke(it,
null);
60
// (6)The following lines report where the code is running, but otherwise add
nothing
61
Console.ForegroundColor
= ConsoleColor.Blue;
62
Console.WriteLine(AppDomain.CurrentDomain.FriendlyName);
63
Console.ForegroundColor
= ConsoleColor.Gray;
64
65
}
66
catch (Exception
Ex)
67
{
68
MessageBox.Show(Ex.Message);
69
}
70
}
71
else
72
{
73
Console.ForegroundColor
= ConsoleColor.Red;
74
foreach (CompilerError
ce in cr.Errors)
75
{
76
Console.WriteLine(ce.ErrorText);
77
}
78
Console.ForegroundColor
= ConsoleColor.Gray;
79
}
80
}//RunIt
81
82
83
84 }//CompileInvoke
Class
85
86
87
88
Figure 1.
In section (1), we create our two fundamental objects, the CSharpCodeProvider
which will supply the compiler and the CompilerParameters object which we will
use to control the conditions of compilation. In section (2) we set a parameter
which adds System.Windows.Forms.dll as a reference. Obviously we can add
whatever we need. For example, we could add a reference to the application
itself, and allow users to control the program with .NET scripts. In this case,
we tell the set the GenerateInMemory parameter to true, indicating
that we do not wish to create an actual exe or dll file.
In section (4), we use a StringBuilder to create the actual source code for out
class. When the code string is finished, we assign it to the first element of a
string array called theCode. The compiler expects a string array as an
argument, even if we are only compiling a single string as we are here. A
multiple element string array would be the equivalent of compiling a project
with multiple C# source files. Each element of the array must therefore have
the appropriate namespace declarations etc.
In section (5) we actually do the work of compilation. We obtain a CompilationResults
object from the CompilaAssemblyFromSource method, and then
assign the CompiledAssembly reference to a variable of type
assembly. This is a full-fledged assembly and contains our new class and
method. However, the code which will run this new assembly does not know about
the class or method, so we must do some more work. We call GetType to obtain
the type of our "hola" class and we then use Activator.CreateInstance
to instantiate it. A MethodInfo object is then used to invoke
the "f" mthod which was defined in out source code. In this example we are
keeping things simple, but in practice the function we are invoking might
require parameters. In this case, the second argument of Invoke
would be an Object array holding the parameters. After Invoke
executes (and we're certain not to get an error, aren;t we?) a few lines of
code print to the console the name of the AppDomain in which this code is
executing. At present, this code does not provide us with any useful
information. Later, if we run this code from a different AppDomain, this code
will provide confirmation of that fact.
We will need a simple user interface to test this code. If you like, you can
create a console application which loads a Windows Form with a RichTextBox
control serving as a simple source code editor. Or, if you prefer, you can use
the form found in the collector01.zip file. The form must contain a button or a
menu item which calls our RunIt method and passes the contents of the
RichTextBox containing the C# source code. The code might look something like
this:
12
internal static
void RunItHere(string
CodeSnippet)
13
{
14
(new CompileInvoke()).RunIt(CodeSnippet);
15
}
Figure 2.
Loading .NET Assemblies into a New AppDomain
Loading .NET assemblies into a new AppDomain is more difficult to understand
than it is to actually program. The conceptual difficulty lies in the fact that
the code which gets the ball rolling runs in the first AppDomain, so the code
loaded into the new AppDomain will not be directly accessible. The alternative
method to RunItHere (figure 2) is RunItInNewAppDomain which we will design to
create a new AppDomain and then load our CompileInvoke object into that new
AppDomain. We will then have to find some way of calling the RunIt method,
which as we have said cannot be called directly. Here is our alternative to the
code in Figure 2:
17
internal static
void RunItInNewAppDomain(string
CodeSnippet)
18
{
19
// (1) Create the new AppDomain
20
AppDomain AD =
AppDomain.CreateDomain("HeyThereBuddy");
// It's a friendly name
21
// (2) load our class into the
AppDomain, instantiate CompileInvoke,
22
// and get a reference to a
proxy for the CompileInvoke object
23
CompileInvoke mbro = (CompileInvoke)AD.CreateInstanceAndUnwrap("AppDomainTestLibrary",
"CompileInvoke");
24
mbro.RunIt(CodeSnippet);
25
}
Figure 3.
Section (1) creates the AppDomain with a friendly name of our choosing (as
opposed to a full name). Section (2) does quite a bit. It tells the new
AppDomain to load AppDomainTestLibrary.dll and to instantiate a CompileInvoke
object. We will need to cast the return of CreateInstanceAndUnwrap
as the correct type. But the return value is not actually a reference to our
new object. Since the code in the new AppDomain cannot be directly accessed,
what we get is actually a reference to a proxy which will marshal any calls to
the code in the new AppDomain. But how did the proxy get created? The answer is
"It didn't!". If we tried to run this code, we would get a runtime
error. In order to be accessed across AppDomains, a class must either be serializable
or it must inherit from MarshalByRefObject. My CompileInvoke
method is neither, but you probably already noticed that. Let's modify the
CompileInvoke class:
10 //
must either be serializable or inherit from MarshalByRefObject
11 public
class CompileInvoke
: MarshalByRefObject
12 {
13
public void RunIt(string
CodeSnippet)
14
{...
Figure 4.
Now we should be able to test our application.
Of course, up till now we have been letting the AppDomain management tasks to
the CLR itself. Now we will turn our attention to designing our own AppDomain
manager. In .NET versions prior to 2.0, this usually involved writing unmanaged
code in our host. Now, however, .NET 2.0 has provided a toolset to accomplish
this task entirely in managed code. Appropriately enough, the new class
provided for this purpose is called the AppDomainManager class.
Next:
Page 2
|