1

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


pengu1n
  • 471
  • 6
  • 15
  • 1
    Posting code off-site is not acceptable. The [help/on-topic] guidelines require that questions asking us for help with non-working code **must** include that code here in the question itself, in the form of a [mre]. Please [edit] your post to provide that code if you want help. If you can't do so, your question is not suitable for SO. – Ken White Mar 14 '22 at 22:32
  • I appreciate that. When I tried 'code' and pasted the working minimal example, code formatting was lost (parts outside of
     making the code completely unreadable).
    – pengu1n Mar 14 '22 at 22:35
  • 2
    Don't use `
    `. Use the code formatting buttons on the toolbar. Millions of others have managed to properly post their code here, so your being unable to figure it out doesn't mean you get an exception to the guidelines. Copy and paste your code into the question, select it all, and use the formatting toolbar button or hit Ctrl+K to format it.
    – Ken White Mar 14 '22 at 22:36
  • The code toolbar button did not work either, but the ``` markdown worked. I am not a big SO poster, and it shows. :) – pengu1n Mar 14 '22 at 22:40
  • `new WeakReference(assemblyLoadContext)`, but `assemblyLoadContext` is still in scope and not null. – Jeremy Lakeman Mar 15 '22 at 00:20
  • 1
    For future reference, the easy way to post small code segments is to indent it (4 spaces or 1 tab) in your code editor before copying it here. Markdown is good that way :P – Corey Mar 15 '22 at 00:49
  • @JeremyLakeman - Thanks I made the change to reflect your comment (see the updated source code) but it made no difference. Any further pointers? – pengu1n Mar 15 '22 at 01:37
  • 1
    I don't know enough about the specific guarantees of the runtime, but I would suspect that other local variables are not-null and still in scope. Or at least as far as the GC is concerned. Even if you think they should be out of scope. I'd recommend splitting the method in two, ensuring that the entire stack frame is out of scope. – Jeremy Lakeman Mar 15 '22 at 01:50
  • @JeremyLakeman. Yes, that makes sense, and putting everything above "if (assemblyLoadContextWeakRef != null)" into a separate method releases everything resulting in the successful unloading. Thank you heaps. Perhaps place your comment as answer, so that I can upvote? – pengu1n Mar 15 '22 at 03:20

1 Answers1

1

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.

Jeremy Lakeman
  • 9,515
  • 25
  • 29