Sco*_*ski 44 .net compiler-construction clr language-features programming-languages
有谁知道是否可以在.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;
Run Code Online (Sandbox Code Playgroud)
"tuple"的定义如下:
public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration
{
//...
}
Run Code Online (Sandbox Code Playgroud)
在这个特定的例子中,类型构造函数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;
}
Run Code Online (Sandbox Code Playgroud)
然后只重定向来自第i,j和k元组字段来访问Field1
,Field2
和Field3
.
然而,这不是一个真正可行的选择.这意味着,在编译时tuple<x as int>
和tuple<y as int>
将结束是不同的类型,而在运行时,他们将作为同一类型进行处理.这会对平等和类型识别等问题造成许多问题.这对我的口味来说太过疏漏.
其他可能的选择是使用"状态包对象".但是,使用状态包会破坏支持语言中"类型构造函数"的全部目的.其目的是启用"自定义语言扩展"以在编译时生成新类型,编译器可以使用它来执行静态类型检查.
在Java中,这可以使用自定义类加载器来完成.基本上,可以在不实际定义磁盘类型的情况下发出使用元组类型的代码.然后可以定义一个自定义的"类加载器",它将在运行时动态生成元组类型.这将允许在编译器内部进行静态类型检查,并将跨编译边界统一元组类型.
但遗憾的是,CLR不支持自定义类加载.CLR中的所有加载都在汇编级别完成.可以为每个"构造类型"定义一个单独的程序集,但这很快就会导致性能问题(许多程序集中只有一种类型会使用太多资源).
所以,我想知道的是:
是否有可能在.NET中模拟Java类加载器,我可以在其中发出对不存在类型的引用,然后在运行时需要使用它的代码运行之前在运行时动态生成对该类型的引用?
注意:
*我实际上已经知道了问题的答案,我在下面提供了答案.然而,我花了大约3天的研究时间,以及相当多的IL黑客攻击来提出解决方案.我认为在这里记录它是个好主意,以防其他人遇到同样的问题.*
Sco*_*ski 52
答案是肯定的,但解决方案有点棘手.
的System.Reflection.Emit
命名空间定义类型的,其允许动态地生成组件.它们还允许以递增方式定义生成的程序集.换句话说,可以向动态程序集添加类型,执行生成的代码,然后向程序集添加更多类型.
的System.AppDomain
类也定义一个AssemblyResolve时将触发框架无法加载的组件事件.通过为该事件添加处理程序,可以定义一个"运行时"程序集,所有"构造"类型都放在该程序集中.使用构造类型的编译器生成的代码将引用运行时程序集中的类型.因为运行时程序集实际上不存在于磁盘上,所以在编译代码第一次尝试访问构造类型时会触发AssemblyResolve事件.然后,事件的句柄将生成动态程序集并将其返回给CLR.
不幸的是,要让它发挥作用还有一些棘手的问题.第一个问题是确保在编译代码运行之前始终安装事件处理程序.使用控制台应用程序很容易.连接事件处理程序的代码可以Main
在其他代码运行之前添加到方法中.但是,对于类库,没有主要方法.dll可以作为用另一种语言编写的应用程序的一部分加载,因此实际上不可能假设总有一个主方法可用于连接事件处理程序代码.
第二个问题是确保在使用引用它们的任何代码之前将引用的类型全部插入到动态程序集中.的System.AppDomain
类也定义了TypeResolve
CLR无法解析动态程序集中的类型时执行的事件.它使事件处理程序有机会在使用它的代码运行之前定义动态程序集内的类型.但是,在这种情况下,该事件将不起作用.即使引用的程序集是动态定义的,CLR也不会为其他程序集"静态引用"的程序集触发事件.这意味着我们需要一种在编译程序集中的任何其他代码运行之前运行代码的方法,并且如果尚未定义它们,则将它需要的类型动态注入运行时程序集.否则,当CLR尝试加载这些类型时,它会注意到动态程序集不包含它们需要的类型,并将抛出类型加载异常.
幸运的是,CLR为这两个问题提供了解决方案:模块初始化器.模块初始值设定项相当于"静态类构造函数",除了它初始化整个模块,而不仅仅是单个类.在理论上,CLR将:
它对所有程序集执行此操作,包括类库和可执行程序,并且EXE将在执行Main方法之前运行模块构造函数.
有关构造函数的更多信息,请参阅此博客文章.
在任何情况下,我的问题的完整解决方案需要几个部分:
以下类定义,在"语言运行时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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
该类定义了一个单例,它包含对将在其中创建构造类型的动态程序集的引用.它还包含一个"哈希集",用于存储已动态生成的类型集,最后定义一个可以成员的成员用于定义类型.此示例只返回一个System.Reflection.Emit.TypeBuilder实例,该实例随后可用于定义正在生成的类.在一个真实的系统中,该方法可能会采用该类的AST表示,并且只是自己进行生成.
发出以下两个引用的编译程序集(以ILASM语法显示):
.assembly extern $Runtime
{
.ver 0:0:0:0
}
.assembly extern SharedLib
{
.ver 1:0:0:0
}
Run Code Online (Sandbox Code Playgroud)
这里"SharedLib"是Language的预定义运行时库,包含上面定义的"Loader"类,"$ Runtime"是将被插入的构造类型的动态运行时程序集.
用语言编译的每个程序集中的"模块构造函数".
据我所知,没有.NET语言允许在源代码中定义模块构造函数.C++/CLI编译器是我所知道的唯一生成它们的编译器.在IL中,它们看起来像这样,直接在模块中定义,而不是在任何类型定义中:
.method privatescope specialname rtspecialname static
void .cctor() cil managed
{
//generate any constructed types dynamically here...
}
Run Code Online (Sandbox Code Playgroud)
对我来说,这不是一个问题,我必须编写自定义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;
}
Run Code Online (Sandbox Code Playgroud)
元组类是作为通用类型生成的,以解决可访问性问题.这将允许编译程序集中的代码使用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();
}
}
Run Code Online (Sandbox Code Playgroud)因此,无论如何,这是我能够提出的机制,以便在CLR中启用自定义类加载器的粗略等价物.
有谁知道更简单的方法吗?