.NETのクラスローダーに相当
-
06-07-2019 - |
質問
<!> quot; javaカスタムクラスローダー<!> quot;に相当するものを定義できるかどうかを知っていますか? .NETで?
背景を少し説明するには:
<!> quot; Liberty <!> quot;と呼ばれる、CLRをターゲットとする新しいプログラミング言語を開発中です。この言語の機能の1つは、<!> quot; type constructors <!> quot;を定義する機能です。これは、コンパイル時にコンパイラーによって実行され、出力として型を生成するメソッドです。それらはジェネリックの一種の一般化であり(言語には通常のジェネリックが含まれています)、次のようなコードを記述できます(<!> quot; Liberty <!> quot;構文):
var t as tuple<i as int, j as int, k as int>;
t.i = 2;
t.j = 4;
t.k = 5;
Where <!> quot; tuple <!> quot;次のように定義されます:
public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration
{
//...
}
この特定の例では、型コンストラクターtuple
はVBおよびC#の匿名型に類似したものを提供します。
ただし、匿名型とは異なり、<!> quot; tuples <!> quot;名前があり、パブリックメソッドシグネチャ内で使用できます。
これは、最終的にコンパイラによって発行されるタイプが複数のアセンブリ間で共有できるようにする方法が必要であることを意味します。たとえば、私は欲しい
アセンブリAで定義された tuple<x as int>
は、アセンブリBで定義されたField1
と同じタイプになります。
これに関する問題は、もちろん、アセンブリAとアセンブリBが異なるタイミングでコンパイルされることです。つまり、どちらもタプル型の互換性のないバージョンを発行することになります。
ある種の<!> quot; type erasure <!> quot;を使用して調べました。これを行うには、このようなタイプの束を持つ共有ライブラリが必要になります(これは<!> quot; Liberty <!> quot;構文です):
class tuple<T>
{
public Field1 as T;
}
class tuple<T, R>
{
public Field2 as T;
public Field2 as R;
}
そして、i、j、およびkタプルフィールドからのアクセスをField2
、Field3
、およびtuple<y as int>
にリダイレクトします。
ただし、これは実際には実行可能なオプションではありません。これは、コンパイル時に<=>と<=>が異なる型になることを意味しますが、実行時にはこれらは同じ型として扱われます。これは、平等やタイプアイデンティティなどの多くの問題を引き起こすでしょう。それは私の趣味にとって抽象化の漏れすぎです。
その他の可能なオプションは、<!> quot; state bag objects <!> quot;を使用することです。ただし、ステートバッグを使用すると、<!> quot; type constructors <!> quot;をサポートするという目的全体が無効になります。言語で。そこにあるアイデアは、<!> quot;カスタム言語拡張機能<!> quot;を有効にすることです。コンパイラが静的型チェックを実行できる新しい型をコンパイル時に生成します。
Javaでは、これはカスタムクラスローダーを使用して実行できます。基本的に、タプル型を使用するコードは、実際にディスク上の型を定義せずに発行できます。カスタム<!> quot; class loader <!> quot;実行時にタプル型を動的に生成するように定義できます。これにより、コンパイラ内部で静的型チェックが可能になり、コンパイルの境界を越えてタプル型が統一されます。
ただし、残念ながら、CLRはカスタムクラスの読み込みをサポートしていません。 CLRでのすべての読み込みは、アセンブリレベルで行われます。 <!> quot; constructed type <!> quot;ごとに個別のアセンブリを定義することも可能ですが、それは非常に迅速にパフォーマンスの問題につながります(1つのタイプのみの多くのアセンブリを使用すると、リソースが多くなりすぎます)。
だから、私が知りたいのは:
.NETのJava Class Loadersのようなものをシミュレートすることは可能ですか?そこでは、存在しない型への参照を発行し、使用するコードが実行される前に実行時にその型への参照を動的に生成できます
注:
*私は実際に質問に対する回答をすでに知っています。これを以下の回答として提供します。しかし、約3日間の調査と、解決策を見つけるためにかなりのILハッキングが必要でした。他の誰かが同じ問題に遭遇した場合に備えて、ここに文書化するのは良い考えだと思いました。 *
解決
答えはイエスですが、解決策は少しトリッキーです。
System.Reflection.Emit
名前空間は、アセンブリを動的に生成できます。また、生成されたアセンブリをインクリメンタルに定義できます。つまり、動的アセンブリに型を追加し、生成されたコードを実行してから、アセンブリに型を追加できます。
System.AppDomain
クラスは、 AssemblyResolve イベントは、フレームワークがアセンブリの読み込みに失敗したときに起動します。そのイベントのハンドラーを追加することにより、単一の<!> quot; runtime <!> quot;を定義することができます。すべての<!> quot; constructed <!> quot;のアセンブリ。タイプが配置されます。構築型を使用するコンパイラによって生成されたコードは、ランタイムアセンブリ内の型を参照します。ランタイムアセンブリは実際にはディスク上に存在しないため、 AssemblyResolve イベントは、コンパイルされたコードが構築型に最初にアクセスしようとしたときに発生します。イベントのハンドルは、動的アセンブリを生成し、CLRに返します。
残念ながら、これを機能させるにはいくつか注意が必要な点があります。最初の問題は、コンパイルされたコードが実行される前に、イベントハンドラーが常にインストールされるようにすることです。コンソールアプリケーションでは、これは簡単です。イベントハンドラを接続するコードは、他のコードが実行される前にMain
メソッドに追加するだけです。ただし、クラスライブラリの場合、メインメソッドはありません。 dllは別の言語で記述されたアプリケーションの一部としてロードされる可能性があるため、イベントハンドラコードを接続するためのメインメソッドが常に利用可能であると想定することは実際には不可能です。
2番目の問題は、参照される型がすべて動的アセンブリに挿入される前に、それらを参照するコードが使用されるようにすることです。 TypeResolve
クラスは、 tuple<i as int, j as int>
CLRが型を解決できない場合に実行されるイベント動的アセンブリ内。イベントハンドラーは、それを使用するコードが実行される前に、動的アセンブリ内で型を定義する機会を与えます。ただし、この場合、そのイベントは機能しません。 CLRは、<!> quot;静的に参照される<!> quot;のアセンブリに対してイベントを発生させません。参照されるアセンブリが動的に定義されている場合でも、他のアセンブリによって。これは、コンパイル済みアセンブリ内の他のコードが実行される前にコードを実行し、まだ定義されていない場合は必要な型をランタイムアセンブリに動的に挿入する方法が必要であることを意味します。そうしないと、CLRがこれらの型を読み込もうとしたときに、動的アセンブリに必要な型が含まれていないことがわかり、型読み込み例外がスローされます。
幸いなことに、CLRは両方の問題の解決策を提供します。モジュール初期化子。モジュール初期化子は<!> quot; static class constructor <!> quot;と同等ですが、単一のクラスだけでなくモジュール全体を初期化する点が異なります。 Baiscally、CLRは次のことを行います。
- モジュール内の型にアクセスする前にモジュールコンストラクターを実行します。
- 実行中は、モジュールコンストラクターによって直接アクセスされるタイプのみがロードされることを保証します
- コンストラクターが終了するまで、モジュールの外部のコードがそのメンバーにアクセスすることを許可しません。
クラスライブラリと実行可能ファイルの両方を含むすべてのアセンブリに対してこれを行い、for EXEは、Mainメソッドを実行する前にモジュールコンストラクターを実行します。
詳細については、ブログ投稿をご覧ください。コンストラクタ。
いずれにせよ、私の問題を完全に解決するにはいくつかの要素が必要です:
-
<!> quot; language runtime dll <!> quot;内で定義される次のクラス定義は、コンパイラによって生成されるすべてのアセンブリによって参照されます(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; } } }
クラスは、構築された型が作成される動的アセンブリへの参照を保持するシングルトンを定義します。また、<!> quot; hash set <!> quot;も保持します。既に動的に生成されたタイプのセットを保存し、最後にタイプを定義するために使用できるメンバーを定義します。この例は、生成されるクラスを定義するために使用できるSystem.Reflection.Emit.TypeBuilderインスタンスを返すだけです。実際のシステムでは、メソッドはおそらくクラスのAST表現を受け取り、それ自体を生成します。
-
次の2つの参照を発行するコンパイル済みアセンブリ(ILASM構文に表示):
.assembly extern $Runtime { .ver 0:0:0:0 } .assembly extern SharedLib { .ver 1:0:0:0 }
ここ<!> quot; SharedLib <!> quot; <!> quot; Loader <!> quot;を含む言語の定義済みランタイムライブラリです。上記で定義されたクラスと<!> quot; $ Runtime <!> quot;は、構築された型が挿入される動的ランタイムアセンブリです。
-
A <!> quot;モジュールコンストラクター<!> quot;言語でコンパイルされたすべてのアセンブリ内。
私が知る限り、ソースでモジュールコンストラクターを定義できる.NET言語はありません。 C ++ / CLIコンパイラーは、私が知っている唯一のコンパイラーです。 ILでは、これらは次のようになり、型定義の内部ではなく、モジュールで直接定義されます。
.method privatescope specialname rtspecialname static void .cctor() cil managed { //generate any constructed types dynamically here... }
私にとっては、これを機能させるためにカスタムILを記述する必要はありません。コンパイラを書いているので、コード生成は問題になりません。
型
tuple<x as double, y as double, z as double>
およびtuple<x as Foo>
を使用したアセンブリの場合、モジュールコンストラクターは次のような型を生成する必要があります(ここでは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; }
タプルクラスは、アクセシビリティの問題を回避するためにジェネリック型として生成されます。これにより、コンパイル済みアセンブリのコードで<=>を使用できます。Fooは非パブリックタイプです。
これを実行したモジュールコンストラクターの本文(ここでは1つの型のみを示し、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で提供することになっているもののタイプだと思います。まだ情報を入手するのは難しいですが、おそらくPDC08でさらに学習するでしょう。しかし、C#3ソリューションを待ち望んでいます...匿名型を使用していると思います。