Почему Assembly.Load не влияет на текущий поток при разрешении ссылок (а не на отражение)?

Извиняюсь заранее, если название не имеет смысла. Я очень новичок в приложениях и сборке, и не знаю, как заявить, что я пытаюсь спросить.

Я возился с загрузкой встроенных DLL-приложений в приложение во время выполнения, и я не могу понять, почему он работает в одном направлении, но не в другом. Похоже, что если вы попытаетесь загрузить DLL файлы (из массива байтов) в текущий appdomain, любые объекты/потоки, созданные после этого, смогут разрешать ссылки на недавно загруженную библиотеку, однако объекты в исходном контексте не будут разрешены в отношении недавно загруженная библиотека.

Вот моя библиотека примеров, которая будет загружаться из встроенного ресурса во время выполнения (требуется ссылка на WPF PresentationFramework.dll для MessageBox):

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

В моем консольном приложении .csproj файл я вручную добавьте следующий внедренный ресурс для этого проекта и включить ссылку на проект в LoaderLibrary:

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

Вот код для моего консольного приложения, которое загружает эту библиотеку (требует ссылку на проект для LoaderLibrary csproj) ТАКЖЕ: нужно установить CopyLocal strong > до false для справки LoaderLibrary:

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];
        }
    }
}

Этот код способен успешно загружать и выполнять функцию LoaderLibrary.LoaderLibrary.Test().

Мой вопрос в том, почему следующее не работает?

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
}

Это также не работает:

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
}

Ответ 1

Большое спасибо Hans Passant и dthorpe за объяснение того, что происходит.

Я нашел dthorpe отличное объяснение того, как JIT-компилятор работает здесь: С# JIT-компиляция и .NET

Чтобы процитировать dthorpe здесь:

Да, код JIT'ing IL включает перевод IL на нативную машину инструкции.

Да, среда выполнения .NET взаимодействует с собственным машинным кодом JIT'ed, в том смысле, что среда выполнения имеет блоки памяти, занимаемые собственный машинный код, вызовы во время выполнения в собственный машинный код, и др.

Вы правы, что среда выполнения .NET не интерпретирует код IL в ваших сборках.

Что происходит, когда выполнение достигает функции или блока кода (например, else else блока if), который еще не был JIT, скомпилированный в собственный машинный код, JIT'r вызывается для компиляции этого блока IL в собственный машинный код. Когда это будет сделано, выполнение программы входит свежевыпущенный машинный код для выполнения его программной логики. Если при выполнении этого исполняемого машинного кода выполнение функции вызов функции, которая еще не была скомпилирована для машинного кода, JIT'r вызывается для компиляции этой функции "как раз вовремя". И так далее.

JIT'r не обязательно компилирует всю логику тела функции в машинный код сразу. Если функция имеет операторы if, Блоки операторов команд if и else могут не составлять JIT пока выполнение фактически не пройдет через этот блок. Кодовые пути, которые не выполняются, остаются в форме IL до тех пор, пока они не выполняются.

Скомпилированный собственный машинный код хранится в памяти, так что он может быть снова используется в следующий раз, когда выполняется секция кода. Второй время, когда вы вызываете функцию, она будет работать быстрее, чем в первый раз, когда вы вызовите его, потому что во второй раз не нужно делать шаг JIT.

В настольном компьютере .NET собственный машинный код хранится в памяти для время жизни приложения. В .NET CF собственный машинный код может быть выбрасывается, если приложение работает с низким объемом памяти. Это будет JIT снова скомпилирован из исходного кода IL в следующий раз проходит через этот код.

С информацией из этого вопроса и информацией от Hans Passant, очень ясно, что происходит:

  • Компилятор JIT пытается преобразовать весь код точки входа блок (в этом случае моя функция Main()) в собственный код. Эта требует разрешения всех ссылок.
  • Встроенная сборка LoaderLibrary.dll НЕ загружена в AppDomain еще, потому что код, который это делает, определен в Main() (и он не может выполнить код, который не был скомпилирован).
  • Компилятор JIT пытается разрешить ссылку на LoaderLibrary.dll, выполнив поиск в AppDomain, Global Assembly Cache, App.config/Web.config и зондирование (среда PATH, текущая рабочий каталог и т.д.). Более подробную информацию об этом можно найти в MSDN статья здесь: Как Runtime находит сборки
  • Компилятор JIT не может разрешить ссылку на LoaderLibrary.LoaderLibrary.Test(); и приводит к ошибке Could not load file or assembly or one of its dependencies

Как обойти это, как предложил Ханс Пассант, нужно загрузить ваши сборки в блок кода, который получает JIT, скомпилированный ранее, чем любой блок кода, который ссылается на эти сборки.

Добавив [MethodImpl (MethodImplOptions.NoInlining)] к методам, которые ссылаются на динамически загруженные сборки, это предотвратит попытку оптимизатора встроить код метода.