42

Кто-нибудь знает, можно ли определить эквивалент «java custom class loader» в .NET?Эквивалент загрузчиков классов в .NET

Чтобы дать немного фона:

Я нахожусь в процессе разработки нового языка программирования, нацеленного на CLR, которая называется «Свобода». Одной из особенностей языка является его способность определять «конструкторы типов», которые являются методами, которые выполняются компилятором во время компиляции и генерируют типы как выходные данные. Они являются своего рода обобщение генериков (язык имеет нормальные дженериков в нем), и позволяют код, как это будет написано (в «Либерти» синтаксис):

var t as tuple<i as int, j as int, k as int>; 
t.i = 2; 
t.j = 4; 
t.k = 5; 

Где «кортеж» определяется как так :

public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration 
{ 
    //... 
} 

в этом конкретном примере, конструктор типа tuple обеспечивает нечто похожее на анонимные типы в VB и C#.

Однако, в отличие от анонимных типов, «кортежи» имеют имена и могут использоваться внутри публичных сигнатур методов.

Это означает, что мне нужен способ для типа, который в конечном итоге испускается компилятором для совместного использования в нескольких сборках. Например, я хочу

tuple<x as int>, определенные в Ассамблеи А в конечном итоге быть того же типа, как это определено в tuple<x as int> Ассамблеи B.

Проблема с этим, конечно, является то, что Ассамблея А и B Ассамблеи собираются скомпилироваться в разное время, что означает, что они оба получат свои собственные несовместимые версии типа кортежа.

Я посмотрел в использовании своего рода «типа стирания», чтобы сделать это, так что я бы разделяемую библиотеку с кучей типов, как это (это синтаксис «Свобода»):

class tuple<T> 
{ 
    public Field1 as T; 
} 

class tuple<T, R> 
{ 
    public Field2 as T; 
    public Field2 as R; 
} 

а затем просто перенаправить доступ из полей i, j и k tuple в Field1, Field2 и Field3.

Однако это не является жизнеспособным вариантом. Это означало бы, что во время компиляции tuple<x as int> и tuple<y as int> в конечном итоге будут разными типами, а во время выполнения они будут рассматриваться как один и тот же тип. Это может вызвать множество проблем для таких вещей, как идентификация равенства и типа. Это слишком непроницаемо для абстракции для моих вкусов.

Другими возможными вариантами могут быть использование «предметов государственной сумки». Однако использование государственного мешка может победить всю цель поддержки «конструкторов типов» на языке. Идея состоит в том, чтобы включить «пользовательские расширения языка» для генерации новых типов во время компиляции, с которыми компилятор может выполнять проверку статического типа.

В Java это можно сделать с помощью пользовательских загрузчиков классов. В принципе, код, который использует типы кортежей, может быть выпущен без фактического определения типа на диске. Затем может быть определен пользовательский «загрузчик классов», который будет динамически генерировать тип кортежа во время выполнения. Это позволит проверять статический тип внутри компилятора и объединять типы кортежей между границами компиляции.

К сожалению, однако, CLR не поддерживает загрузку пользовательского класса.Вся загрузка в CLR выполняется на уровне сборки. Можно было бы определить отдельную сборку для каждого «построенного типа», но это очень быстро привело бы к проблемам с производительностью (наличие множества сборок с одним типом в них будет использовать слишком много ресурсов).

Итак, что я хочу знать:

Можно ли смоделировать что-то вроде Java загрузчиков классов в .NET, где я могу испускать ссылку на несуществующий тип в и затем динамически генерировать ссылка на этот тип во время выполнения перед тем, как код, который ему нужно использовать, запускается?

Примечание:

* Я на самом деле уже знаю ответ на вопрос, который я обеспечиваю как ответ ниже. Тем не менее, мне потребовалось около 3 дней исследований и довольно немного взлома IL, чтобы придумать решение. Я подумал, что было бы неплохо документировать его здесь, если бы кто-то другой столкнулся с той же проблемой. *

+0

О, ничего себе, первое сообщение, которое я когда-либо думал, должно иметь главы. Отличная информация! Спасибо за сообщение! –

ответ

51

Ответ да, но решение немного сложно.

Пространство имен System.Reflection.Emit определяет типы, которые позволяют создавать сборки в динамическом режиме. Они также позволяют постепенно создавать сгенерированные сборки. Другими словами, можно добавлять типы в динамическую сборку, выполнять сгенерированный код, а затем добавлять новые типы в сборку.

Класс System.AppDomain также определяет событие AssemblyResolve, которое срабатывает всякий раз, когда инфраструктура не может загрузить сборку. Добавив обработчик для этого события, можно определить единую сборку, в которую помещаются все «построенные» типы. Код, созданный компилятором, который использует построенный тип, будет ссылаться на тип в сборке времени выполнения. Поскольку сборка времени выполнения на самом деле не существует на диске, событие AssemblyResolve будет запущено в первый раз, когда скомпилированный код попытается получить доступ к построенному типу. Затем дескриптор события генерирует динамическую сборку и возвращает ее в CLR.

К сожалению, есть несколько сложных моментов, чтобы заставить это работать. Первой проблемой является обеспечение того, чтобы обработчик события всегда устанавливался до запуска компилируемого кода. С консольным приложением это легко. Код для подключения обработчика событий можно просто добавить к методу Main до того, как будет запущен другой код. Однако для библиотек классов нет основного метода. DLL может быть загружена как часть приложения, написанного на другом языке, поэтому на самом деле невозможно предположить, что всегда существует основной метод, позволяющий подключить код обработчика событий.

Вторая проблема заключается в обеспечении того, чтобы ссылочные типы все вставлялись в динамическую сборку, прежде чем использовать какой-либо код, который ссылается на них. Класс System.AppDomain также определяет событие TypeResolve, которое выполняется всякий раз, когда CLR не может разрешить тип динамической сборки. Это дает обработчику событий возможность определять тип внутри динамической сборки до того, как код, который ее использует, запускается. Однако это событие не будет работать в этом случае. CLR не будет запускать событие для сборок, которые статически ссылаются на другие сборки, даже если ссылочная сборка определена динамически. Это означает, что нам нужен способ запуска кода, прежде чем какой-либо другой код в скомпилированной сборке будет запущен, и попросите его динамически вставить требуемые типы в сборку времени выполнения, если они еще не определены.В противном случае, когда CLR попытается загрузить эти типы, будет видно, что динамическая сборка не содержит типов, которые им нужны, и будет вызывать исключение загрузки типа.

К счастью, CLR предлагает решение обеих проблем: Инициализаторы модулей. Инициализатор модуля является эквивалентом «статического конструктора классов», за исключением того, что он инициализирует весь модуль, а не только один класс. В логическом отношении, CLR будет:

  1. Запуск конструктора модуля до того, как будут доступны любые типы внутри модуля.
  2. Гарантируйте, что только те типы, к которым напрямую обращается конструктор модуля, будут загружены во время выполнения
  3. Не разрешать доступ ко всем членам этого модуля до тех пор, пока конструктор не закончит работу.

Он делает это для всех сборок, включая библиотеки классов и исполняемые файлы, а для EXE будет запускать конструктор модуля перед выполнением метода Main.

См. Это blog post для получения дополнительной информации о конструкторах.

В любом случае, полное решение моей проблемы требует несколько частей:

  1. следующего определения класса, определенных внутри «DLL выполнения языка», на который ссылается все сборками, производимых компилятор (это код C#).

    using System; 
    using System.Collections.Generic; 
    using System.Reflection; 
    using System.Reflection.Emit; 
    
    namespace SharedLib 
    { 
        public class Loader 
        { 
         private Loader(ModuleBuilder dynamicModule) 
         { 
          m_dynamicModule = dynamicModule; 
          m_definedTypes = new HashSet<string>(); 
         } 
    
         private static readonly Loader m_instance; 
         private readonly ModuleBuilder m_dynamicModule; 
         private readonly HashSet<string> m_definedTypes; 
    
         static Loader() 
         { 
          var name = new AssemblyName("$Runtime"); 
          var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); 
          var module = assemblyBuilder.DefineDynamicModule("$Runtime"); 
          m_instance = new Loader(module); 
          AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); 
         } 
    
         static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) 
         { 
          if (args.Name == Instance.m_dynamicModule.Assembly.FullName) 
          { 
           return Instance.m_dynamicModule.Assembly; 
          } 
          else 
          { 
           return null; 
          } 
         } 
    
         public static Loader Instance 
         { 
          get 
          { 
           return m_instance; 
          } 
         } 
    
         public bool IsDefined(string name) 
         { 
          return m_definedTypes.Contains(name); 
         } 
    
         public TypeBuilder DefineType(string name) 
         { 
          //in a real system we would not expose the type builder. 
          //instead a AST for the type would be passed in, and we would just create it. 
          var type = m_dynamicModule.DefineType(name, TypeAttributes.Public); 
          m_definedTypes.Add(name); 
          return type; 
         } 
        } 
    } 
    

    Класс определяет синглтон, который содержит ссылку на динамическую сборку, построенные типы будут созданы. Он также имеет «хэш-набор», который хранит множество типов, которые уже были динамически генерируемым, и, наконец, определяет элемент, который можно использовать для определения типа. Этот пример возвращает экземпляр System.Reflection.Emit.TypeBuilder, который затем может использоваться для определения генерируемого класса. В реальной системе метод, вероятно, возьмет в AST-представление класса, а просто сделает это сам.

  2. скомпилированные сборки, которые испускают следующие две ссылки (показано в синтаксисе ILASM):

    .assembly extern $Runtime 
    { 
        .ver 0:0:0:0 
    } 
    .assembly extern SharedLib 
    { 
        .ver 1:0:0:0 
    } 
    

    Здесь «SharedLib» предопределена библиотека времени выполнения на языке, которая включает в себя класс «Loader», определенный выше, и «$ Время воспроизведения "- это динамическая сборка времени выполнения, в которую будут вставляться готовые типы.

  3. «Конструктор модулей» внутри каждой сборки, составленной на языке.

    Насколько я знаю, нет языков .NET, которые позволяют определять конструкторы модулей в источнике. Компилятор C++/CLI - единственный компилятор, о котором я знаю, который их генерирует. В IL, они выглядят так, определяются непосредственно в модуле, а не в каких-либо определений типа:

    .method privatescope specialname rtspecialname static 
         void .cctor() cil managed 
    { 
        //generate any constructed types dynamically here... 
    } 
    

    Для меня это не проблема, что я должен написать собственный IL, чтобы получить эту работу. Я пишу компилятор, поэтому генерация кода не является проблемой.

    В случае сборки, используемых типах tuple<i as int, j as int> и tuple<x as double, y as double, z as double> конструктор модуля необходимо будет генерировать типы, как следующее (здесь в синтаксисе C#):

    class Tuple_i_j<T, R> 
    { 
        public T i; 
        public R j; 
    } 
    
    class Tuple_x_y_z<T, R, S> 
    { 
        public T x; 
        public R y; 
        public S z; 
    } 
    

    Кортеж классы создаются как родовые типы для решения проблем доступности. Это позволило бы коду в сборной сборке использовать tuple<x as Foo>, где Foo был непубличным.

    Тело конструктора модуля, который сделал это (здесь только показывает один тип, и написан на C# синтаксис) будет выглядеть следующим образом:

    var loader = SharedLib.Loader.Instance; 
    lock (loader) 
    { 
        if (! loader.IsDefined("$Tuple_i_j")) 
        { 
         //create the type. 
         var Tuple_i_j = loader.DefineType("$Tuple_i_j"); 
         //define the generic parameters <T,R> 
         var genericParams = Tuple_i_j.DefineGenericParameters("T", "R"); 
         var T = genericParams[0]; 
         var R = genericParams[1]; 
         //define the field i 
         var fieldX = Tuple_i_j.DefineField("i", T, FieldAttributes.Public); 
         //define the field j 
         var fieldY = Tuple_i_j.DefineField("j", R, FieldAttributes.Public); 
         //create the default constructor. 
         var constructor= Tuple_i_j.DefineDefaultConstructor(MethodAttributes.Public); 
    
         //"close" the type so that it can be used by executing code. 
         Tuple_i_j.CreateType(); 
        } 
    } 
    

Так что в любом случае, это был механизм Я смог придумать, чтобы позволить грубый эквивалент пользовательских загрузчиков классов в CLR.

Кто-нибудь знает более простой способ сделать это?

+0

Ugh, как ваше определение конструктора вашего модуля отличается от обычного класса конструктора? Разница в использовании 'privatescope' в отличие от' private hidebysig'? –

+0

Ahh, только что понял. Никакая разница, кроме модуля, не помещается в какой-либо конкретный тип. Не знал, что вы могли бы это сделать :) –

-5

Я думаю, что это тот тип, который DLR должен предоставить в C# 4.0. Пока сложно найти информацию, но, возможно, мы узнаем больше на PDC08. С нетерпением жду вашего решения C# 3, хотя ... Я предполагаю, что он использует анонимные типы.

Смежные вопросы