Condividi tramite


Running .NET applications in-process using AppDomains

When testing a compiler for a managed language a very convenient end-to-end testing technique is to compile a test program, then run it and verify that it gives the expected output. Not only you cover all parts of the compiler in this manner (parser, binder and emitter), but also you verify that your compiler produces correct IL (otherwise the CLR won’t load and verify your assembly) and your final program has the expected behavior (output has to match).

One downside is that if you have 50,000 test programs, you have to pay the process startup cost and the CLR startup cost 50,000 times. AppDomains to the rescue – they were originally designed as lightweight managed processes, so why not use them as such?

To demonstrate this approach, we’re going to write a .NET program that can run any .NET Console application, intersept its output to the Console, and print it out for demo purposes. First of all, let’s create a C# console application that prints out “Hello World” and save it as Program.exe. Then let’s create a sample “verifier” program that will start and run Program.exe without spinning up a separate process and a separate CLR instance:

 using System;
using System.IO;
using System.Reflection;
 
namespace AppDomainTools
{
    public class Launcher : MarshalByRefObject
    {
        public static void Main(string[] args)
        {
            TextWriter originalConsoleOutput = Console.Out;
            StringWriter writer = new StringWriter();
            Console.SetOut(writer);
 
            AppDomain appDomain = AppDomain.CreateDomain("Loading Domain");
            Launcher program = (Launcher)appDomain.CreateInstanceAndUnwrap(
                typeof(Launcher).Assembly.FullName,
                typeof(Launcher).FullName);
 
            program.Execute();
            AppDomain.Unload(appDomain);
 
            Console.SetOut(originalConsoleOutput);
            string result = writer.ToString();
            Console.WriteLine(result);
        }
 
        /// <summary>
        /// This gets executed in the temporary appdomain.
        /// No error handling to simplify demo.
        /// </summary>
        public void Execute()
        {
            // load the bytes and run Main() using reflection
            // working with bytes is useful if the assembly doesn't come from disk
            byte[] bytes = File.ReadAllBytes("Program.exe");
            Assembly assembly = Assembly.Load(bytes);
            MethodInfo main = assembly.EntryPoint;
            main.Invoke(null, new object[] { null });
        }
    }
}

This approach is especially beneficial if you have to run a lot of small programs – you save a lot on process startup costs and CLR startup costs. Note that I’m representing an assembly as a plain byte array – this allows us to avoid disk I/O if the assembly was just compiled and wasn’t even saved to disk, so you can compile and run it immediately, without ever writing the compiler’s output to disk.

Also note that Launcher inherits from MarshalByRefObject – this allows us to pass data back and forth across AppDomain boundaries. If you like, just create an instance field in the Launcher class and it will get automatically serialized/deserialized when it passes through the DoCallback invocation point.

Comments

  • Anonymous
    February 13, 2011
    Technically speaking you don't even need an AppDomain in this example - you can load and run the assembly in the primary appdomain of the process. The downside is that you'll never be able to unload this assembly. So if you load a lot of them, they will keep piling up until you run out of memory.

  • Anonymous
    February 13, 2011
    The comment has been removed

  • Anonymous
    February 13, 2011
    Thanks Steve. Great point, I corrected the code to use CreateInstanceAndUnwrap vs. calling a constructor.

  • Anonymous
    February 14, 2011
    The comment has been removed

  • Anonymous
    February 14, 2011
    Omer: I guess I could :) Thanks.

  • Anonymous
    February 16, 2011
    Omer: I am doing a similar thing with FxCopCmd.exe. Using AppDomain.ExecuteAssembly pops up the console window, while assembly.EntryPoint.Invoke does not. I use the latter since I want FxCopCmd to run inside Visual Studio with no pop ups. Steve: Also for FxCopCmd.exe, the only way I could make it work was to have a secondary AppDomain with the ApplicationBase property set to the dir of FxCopCmd.exe, as FxCopCmd.exe seems to use that property.