44

有谁知道是否可以在.NET 中定义等效于“java 自定义类加载器”的内容?

提供一点背景:

我正在开发一种针对 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 将在不同的时间编译,这意味着它们最终都会发出自己不兼容的元组类型版本。

我研究了使用某种“类型擦除”来做到这一点,这样我就可以拥有一个包含一堆这样的类型的共享库(这是“Liberty”语法):

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

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

然后将访问从 i、j 和 k 元组字段重定向到Field1Field2Field3.

然而,这并不是一个真正可行的选择。这意味着在编译时tuple<x as int>最终tuple<y as int>会成为不同的类型,而在运行时它们将被视为相同的类型。这会给诸如平等和类型标识之类的事情带来很多问题。这对我的口味来说太抽象了。

其他可能的选择是使用“状态包对象”。但是,使用状态包会破坏语言中支持“类型构造函数”的全部目的。想法是启用“自定义语言扩展”以在编译时生成新类型,编译器可以使用这些新类型进行静态类型检查。

在 Java 中,这可以使用自定义类加载器来完成。基本上可以发出使用元组类型的代码,而无需在磁盘上实际定义类型。然后可以定义一个自定义的“类加载器”,它将在运行时动态生成元组类型。这将允许在编译器内部进行静态类型检查,并将跨编译边界统一元组类型。

然而不幸的是,CLR 不提供对自定义类加载的支持。CLR 中的所有加载都是在程序集级别完成的。可以为每个“构造类型”定义一个单独的程序集,但这会很快导致性能问题(有许多只有一种类型的程序集会使用太多资源)。

所以,我想知道的是:

是否可以在 .NET 中模拟 Java 类加载器之类的东西,我可以在其中发出对不存在类型的引用,然后在需要使用它的代码运行之前在运行时动态生成对该类型的引用?

笔记:

*我实际上已经知道这个问题的答案,我在下面提供了答案。然而,我花了大约 3 天的时间进行研究,并进行了大量的 IL 黑客攻击,以便提出解决方案。我认为在这里记录它是一个好主意,以防其他人遇到同样的问题。*

4

2 回答 2

52

答案是肯定的,但解决方案有点棘手。

System.Reflection.Emit命名空间定义了允许动态生成程序集的类型。它们还允许增量定义生成的程序集。换句话说,可以向动态程序集添加类型,执行生成的代码,然后再向程序集添加更多类型。

该类System.AppDomain还定义了一个AssemblyResolve事件,该事件在框架无法加载程序集时触发。通过为该事件添加处理程序,可以定义一个“运行时”程序集,所有“构造”类型都放置在其中。使用构造类型的编译器生成的代码将引用运行时程序集中的类型。因为运行时程序集实际上并不存在于磁盘上,所以在编译代码第一次尝试访问构造类型时会触发AssemblyResolve事件。然后,事件的句柄将生成动态程序集并将其返回给 CLR。

不幸的是,要让它发挥作用有一些棘手的问题。第一个问题是确保在编译代码运行之前始终安装事件处理程序。使用控制台应用程序,这很容易。连接事件处理程序的代码可以Main在其他代码运行之前添加到方法中。然而,对于类库,没有 main 方法。一个 dll 可以作为用另一种语言编写的应用程序的一部分加载,因此不可能假设总是有一个 main 方法可用于连接事件处理程序代码。

第二个问题是确保在使用任何引用它们的代码之前将引用的类型全部插入到动态程序集中。该类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 ,其中 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 中启用大致等效的自定义类加载器。

有谁知道更简单的方法来做到这一点?

于 2008-10-09T03:39:09.350 回答
-5

我认为这是 DLR 应该在 C# 4.0 中提供的类型。有点难以获得信息,但也许我们会在 PDC08 上了解更多信息。虽然急切地等待看到您的 C# 3 解决方案......我猜它使用匿名类型。

于 2008-10-09T03:38:42.563 回答