Вопрос

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

Чтобы дать небольшую предысторию:

Я нахожусь в процессе разработки нового языка программирования, ориентированного на среду CLR, под названием "Liberty".Одной из особенностей языка является его способность определять "конструкторы типов", которые представляют собой методы, которые выполняются компилятором во время компиляции и генерируют типы в качестве выходных данных.Они являются своего рода обобщением дженериков (в языке есть обычные дженерики) и позволяют писать подобный код (в синтаксисе "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 будут компилироваться в разное время, что означает, что в конечном итоге они оба будут выдавать свои собственные несовместимые версии типа tuple.

Я рассматривал возможность использования какого-то "стирания типов" для этого, чтобы у меня была общая библиотека с кучей типов, подобных этому (это синтаксис "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 в .NET, где я могу выдавать ссылку на несуществующий тип, а затем динамически генерировать ссылку на этот тип во время выполнения до запуска кода, который должен его использовать?

ПРИМЕЧАНИЕ:

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

  1. Следующее определение класса, определенное внутри "language runtime 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.

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

Другие советы

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top