Wednesday, October 1, 2008

Code Toaster AppDomain Magic

My personal project for almost 4 years now, Code Toaster, is almost ready to show to the world (again, if you've been following this blog for a while). Code Toaster is an Integrated IDE, and libraries, for rapid creation of developing code generation templates. You can find a lot more information over at http://www.codetoaster.com/, including documentation, code samples, and video tutorials.

But in this post, I'd like to take a look at a few of the more difficult architectural challenges I faced when building Code Toaster, all of which were solved by taking advantage (or working around issues with) the .Net Application Domain (aka AppDomain). But first, some background information.

The AppDomain

A .Net process consists of one or more Application Domains, which are used to provide code isolation, security, and so forth. Code running in one Application Domain cannot directly invoke code running in another Application Domain, however methods on the AppDomain class make it possible to load a .Net assembly into a new AppDomain, create an instance of a class in that assembly, and invoke its properties and methods.

AppDomain's are important to Code Toaster for several reasons. The foremost is that, once an assembly is loaded into an AppDomain using Assembly.Load (or one of its associated methods), the assembly cannot be unloaded from the AppDomain. In several cases it's necessary for Code Toaster to temporarily load an assembly to retrieve type (or other) information contained in an assembly, and then unload the assembly to reclaim the memory used.

Scenarios that benefited from using AppDomains

  1. In one scenario, Code Toaster loads assemblies in the GAC to determine each assembly's target runtime. Loading every assembly in the GAC into memory could consume roughly 25-50 megabytes of RAM : it's nice to be able to reclaim that memory afterwards.
  2. In another scenario: when Code Toaster compiles a template project into a new assembly, that assembly must be loaded (into some AppDomain) so that it can be executed. As the developer iteratively modifies, compiles, modifies, and compiles, each time loading a new assembly, memory usage can slowly add up (each compile loads a new assembly). It's nice to be able to reclaim that memory after each compilation.
  3. Another very important scenario occurs when debugging templates. Attaching the Visual Studio debugger to a template executing in Code Toaster's AppDomain causes the Code Toaster process (codetoaster.exe) to end when the debugger detaches. This is not unique to Code Toaster: the popular CodeSmith code generation tool suffers from the same problem. As I was later to discover, executing templates in their own AppDomain causes debugging to operate as desired (detaching the debugger does not kill the process).

The figure below shows how Visual Studio attaches to a separate AppDomain within the CodeToaster.exe process, whose purpose is solely to contain and execute templates in isolation. This provides several benefits:

  1. Each time a template is recompiled, the "template execution" AppDomain is unloaded and recreated, reclaiming any memory space used by the previously loaded template assembly.
  2. Debugging works as expected (detaching the Visual Studio debugger from a running template will not crash CodeToaster.exe).


This was easier said than done. It's one thing to run some code in another AppDomain. It's quite another to load a Type in AppDomain B into a Property Explorer in AppDomain A, and have it appear and work correctly, with the appropriate adornments for UITypeEditors and so forth (see the figure to the left : the template BDC.Sample is loaded in another AppDomain, but appears correctly in the Template Property Explorer).

In future posts I intend to take a closer look at the code that made all of this come together. For now, I invite you to head on over to http://www.codetoaster.com/ and check out the finest code generation tool ever made!