gpt4 book ai didi

.net - .NET 中类加载器的等效项

转载 作者:行者123 更新时间:2023-12-02 23:41:35 28 4
gpt4 key购买 nike

有谁知道是否可以在 .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# 中的匿名类型的东西。

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

这意味着我需要一种最终由编译器发出的类型的方法,以便在多个程序集中共享。例如,我想要
tuple<x as int>在程序集 A 中定义为与 tuple<x as 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 as int>tuple<y as int>最终会成为不同的类型,而在运行时它们将被视为相同的类型。这会导致诸如平等和类型标识之类的许多问题。这对我的口味来说太抽象了。

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

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

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

所以,我想知道的是:

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

注意:

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

最佳答案

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

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

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

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

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

幸运的是,CLR 为这两个问题提供了解决方案:模块初始值设定项。模块初始化器等同于“静态类构造器”,不同之处在于它初始化整个模块,而不仅仅是单个类。基本上,CLR 将:

  • 在访问模块内的任何类型之前运行模块构造函数。
  • 保证在执行
  • 时只加载模块构造函数直接访问的那些类型
  • 在构造函数完成之前,不允许模块外的代码访问它的任何成员。

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

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

    无论如何,我的问题的完整解决方案需要几个部分:
  • 以下类定义在“语言运行时 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 表示形式,然后自行生成。
  • 发出以下两个引用的已编译程序集(以 ILASM 语法显示):
    .assembly extern $Runtime
    {
    .ver 0:0:0:0
    }
    .assembly extern SharedLib
    {
    .ver 1:0:0:0
    }

    这里的“SharedLib”是该语言的预定义运行时库,包括上面定义的“Loader”类,“$Runtime”是将插入构造类型的动态运行时程序集。
  • 用该语言编译的每个程序集中的“模块构造函数”。

    据我所知,没有 .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 是一些非公共(public)类型。

    执行此操作的模块构造函数的主体(此处仅显示一种类型,并用 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 中启用自定义类加载器的粗略等效项。

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

    关于.net - .NET 中类加载器的等效项,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/185836/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com