pengu1n
pengu1n

Reputation: 503

Dynamic Compilation in .NET Core 6

I have been experimenting with the C# dynamic compilation as described in Laurent's excellent blog: https://laurentkempe.com/2019/02/18/dynamically-compile-and-run-code-using-dotNET-Core-3.0/ (Merci Laurent!!)

I copied and pasted the code into a single file and all in the Main method to understand the control flow better.

I have however been unable to work out why the unloading of the DLL consistently fails (i.e. the WeakReference is still live). Laurent's code (as published on GitHub) does unload the DLL while my copy-pasted monolithic code does not.

Could someone help me spot where I have gone wrong?


using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

namespace CoreCompile
{
   public class CompilerTest
   {
      public static void Main(string[] args)
      {
         Console.WriteLine("Hello, World!");

         string sourcePath = args.Length > 0 ? args[0] : @"D:\DynamicRun\Sources\DynamicProgram.cs";
         string sourceCode = File.ReadAllText(sourcePath);
         string assemblyPath = Path.ChangeExtension(Path.GetFileNameWithoutExtension(sourcePath), "DLL");

         var codeString = SourceText.From(sourceCode);
         var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp10);

         var parsedSyntaxTree = SyntaxFactory.ParseSyntaxTree(codeString, options);

         var references = new List<MetadataReference>
            {
                MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
                MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
            };

         Assembly.GetEntryAssembly()?.GetReferencedAssemblies().ToList()
             .ForEach(a => references.Add(MetadataReference.CreateFromFile(Assembly.Load(a).Location)));

         var csCompilation = CSharpCompilation.Create(assemblyPath,
             new[] { parsedSyntaxTree },
             references: references,
             options: new CSharpCompilationOptions(OutputKind.ConsoleApplication,
                 optimizationLevel: OptimizationLevel.Release,
                 assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default));

         WeakReference assemblyLoadContextWeakRef = null;
         using (var peStream = new MemoryStream())
         {
            var result = csCompilation.Emit(peStream);

            if (result.Success)
            {
               Console.WriteLine("Compilation done without any error.");
               peStream.Seek(0, SeekOrigin.Begin);
               var compiledAssembly = peStream.ToArray();
               string[] arguments = new[] { "France" };

               using (var asm = new MemoryStream(compiledAssembly))
               {
                  var assemblyLoadContext = new SimpleUnloadableAssemblyLoadContext();
                  var assembly = assemblyLoadContext.LoadFromStream(asm);
                  var entry = assembly.EntryPoint;
                  _ = entry != null && entry.GetParameters().Length > 0
                      ? entry.Invoke(null, new object[] { arguments })
                      : entry.Invoke(null, null);

                  assemblyLoadContext.Unload();
                  assemblyLoadContextWeakRef = new WeakReference(assemblyLoadContext);
               } // using
            }
            else
            {
               Console.WriteLine("Compilation done with error.");
               var failures = result.Diagnostics.Where(diagnostic => diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error);
               foreach (var diagnostic in failures)
               {
                  Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
               }
            }
         } // using

         if (assemblyLoadContextWeakRef != null)
         {
            for (var i = 0; i < 8 && assemblyLoadContextWeakRef.IsAlive; i++)
            {
               GC.Collect();
               GC.WaitForPendingFinalizers();
            }
            Console.WriteLine(assemblyLoadContextWeakRef.IsAlive ? "Unloading failed!" : "Unloading success!");
         }

      } // Main
   } // class

   internal class SimpleUnloadableAssemblyLoadContext : AssemblyLoadContext
   {
      public SimpleUnloadableAssemblyLoadContext()
          : base(true)
      {
      }

      protected override Assembly Load(AssemblyName assemblyName)
      {
         return null;
      }
   }


} // namespace


Upvotes: 1

Views: 4149

Answers (1)

Jeremy Lakeman
Jeremy Lakeman

Reputation: 11163

Forcing objects to be garbage collected is something of a dark art. You need to ensure that the garbage collector will not be able to locate any variables from your Main method, otherwise the objects will be kept alive.

For example, a debug build and a release build will behave differently, as release builds will throw away variables as soon as they are no longer required.

From your example, your local variable assemblyLoadContext will still be in scope, particularly in a debug build. As you might place a break point at the end of the method in order to examine any local variable.

Perhaps the simplest thing you could do is move most of your code to a separate method. Once that method returns, all local variables should be out of scope and undetectable by the garbage collector.

Upvotes: 1

Related Questions