Подтвердить что ты не робот

Эквивалент загрузчиков классов в .NET.

Кто-нибудь знает, можно ли определить эквивалент "загрузчика пользовательских классов java" в .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 и С#.

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

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

tuple<x as int>, определенный в сборке А, чтобы в конечном итоге быть тем же типом, что и tuple<x as int>, определенным в сборке 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 Class Loaders в .NET, где я могу исправить ссылку на несуществующий тип, а затем динамически генерировать ссылку на этот тип во время выполнения до того, как код, который ему нужно использовать, запускается

Примечание:

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Дополнительную информацию о конструкторах см. в блоге.

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

  • Следующее определение класса, определенное внутри "языковой среды выполнения", на которое ссылаются все сборки, созданные компилятором (это код С#).

    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-представление класса, и просто сделает это сам.

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

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

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

  • "Конструктор модуля" внутри каждой сборки, скомпилированной на языке.

    Насколько я знаю, нет .NET-языков, которые позволяют определять конструкторы модулей в источнике. Компилятор С++/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>, конструктор модуля должен был генерировать такие типы, как следующие (здесь в синтаксисе С#):

    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 - некоторый непубличный тип.

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

    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.

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

Ответ 2

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