代码之家  ›  专栏  ›  技术社区  ›  Scott Wisniewski

等效于.NET中的类加载器

  •  44
  • Scott Wisniewski  · 技术社区  · 16 年前

    有没有人知道是否可以在.NET中定义一个等价的“Java自定义类加载器”?

    提供一些背景:

    我正在开发一种针对clr的新编程语言,叫做“自由”。该语言的一个特点是它能够定义“类型构造函数”,这是编译器在编译时执行的方法,并将类型生成为输出。它们是泛型的一种泛化(语言中确实有正常的泛型),并允许编写类似这样的代码(使用“自由”语法):

    var t as tuple<i as int, j as int, k as int>;
    t.i = 2;
    t.j = 4;
    t.k = 5;
    

    其中“tuple”的定义如下:

    public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration
    {
       //...
    }
    

    在这个特定的示例中,类型构造函数 tuple 提供类似于VB和C中匿名类型的内容。

    但是,与匿名类型不同,“元组”具有名称,可以在公共方法签名中使用。

    这意味着我需要一种方式,使编译器最终发出的类型能够跨多个程序集共享。例如,我想要

    tuple<x as int> 在程序集A中定义为与 tuple<x作为int> 在组件B中定义。

    当然,这个问题是,程序集A和程序集B将在不同的时间编译,这意味着它们最终都会发出自己不兼容的元组类型版本。

    我研究了使用某种“类型擦除”来实现这一点,这样我就有了一个共享库,其中有许多类型(这是“自由”语法):

    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作为int> tuple<y as int> 最终将成为不同的类型,而在运行时,它们将被视为相同的类型。这将导致许多问题,如平等和类型认同。对我来说,抽象的概念太漏了。

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

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

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

    所以,我想知道的是:

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

    注:

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

    2 回复  |  直到 9 年前
        1
  •  51
  •   Mat    10 年前

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

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

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

    不幸的是,要使这项工作顺利进行还有一些棘手的问题。第一个问题是确保在运行已编译的代码之前始终安装事件处理程序。使用控制台应用程序,这很容易。用于连接事件处理程序的代码可以添加到 Main 方法。但是,对于类库,没有主方法。DLL可以作为用另一种语言编写的应用程序的一部分进行加载,因此不可能假定始终存在可用于连接事件处理程序代码的主方法。

    第二个问题是确保引用的类型都在使用引用它们的任何代码之前插入到动态程序集中。这个 系统.appdomain 类还定义了 TypeResolve 每当CLR无法解析动态程序集中的类型时执行的事件。它使事件处理程序有机会在使用它的代码运行之前在动态程序集中定义类型。但是,在这种情况下,该事件将不起作用。即使引用的程序集是动态定义的,CLR也不会为其他程序集“静态引用”的程序集触发事件。这意味着我们需要一种方法来在编译程序集中的任何其他代码运行之前运行代码,并让它动态地将所需的类型注入到运行时程序集中(如果尚未定义这些类型的话)。否则,当CLR尝试加载这些类型时,它将注意到动态程序集不包含它们需要的类型,并将引发类型加载异常。

    幸运的是,clr提供了两个问题的解决方案:模块初始值设定项。模块初始值设定项等价于“静态类构造函数”,只是它初始化了整个模块,而不仅仅是单个类。贝塞尔,CLR将:

    1. 在访问模块内的任何类型之前运行模块构造函数。
    2. 确保只有模块构造函数直接访问的类型才会在执行时加载
    3. 在构造函数完成之前,不允许模块外部的代码访问它的任何成员。

    它为所有程序集(包括类库和可执行文件)执行此操作,并且for-exes将在执行主方法之前运行模块构造函数。

    看到这个 blog post 有关构造函数的详细信息。

    在任何情况下,解决我的问题都需要几个步骤:

    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;
              }
          }
      }
      

      类定义了一个singleton,它保存对将在其中创建构造类型的动态程序集的引用。它还保存一个“哈希集”,存储已经动态生成的类型集,最后定义一个可用于定义类型的成员。此示例只返回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中实现自定义类装入器的大致等效的机制。

    有人知道更简单的方法吗?

        2
  •  -5
  •   Kevin Dostalek    16 年前

    我认为这是DLR应该在C 4.0中提供的类型。信息还很难提供,但也许我们会在pdc08中学到更多。尽管如此,急切地等待着看到你的C 3解决方案…我猜它使用匿名类型。