44

.NET で「Java カスタム クラス ローダー」に相当するものを定義できるかどうかは誰にもわかりませんか?

少し背景を説明するには:

「Liberty」と呼ばれる、CLR を対象とする新しいプログラミング言語を開発中です。この言語の機能の 1 つは、「型コンストラクター」を定義する機能です。これは、コンパイル時にコンパイラーによって実行され、出力として型を生成するメソッドです。それらはジェネリックの一種の一般化であり (言語には通常のジェネリックが含まれています)、次のようなコードを ("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# の匿名型に似たものを提供します。

ただし、匿名型とは異なり、「タプル」には名前があり、パブリック メソッド シグネチャ内で使用できます。

これは、最終的にコンパイラによって出力される型を複数のアセンブリ間で共有できるようにする方法が必要であることを意味します。たとえば、私はしたいです

tuple<x as int>tuple<x as int>アセンブリ A で定義され、最終的にアセンブリ 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 タプル フィールドからのアクセスを 、 、および にリダイレクトField1Field2ますField3

ただし、それは実際には実行可能なオプションではありません。これは、コンパイル時tuple<x as int>tuple<y as int>は と が異なる型になることを意味しますが、実行時には同じ型として扱われます。これは、等価性や型の同一性などに多くの問題を引き起こします。それは私の好みにはあまりにも抽象的すぎます。

他の可能なオプションは、「ステート バッグ オブジェクト」を使用することです。ただし、状態バッグを使用すると、言語で「型コンストラクター」をサポートするという目的全体が無効になります。「カスタム言語拡張機能」を有効にして、コンパイラが静的な型チェックを実行できるコンパイル時に新しい型を生成できるようにするという考え方があります。

Java では、カスタム クラス ローダーを使用してこれを行うことができます。基本的に、タプル型を使用するコードは、実際にディスク上で型を定義しなくても発行できます。次に、実行時にタプル型を動的に生成するカスタム「クラス ローダー」を定義できます。これにより、コンパイラ内で静的な型チェックが可能になり、コンパイルの境界を越えてタプル型が統一されます。

ただし、残念ながら、CLR はカスタム クラスの読み込みをサポートしていません。CLR でのすべての読み込みは、アセンブリ レベルで行われます。「構築された型」ごとに個別のアセンブリを定義することは可能ですが、それはすぐにパフォーマンスの問題につながります (型が 1 つしかないアセンブリが多数あると、リソースが多すぎます)。

だから、私が知りたいのは:

.NET で Java クラス ローダーのようなものをシミュレートして、存在しない型への参照を発行し、それを使用する必要があるコードを実行する前に、実行時にその型への参照を動的に生成することはできますか?

ノート:

*実際には、質問への回答は既に知っています。以下に回答として提供します。ただし、解決策を見つけるために、約 3 日間の調査と、かなりの IL ハッキングが必要でした。他の誰かが同じ問題に遭遇した場合に備えて、ここに文書化することをお勧めします. *

4

2 に答える 2

52

答えはイエスですが、解決策は少しトリッキーです。

名前System.Reflection.Emit空間は、アセンブリを動的に生成できる型を定義します。また、生成されたアセンブリを段階的に定義することもできます。つまり、動的アセンブリに型を追加し、生成されたコードを実行してから、アセンブリにさらに型を追加することができます。

このSystem.AppDomainクラスは、フレームワークがアセンブリの読み込みに失敗したときに発生するAssemblyResolveイベントも定義します。そのイベントのハンドラーを追加することにより、すべての「構築された」型が配置される単一の「ランタイム」アセンブリを定義できます。構築された型を使用するコンパイラによって生成されたコードは、ランタイム アセンブリ内の型を参照します。ランタイム アセンブリは実際にはディスク上に存在しないため、AssemblyResolveイベントは、コンパイルされたコードが構築された型に初めてアクセスしようとしたときに発生します。次に、イベントのハンドルが動的アセンブリを生成し、それを CLR に返します。

残念ながら、これを機能させるにはいくつかのトリッキーなポイントがあります。最初の問題は、コンパイルされたコードが実行される前にイベント ハンドラーが常にインストールされるようにすることです。コンソール アプリケーションを使用すると、これは簡単です。イベント ハンドラーを接続するMainコードは、他のコードを実行する前にメソッドに追加するだけです。ただし、クラス ライブラリの場合、main メソッドはありません。dll は、別の言語で記述されたアプリケーションの一部として読み込まれる可能性があるため、イベント ハンドラー コードを接続するためのメイン メソッドが常に存在するとは限りません。

2 番目の問題は、それらを参照するコードが使用される前に、参照される型がすべて動的アセンブリに挿入されるようにすることです。System.AppDomainクラスはまた、TypeResolveCLR が動的アセンブリ内の型を解決できないときに実行されるイベント。これにより、イベント ハンドラーは、それを使用するコードが実行される前に、動的アセンブリ内で型を定義する機会が与えられます。ただし、この場合、そのイベントは機能しません。CLR は、参照先のアセンブリが動的に定義されている場合でも、他のアセンブリによって "静的に参照されている" アセンブリのイベントを発生させません。これは、コンパイルされたアセンブリ内の他のコードが実行される前にコードを実行し、必要な型がまだ定義されていない場合はランタイム アセンブリに動的に挿入する方法が必要であることを意味します。そうしないと、CLR がそれらの型を読み込もうとしたときに、必要な型が動的アセンブリに含まれていないことがわかり、型読み込み例外がスローされます。

幸いなことに、CLR は両方の問題に対する解決策を提供します: モジュール初期化子です。モジュール初期化子は、単一のクラスだけでなくモジュール全体を初期化することを除いて、「静的クラス コンストラクター」と同等です。基本的に、CLR は次のことを行います。

  1. モジュール内の型にアクセスする前に、モジュール コンストラクターを実行します。
  2. モジュール コンストラクターによって直接アクセスされる型のみが実行中に読み込まれることを保証する
  3. コンストラクターが終了するまで、モジュールの外部のコードがそのメンバーにアクセスできないようにします。

これは、クラス ライブラリと実行可能ファイルの両方を含むすべてのアセンブリに対して行われ、EXE に対しては、Main メソッドを実行する前にモジュール コンストラクターが実行されます。

コンストラクターの詳細については、このブログ投稿を参照してください。

いずれにせよ、私の問題を完全に解決するには、いくつかの要素が必要です。

  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;
            }
        }
    }
    

    このクラスは、構築された型が作成される動的アセンブリへの参照を保持するシングルトンを定義します。また、既に動的に生成された型のセットを格納する「ハッシュ セット」も保持し、最後に次のことができるメンバーを定義します。タイプを定義するために使用されます。この例では、生成されるクラスを定義するために使用できる System.Reflection.Emit.TypeBuilder インスタンスを返すだけです。実際のシステムでは、メソッドはおそらくクラスの AST 表現を取り込んで、それ自体を生成するだけです。

  2. 次の 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 はパブリックでない型でした。

    これを行うモジュール コンストラクターの本体 (ここでは 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 でカスタム クラス ローダーにほぼ相当する機能を実現するために思いついたメカニズムです。

これを行う簡単な方法を知っている人はいますか?

于 2008-10-09T03:39:09.350 に答える
-5

これは、DLR が C# 4.0 で提供することになっているタイプのものだと思います。まだ情報を入手するのは難しいですが、おそらく PDC08 でさらに詳しく知ることができます。ただし、C# 3 のソリューションが公開されるのを待ち望んでいます...匿名型を使用していると思います。

于 2008-10-09T03:38:42.563 に答える