Running .NET applications in-process using AppDomains

Running .NET applications in-process using AppDomains

  • Comments 6

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(nullnew 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.

  • 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.

  • The code you've posted above doesn't really use the secondary AppDomain for anything.  You're using DoCallback to invoke a delegate that's bound to a MarshalByRefObject, so the delegate is going to invoke in the AppDomain the MBRO was created in.

    The flow looks like this: Default Domain -> DoCallback -> Second domain -> Transparent Proxy[program].Execute() -> Default domain -> Execute()

    You probably wanted to use AppDomain.CreateInstanceAndUnwrap to get an instance of Launcher in the secondary appdomain, then call Execute on it directly.

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

  • Couldn't you use one of the AppDomain.ExecuteAssembly family of methods instead of your hand-written Execute method?

  • Omer: I guess I could :) Thanks.

  • 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.

Page 1 of 1 (6 items)
Leave a Comment
  • Please add 3 and 8 and type the answer here:
  • Post