Why does Assembly.Load seem to not affect the current thread when resolving references (not through reflection)?
Asked Answered
A

1

3

I apologize in advance if the title doesn't make sense. I'm very new to appdomains and assembly loading and don't really know how to state what I'm trying to ask.

I have been fiddling around with loading embedded DLLs into an application during runtime and I can't seem to figure out why it works one way but not the other. It seems like if you try to load DLLs (from a byte array) into the current appdomain, any objects/threads created after that will be able to resolve references against the newly loaded library, however objects in the original context will not resolve against the newly loaded library.

Here is my example library that will be loaded from as an embedded resource during runtime (requires a reference to the WPF PresentationFramework.dll for MessageBox):

namespace LoaderLibrary
{
    public class LoaderLibrary
    {
        public static void Test()
        {
            System.Windows.MessageBox.Show("success");
        }
    }
}

In my console app .csproj file I manually add the following embedded resource for that project and include a project reference to LoaderLibrary as well:

  <ItemGroup>
    <EmbeddedResource Include="..\LoaderLibrary\bin\$(Configuration)\LoaderLibrary.dll">
      <LogicalName>EmbeddedResource.LoaderLibrary.dll</LogicalName>
    </EmbeddedResource>
  </ItemGroup>

Here is the code for my console app that loads that library (requires a project reference to the LoaderLibrary csproj) ALSO: Need to set CopyLocal to false for LoaderLibrary reference:

namespace AssemblyLoaderTest
{
    class Program
    {
        static void Main(string[] args)
        {
            EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
            System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };

            var app = new TestApp();
        }
    }

    public class TestApp
    {
        public TestApp()
        {
            LoaderLibrary.LoaderLibrary.Test();            
        }
    }

    public class EmbeddedAssembly
    {
        static System.Collections.Generic.Dictionary<string, System.Reflection.Assembly> assemblies = new System.Collections.Generic.Dictionary<string, System.Reflection.Assembly>();
        public static void Load(string embeddedResource)
        {
            using (System.IO.Stream stm = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(embeddedResource))
            using (var mstream = new System.IO.MemoryStream())
            {
                stm.CopyTo(mstream);
                var assembly = System.Reflection.Assembly.Load(mstream.ToArray());
                assemblies.Add(assembly.FullName, assembly);
                return;
            }
        }

        public static System.Reflection.Assembly Get(string assemblyFullName)
        {
            return (assemblies.Count == 0 || !assemblies.ContainsKey(assemblyFullName)) ? null : assemblies[assemblyFullName];
        }
    }
}

This code is able to successfully load and execute the LoaderLibrary.LoaderLibrary.Test() function.

My question is why does the following not work?

static void Main(string[] args)
{
    EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
    System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };

    LoaderLibrary.LoaderLibrary.Test(); // very unhappy line of code
}

This also doesn't work:

static void Main(string[] args)
{
    EmbeddedAssembly.Load("EmbeddedResource.LoaderLibrary.dll");
    System.AppDomain.CurrentDomain.AssemblyResolve += (s, a) => { return EmbeddedAssembly.Get(a.Name); };

    var app = new TestApp();
    LoaderLibrary.LoaderLibrary.Test(); // very unhappy line of code
}
Afore answered 18/2, 2016 at 11:56 Comment(3)
Assemblies are loaded by the just-in-time compiler. It needs to convert your Main() method to machine code before it can start running. This of course happens before it runs so AssemblyResolve cannot do anything yet to help find the assembly. Gets worse in the Release build, the optimizer looks ahead for opportunities to inline methods. You need another method, say RealMain(), move the Test() call into that method. And give it the [MethodImpl(MethodImplOptions.Nolinling)] attribute to slow down the optimizer.Seger
So basically it takes whatever function is the entry point and converts it to IL before trying to execute any code? And since the assembly technically hasn't been added to the AppDomain it results in a failed dependency resolution?Afore
Ok I'm pretty sure I understand it now. Basically during the JIT compilation of the entry point it fails to resolve the dependency since it obviously can't execute any code before compiling it, resulting in the error I was seeing. Moving the calls to the dynamically loaded assemblies into a different function scope and marking the methods that call them with the [MethodImpl(MethodImplOptions.Nolinling)] helps to prevent the optimizer from moving the code that references the dynamically loaded assemblies into a scope that would get compiled prior to the loading of the assembly.Afore
A
3

Big thanks to Hans Passant and dthorpe for explaining what was happening.

I found dthorpe's great explanation on how the JIT compiler works here: C# JIT compiling and .NET

To quote dthorpe here:

Yes, JIT'ing IL code involves translating the IL into native machine instructions.

Yes, the .NET runtime interacts with the JIT'ed native machine code, in the sense that the runtime owns the memory blocks occupied by the native machine code, the runtime calls into the native machine code, etc.

You are correct that the .NET runtime does not interpret the IL code in your assemblies.

What happens is when execution reaches a function or code block (like, an else clause of an if block) that has not yet been JIT compiled into native machine code, the JIT'r is invoked to compile that block of IL into native machine code. When that's done, program execution enters the freshly emitted machine code to execute it's program logic. If while executing that native machine code execution reaches a function call to a function that has not yet been compiled to machine code, the JIT'r is invoked to compile that function "just in time". And so on.

The JIT'r doesn't necessarily compile all the logic of a function body into machine code at once. If the function has if statements, the statement blocks of the if or else clauses may not be JIT compiled until execution actually passes through that block. Code paths that have not executed remain in IL form until they do execute.

The compiled native machine code is kept in memory so that it can be used again the next time that section of code executes. The second time you call a function it will run faster than the first time you call it because no JIT step is necessary the second time around.

In desktop .NET, the native machine code is kept in memory for the lifetime of the appdomain. In .NET CF, the native machine code may be thrown away if the application is running low on memory. It will be JIT compiled again from the original IL code the next time execution passes through that code.

With the information from that question, and the information from Hans Passant, it is very clear what is happening:

  1. The JIT compiler attempts to convert the entire entry point code block (in this case my Main() function) into native code. This requires that it resolve all references.
  2. The embedded assembly LoaderLibrary.dll has NOT been loaded into the AppDomain yet because the code that does this is defined in the Main() function (and it can't execute code that hasn't been compiled).
  3. The JIT compiler attempts to resolve the reference to LoaderLibrary.dll by searching the AppDomain, Global Assembly Cache, App.config/Web.config, and probing (environment PATH, current working directory, etc.) More info on this can be found in the MSDN article here: How the Runtime Locates Assemblies
  4. The JIT compiler fails to resolve the reference to LoaderLibrary.LoaderLibrary.Test(); and results in the error Could not load file or assembly or one of its dependencies

The way to get around this as suggested by Hans Passant is to load your assemblies in a code block that gets JIT compiled earlier than any code block that references those assemblies.

By adding the [MethodImpl(MethodImplOptions.NoInlining)] to methods that reference the dynamically loaded assemblies, it will prevent the optimizer from trying to inline the method code.

Afore answered 24/2, 2016 at 3:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.