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

相当于中的Class Loaders。导航战鉴定小组

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

    有人知道是否可以在中定义“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;
    

    其中“tuple”的定义如下:

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

    在这个特定的例子中,类型构造函数 tuple 提供了类似于VB和C#中的匿名类型的东西。

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

    这意味着我需要一种方法,使编译器最终发出的类型可以在多个程序集之间共享。例如,我想要

    tuple<x as int> 在程序集A中定义,最终与 元组<x为整数> 定义见汇编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 .

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

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

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

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

    所以,我想知道的是:

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

    注:

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

    2 回复  |  直到 11 年前
        1
  •  51
  •   Mat    12 年前

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

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

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

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

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

    幸运的是,CLR为这两个问题提供了一个解决方案:模块初始化器。模块初始化器相当于“静态类构造函数”,除了它初始化整个模块,而不仅仅是一个类。Baiscaly,CLR将:

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

    它对所有程序集都这样做,包括类库和可执行文件,对于EXE,它将在执行Main方法之前运行模块构造函数。

    看这个 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;
              }
          }
      }
      

      该类定义了一个单例,其中包含对将在其中创建构造类型的动态程序集的引用。它还包含一个“哈希集”,用于存储已经动态生成的类型集,并最终定义一个可用于定义类型的成员。这个例子只返回一个系统。反思。发射。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    17 年前

    我认为这就是DLR在C#4.0中应该提供的东西。目前还很难获得信息,但也许我们会在PDC08中学到更多。急切地等待着你的C#3解决方案。..我猜它使用匿名类型。