Эквивалент загрузчиков классов в .NET - PullRequest
44 голосов
/ 09 октября 2008

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

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

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

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> определен в сборке A, и в конечном итоге будет соответствовать типу tuple<x as int>, определенному в сборке B.

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

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

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

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

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

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

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

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

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

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

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

Примечание:

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

Ответы [ 2 ]

52 голосов
/ 09 октября 2008

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

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

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

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

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

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

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

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

См. сообщение в блоге для получения дополнительной информации о конструкторах.

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

  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», а «$ Runtime» - это динамическая сборка времени выполнения, в которую будут вставлены созданные типы.

  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 голосов
/ 09 октября 2008

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...