How can I prevent CompileAssemblyFromSource from leaking memory?

15.1k views Asked by At

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.

4

There are 4 answers

2
toad On BEST ANSWER

Unloading an assembly is not supported. Some information on why can be found here. Some information on using an AppDomain can be found here.

5
Dan Blanchard On

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.

Edit: fixed link as pointed out in comments below.

4
Jonathan Allen On

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

3
Nogwater On

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.