.NET中类加载器的等价类
有谁知道是否有可能在.NET中定义“java自定义类加载器”的等价物?
给一点背景:
我正在开发一种以CLR为目标的新的编程语言,称为“自由”。 该语言的特点之一是它能够定义“types构造函数”,它是编译器在编译时执行的方法,并生成types作为输出。 它们是generics的泛化(语言确实具有普通的generics),并允许这样的代码被编写(使用“Liberty”语法):
var t as tuple<i as int, j as int, k as int>; ti = 2; tj = 4; tk = 5;
其中“元组”的定义如下:
public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration { //... }
在这个特定的例子中,types构造函数tuple
提供了类似于VB和C#中的匿名types的东西。
然而,与匿名types不同,“元组”具有名称,可以在公共方法签名中使用。
这意味着我需要一种types的方式,最终最终由编译器发出,可以跨多个程序集共享。 例如,我想要
在程序集A中定义的tuple<x as int>
最终与程序集B中定义的tuple<x as int>
相同。
这个问题当然就是Assembly A和Assembly B将会在不同的时间被编译,这意味着他们最终都会发布他们自己的元组types的不兼容版本。
我研究了使用某种“types擦除”来做到这一点,所以我会有一个像这样的一堆types的共享库(这是“Liberty”语法):
class tuple<T> { public Field1 as T; } class tuple<T, R> { public Field2 as T; public Field2 as R; }
然后将访问从i,j和k元组字段redirect到Field1
, Field2
和Field3
。
但是,这不是一个真正可行的select。 这意味着在编译时tuple<x as int>
和tuple<y as int>
将最终成为不同types,而在运行时它们将被视为相同types。 这会给平等和types认同等问题带来许多问题。 这对我的口味来说太抽象了。
其他可能的select是使用“状态包对象”。 然而,使用状态包会打破语言中支持“types构造函数”的全部目的。 这个想法是使编译时能够使用“自定义语言扩展”生成新的types,编译器可以使用静态types检查。
在Java中,这可以使用自定义类加载器来完成。 基本上,使用元组types的代码可以在没有实际定义磁盘types的情况下发射。 然后可以定义一个定制的“类加载器”,在运行时dynamic生成元组types。 这将允许在编译器内部进行静态types检查,并且会在编译边界上统一元组types。
然而,不幸的是,CLR不提供对自定义类加载的支持。 CLR中的所有加载都是在汇编级完成的。 可以为每个“构造types”定义一个单独的程序集,但是这很快就会导致性能问题(许多只有一种types的程序集会使用太多的资源)。
所以,我想知道的是:
是否有可能在.NET中模拟Java Class Loaders之类的东西,在那里我可以发出对一个不存在的types的引用,然后在需要使用它的代码运行之前,在运行时dynamic生成对该types的引用?
注意:
*其实我已经知道这个问题的答案,我在下面提供答案。 然而,我花了大约3天的时间进行研究,还有相当一部分的黑客攻击,以便提出解决scheme。 我认为这是一个好主意,以防万一遇到同样的问题。 *
答案是肯定的,但解决scheme有点棘手。
System.Reflection.Emit
命名空间定义了允许dynamic生成程序集的types。 他们还允许生成的程序集逐步定义。 换句话说,可以向dynamic程序集添加types,执行生成的代码,然后向程序集添加更多types。
System.AppDomain
类还定义一个AssemblyResolve事件,每当框架加载程序集失败时触发。 通过为该事件添加处理程序,可以定义一个“运行时”程序集,将所有“构造”types放入其中。 由编译器生成的使用构造types的代码将引用运行时程序集中的types。 由于运行时程序集实际上并不存在于磁盘上,所以在第一次编译代码尝试访问构造types时,会触发AssemblyResolve事件。 然后事件的句柄将生成dynamic程序集并将其返回给CLR。
不幸的是,有一些棘手的问题需要解决。 第一个问题是确保在编译代码运行之前总是安装事件处理程序。 使用控制台应用程序很容易。 连接事件处理程序的代码可以在其他代码运行之前添加到Main
方法中。 但是,对于类库,没有主要的方法。 一个dll可能会被加载为另一种语言编写的应用程序的一部分,所以不可能假设总是有一个主要的方法来连接事件处理程序代码。
第二个问题是确保引用types在使用任何引用它们的代码之前都被插入到dynamic程序集中。 System.AppDomain
类还定义了一个TypeResolve
事件,该事件在CLR无法parsingdynamic程序集中的types时执行。 它使事件处理程序有机会在运行使用它的代码之前定义dynamic程序集内部的types。 但是,这个事件在这种情况下不起作用。 即使引用的程序集是dynamic定义的,CLR也不会为其他程序集“静态引用”的程序集激发事件。 这意味着我们需要一种在编译的程序集中的任何其他代码运行之前运行代码的方法,并且如果它们还没有被定义,就会dynamic地将它需要的types注入到运行时程序集中。 否则,当CLR尝试加载这些types时,它会注意到dynamic程序集不包含它们需要的types,并且会抛出一个types加载exception。
幸运的是,CLR为这两个问题提供了一个解决scheme:模块初始化器。 模块初始化器相当于“静态类构造器”,不同之处在于它初始化整个模块,而不仅仅是一个类。 在财政方面,CLR将:
- 在访问模块内的任何types之前运行模块构造函数。
- 保证只有那些由模块构造器直接访问的types才会在执行时加载
- 在构造函数完成之前,不允许模块外的代码访问它的任何成员。
它为所有程序集执行此操作,包括类库和可执行文件,EXE将在执行Main方法之前运行模块构造函数。
看到这个博客文章关于构造函数的更多信息。
无论如何,我的问题的完整解决scheme需要几件:
-
在“语言运行时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; } } }
这个类定义了一个单元,它包含了构造types将被创build的dynamic程序集的引用。它还包含一个“哈希集合”,用于存储已经dynamic生成的types集合,最后定义一个成员用于定义types。 这个例子只是返回一个System.Reflection.Emit.TypeBuilder实例,然后可以用来定义正在生成的类。 在一个真正的系统中,该方法可能会采用类的AST表示forms,而只是自己生成一代。
-
编译发布以下两个引用的程序集(以ILASM语法显示):
.assembly extern $Runtime { .ver 0:0:0:0 } .assembly extern SharedLib { .ver 1:0:0:0 }
这里“SharedLib”是语言的预定义运行时库,包括上面定义的“Loader”类,“$ Runtime”是将被插入的dynamic运行时程序集。
-
在该语言中编译的每个程序集内的“模块构造函数”。
据我所知,没有.NET语言允许在源代码中定义模块构造函数。 C ++ / CLI编译器是我所知道的唯一编译器。 在IL中,它们看起来像这样,直接在模块中定义,而不在任何types定义中:
.method privatescope specialname rtspecialname static void .cctor() cil managed { //generate any constructed types dynamically here... }
对我来说,这不是一个问题,我不得不编写自定义IL来使这个工作。 我正在编写一个编译器,所以代码生成不是一个问题。
在使用types
tuple<i as int, j as int>
和tuple<x as double, y as double, z as double>
模块构造函数需要生成types如下所示(这里是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; }
元组类被生成为genericstypes来解决可访问性问题。 这将允许编译的程序集中的代码使用
tuple<x as Foo>
,其中Foo是一些非公共types。这样做的模块构造函数(这里只显示一种types,用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中实现自定义类加载器的粗略对等机制。
有谁知道更简单的方法来做到这一点?
我认为这是DLR应该在C#4.0中提供的types。 很难得到信息,但也许我们会在PDC08上学到更多。 急切地等待看你的C#3解决scheme,虽然…我猜它使用匿名types。