Ответ 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.
Кто-нибудь знает более простой способ сделать это?