views:

422

answers:

4

I have some C# code which is using CSharpCodeProvider.CompileAssemblyFromSource to create an assembly in memory. After the assembly has been garbage collected, my application uses more memory than it did before creating the assembly. My code is in a ASP.NET web app, but I've duplicated this problem in a WinForm. I'm using System.GC.GetTotalMemory(true) and Red Gate ANTS Memory Profiler to measure the growth (about 600 bytes with the sample code).

From the searching I've done, it sounds like the leak comes from the creation of new types, not really from any objects that I'm holding references to. Some of the web pages I've found have mentioned something about AppDomain, but I don't understand. Can someone explain what's going on here and how to fix it?

Here's some sample code for leaking:

private void leak()
{
    CSharpCodeProvider codeProvider = new CSharpCodeProvider();
    CompilerParameters parameters = new CompilerParameters();
    parameters.GenerateInMemory = true;
    parameters.GenerateExecutable = false;

    parameters.ReferencedAssemblies.Add("system.dll");

    string sourceCode = "using System;\r\n";
    sourceCode += "public class HelloWord {\r\n";
    sourceCode += "  public HelloWord() {\r\n";
    sourceCode += "    Console.WriteLine(\"hello world\");\r\n";
    sourceCode += "  }\r\n";
    sourceCode += "}\r\n";

    CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, sourceCode);
    Assembly assembly = null;
    if (!results.Errors.HasErrors)
    {
        assembly = results.CompiledAssembly;
    }
}

Update 1: This question may be related: Dynamically loading and unloading a a dll generated using CSharpCodeProvider

Update 2: Trying to understand application domains more, I found this: What is an application domain - an explanation for .Net beginners

Update 3: To clarify, I'm looking for a solution that provides the same functionality as the code above (compiling and providing access to generated code) without leaking memory. It looks like the solution will involve creating a new AppDomain and marshaling.

+5  A: 

Unloading an assembly is not supported. Some information on why can be found here http://blogs.msdn.com/jasonz/archive/2004/05/31/145105.aspx.

Some information on using an AppDomain can be found at http://blogs.msdn.com/suzcook/archive/2003/07/08/57211.aspx.

Jeremy Wilde
Jeremy is correct, there is no way to force .NET to unload an assembly. You're going to have to use an appdomain (which is kind of annoying, but really not all that bad), so you can dump the whole thing when you're done.
David Hay
So, what I'm hearing is you can't unload an assembly, unless you load it in it's own AppDomain and then dump the whole thing. That sounds like a workable solution, so how do I do that to compile and use dynamic code?
Nogwater
+3  A: 

You may also find this blog entry useful: Using AppDomain to Load and Unload Dynamic Assemblies. It provides some example code demonstrating how create an AppDomain, load a (dynamic) assembly into it, do some work in the new AppDomain then unload it.

Dan Blanchard
This seems to be the direction that I'll need to know. Can I use this with CompileAssemblyFromSource? Can I create new AppDomains from a web application?
Nogwater
You could try and call the CompileAssemblyFromSource in a separate AppDomain, and then unload that domain when you're done.
Mikael Svenson
+5  A: 

I think I have a working solution. Thanks to everyone for pointing me in the right direction (I hope).

Assemblies can't be unloaded directly, but AppDomains can. I created a helper library that gets loaded in a new AppDomain and is able to compile a new assembly from code. Here's what the class in that helper library looks like:

public class CompilerRunner : MarshalByRefObject
{
    private Assembly assembly = null;

    public void PrintDomain()
    {
        Console.WriteLine("Object is executing in AppDomain \"{0}\"",
            AppDomain.CurrentDomain.FriendlyName);
    }

    public bool Compile(string code)
    {
        CSharpCodeProvider codeProvider = new CSharpCodeProvider();
        CompilerParameters parameters = new CompilerParameters();
        parameters.GenerateInMemory = true;
        parameters.GenerateExecutable = false;
        parameters.ReferencedAssemblies.Add("system.dll");

        CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, code);
        if (!results.Errors.HasErrors)
        {
            this.assembly = results.CompiledAssembly;
        }
        else
        {
            this.assembly = null;
        }

        return this.assembly != null;
    }

    public object Run(string typeName, string methodName, object[] args)
    {
        Type type = this.assembly.GetType(typeName);
        return type.InvokeMember(methodName, BindingFlags.InvokeMethod, null, assembly, args);
    }

}

It's very basic, but was enough for testing. PrintDomain is there to verify that it does live in my new AppDomain. Compile takes some source code and tries to create an assembly. Run lets us test executing static methods from the given source code.

Here's how I use the helper library:

static void CreateCompileAndRun()
{
    AppDomain domain = AppDomain.CreateDomain("MyDomain");

    CompilerRunner cr = (CompilerRunner)domain.CreateInstanceFromAndUnwrap("CompilerRunner.dll", "AppDomainCompiler.CompilerRunner");            
    cr.Compile("public class Hello { public static string Say() { return \"hello\"; } }");            
    string result = (string)cr.Run("Hello", "Say", new object[0]);

    AppDomain.Unload(domain);
}

It basically creates the domain, creates an instance of my helper class (CompilerRunner), uses it to compile a new assembly (hidden), runs some code from that new assembly, and then unloads the domain to free up memory.

You'll notice the use of MarshalByRefObject and CreateInstanceFromAndUnwrap. These are important for ensuring that the helper library really does live in the new domain.

If anyone notices any problems or has suggestions for improving this, I'd love to hear them.

Nogwater
Something to note about crossing domain boundaries. If you do not derive the marshalled type from MarshalByRefObject, then marshalling will use copy-by-value semantics. This can result in some very slow execution, as communication across the domain boundary will be very chatty. If you can't have your types deriving from MarshalByRefObject, you might want to create a generic proxy object that you instantiate in the secondary AppDomain that DOES derive from MarshalByRefObject, which mediates communication. If you do implement MarshalByRefObject, beware of object lifetime, and implement ...
jrista
an override of *InitializeLifetimeService* such that it keeps the object alive long enough. If you want the object to persist forever, return null from InitializeLifetimeService.
jrista
@jrista Thanks for the pointer. Is the marshalled type in the above example the Hello class (the one accessed with this.assembly.GetType(typeName))?
Nogwater
A: 

Can you wait until .NET 4.0? With it you can use expression trees and the DLR to dynamically generate code without the code-gen memory loss issue.

Another option is to use .NET 3.5 with a dynamic language like IronPython.

EDIT: Expression Tree Example

http://www.infoq.com/articles/expression-compiler

Jonathan Allen
I like your suggestions, but unfortunately they won't work for me on this project (we're not using .NET 4 yet, and can't use IronPython (only C#)). If you don't mind, could you flesh out your answer relating to expression trees. It might help someone else. Can they be used to take something stored in a string and turn it into working code that can be unloaded from memory? Thanks.
Nogwater
I added a link to an article on expression trees.
Jonathan Allen
If you dynamically create code with DLR, it will take up space when you run it. Would you be able to free this in .Net 4 without appdomains?The memory leak in the question is not due to the code-gen, but because of loading an assembly in the same appdomain (so not really a leak) which can't be freed.
Mikael Svenson
If you use "DynamicMethod", the dynamically generated code is associated with an object that can be garbaged collected. http://msdn.microsoft.com/en-us/magazine/cc163491.aspx
Jonathan Allen