In-Memory Code Generation With .NET – Compilation

In the previous article from this series, templating engines to generate .NET source code were presented. Once source code is generated, it needs to get compiled into MSIL. This article shows how to dynamically compile .NET code.

CodeDom

The .NET framework provides compilers for C#, VB and JScript. These are the classes Microsoft.CSharp.CSharpCodeProvider, Microsoft.VisualBasic.VBCodeProvider and Microsoft.JScript.JScriptCodeProvider, repectivly. These classes are thin wrappers around the native compilers csc.exe, vbc.exe and jsc.exe. Since theses compilers operate on files on the file system, the wrapper classes need to save the source files into temporary files to be able to pass them to the compilers. Likewise, the compiled assemblies are (temporarily) saved to disk and then loaded by the wrappers. Because of that, true in-memory assembly generation is not possible with these classes. Also, these classes need full-trust permissions.

public static Assembly Compile(string[] sources, bool isDebug, string tempDir, params AssemblyName[] referencedAssemblies)
{
    var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> {{"CompilerVersion", "v4.0"}});

    var assemblyReferences = new[]
        {
            "System.dll",
            "System.Core.dll",
            "mscorlib.dll"
        }
        .Union(from ass in referencedAssemblies
                select new Uri(ass.CodeBase).LocalPath)
        .Distinct(StringComparer.OrdinalIgnoreCase)
        .ToArray();

    var parameters = new CompilerParameters
        {
            GenerateInMemory = true,
            GenerateExecutable = true,
            IncludeDebugInformation = isDebug,
            OutputAssembly = Path.Combine(tempDir, "generatedassembly.dll")
        };

    parameters.ReferencedAssemblies.AddRange(assemblyReferences);

    if (tempDir.Length > 0) parameters.TempFiles = new TempFileCollection(tempDir);
    parameters.TempFiles.KeepFiles = isDebug;

    var compilerResults = codeProvider.CompileAssemblyFromSource(parameters, sources);

    if (compilerResults.Errors.HasErrors)
    {
        var message = string.Join("\r\n", compilerResults.Errors);
        throw new ApplicationException(message);
    }

    return compilerResults.CompiledAssembly;
}

The compiler option can be set with the class CompilerParameters. Among others, there ist a property called GenerateInMemory. If this is set to true, the compiled assemblies get deleted after being loaded into memory. Using this setting, an in-memory generation can be mimiked. On the other hand, assemblies that are loaded into memory but deleted from disk cannot be referenced by newer compilations. So if for instance different assenmblies must be generated that reference each other, the property GenerateInMemory must be set to false.

Roslyn

“Project Roslyn” is Microsoft’s new approach towards purely managed C#and VB compilers. In contrast to CodeDom, Roslyn does generate assemblies in memory without having to read or wrtie files to disk. Currently being in CTP status, not all functionality is implemented, yet. For example, it cannot reference assemblies that are not located on the files sysetm. So currently, if multiple assemblies that reference each other should be generated, true in-memory generation is not possible, either.

public static Assembly Compile(string[] sources, bool isDebug, string tempDir, params AssemblyName[] referencedAssemblies)
{
    var assemblyFileName = tempDir + "gen" + Guid.NewGuid().ToString().Replace("-", "") + ".dll";
    var assemblyPath = Path.GetFullPath(assemblyFileName);

    var compilation = Compilation.Create(assemblyFileName,
                                            new CompilationOptions(OutputKind.DynamicallyLinkedLibrary))
                                    .AddSyntaxTrees(from source in sources
                                                    select SyntaxTree.ParseCompilationUnit(source))
                                    .AddReferences(from ass in referencedAssemblies
                                                select new AssemblyFileReference(new Uri(ass.CodeBase).LocalPath))
                                              //select new AssemblyObjectReference(Assembly.Load(ass)))
                                    .AddReferences(from ass in new[]
                                                {
                                                    "System",
                                                    "System.Core",
                                                    "mscorlib"
                                                }
                                                select new AssemblyNameReference(ass));

    EmitResult emitResult;

    //using (var stream = new MemoryStream())
    using (var stream = new FileStream(assemblyPath, FileMode.Create, FileAccess.Write))
    {
        emitResult = compilation.Emit(stream);
    }

    if (!emitResult.Success)
    {
        var message = string.Join("\r\n", emitResult.Diagnostics);
        throw new ApplicationException(message);
    }

    return Assembly.LoadFile(assemblyPath);
}

In the code above, there are two commented-out lines. The first on uses the type AssemblyObjectReference which is currently not implemented. This type would allow referencing assemblies from memory. The second commented-out line regards where the complied asssembly is written to. Once in-memory references are implemented, complied assemblies can be written into memory and be referenced by other assemblies to be generated.

EDIT: For a more current example of the Roslyn API shown above, see this post. Note that this post does not tackle the assembly reference issue.

Series Navigation<< In-Memory Code Generation With .NET – Templating EnginesIn-Memory Code Generation With .NET – Emit & Linq >>

Freelance full-stack .NET and JS developer and architect. Located near Cologne, Germany.

Leave a Reply

Your email address will not be published. Required fields are marked *