C#と諸々

C#がメインで他もまぁ諸々なブログです
おかしなこと書いてたら指摘してくれると嬉しいです(´・∀・`)
つーかコメント欲しい(´・ω・`)

2008/05/18 00:09
前置き長いですが、オブジェクトの型を破壊的に変換 の続きです。


ガベージコレクションを開始する際、CLR はまず、全てのスレッドを一時停止します。そして、それぞれのスレッドが実行中だったコードを確認し、「セーフポイント」に到達しているかどうかを調べます。
セーフポイントに到達しているスレッドは、ガベージコレクションが完了するまでそのまま一時停止にしておいて問題ありません。
セーフポイントに到達していないスレッドはそのまま一時停止にしておくわけにはいきません。しかし、ガベージコレクションの実行中は全てのスレッドを一時停止 しておく必要があります。そのため CLR は、スレッドの乗っ取りを行おうとします。簡単に言うと、現在実行中のメソッドの戻り先を書き換えてスレッドを再開させます。これにより、メソッドの完了後に特殊な関数を実行するようにし、この特殊な関数内でスレッドを一時停止するわけです。
ただし、乗っ取りを行おうとしてから 250 ミリ秒経過しても乗っ取れなかった (=メソッドが未完了) 場合、CLR は再びスレッドを一時停止し、再びセーフポイントに到達しているかどうかを調べます。到達している場合は、そのまま一時停止します。到達していない場合はスレッドの再開となりますが、前回乗っ取りを行おうとした時とは違うメソッドを実行していた場合は、再開前にそのメソッドの戻り先を書き換えます。再開後数ミリ秒待機しても乗っ取れなかった場合は、再びこの動作を繰り返します。

セーフポイントはコストが高いため、必要最低限の箇所にしか用意されません。具体的には、メソッド呼び出しを含まないループ構造にのみ用意されます。メソッド呼び出しを含まないループ構造では、いつまで経っても乗っ取りが出来ない可能性があるためです。


と、ここまでが前置きです。
これらを踏まえると、オブジェクトの型を破壊的に変換 のメソッドは、アンマネージポインタを取得してから型ハンドルの書き換えを行うまでの間にメソッド呼び出しを行わないようにすれば、ガベージコレクションによって問題を引き起こすことがなくなると思わます。
アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間のメソッド呼び出しは、Type.TypeHandle プロパティの呼び出しと、RuntimeTypeHandle.Value プロパティの呼び出しのみです。つまり、newType 引数から型ハンドルを取得する処理をアンマネージポインタ取得処理より前で行うように修正すれば良いわけです。

/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
    // 型ハンドルを取得
    IntPtr newTypeHandle = newType.TypeHandle.Value;
    // 対象オブジェクトに対する GC ハンドルを生成
    GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
    try
    {
        // GC ハンドルの実際のエントリへのポインタを取得
        void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
        // エントリにはオブジェクトへの参照が格納されている
        void* targetPointer = *((void**)entryPointer);

        // オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
        IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
        // 型ハンドルを書き換えることで型を強制的に変換する
        *typeHandlePointer = newTypeHandle;
    }
    finally
    {
        // GC ハンドルの解放
        targetGCHandle.Free();
    }
}

これで、アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動することはなくなったはずです。

次の検証用コードで検証した結果、問題ありませんでした。

static void Main(string[] args)
{
    bool run = true;
    Action action =
        delegate
        {
            while (run)
            {
                object o = new object();
                Console.WriteLine("collect.");
                GC.Collect();
            }
        };
    action.BeginInvoke(null, null);

    while (true)
    {
        object o = new object();
        ConvertType(o, typeof(void));
        Console.WriteLine(o.GetType());
        if (o.GetType() != typeof(void))
        {
            Console.WriteLine("failed.");
            run = false;
            break;
        }
    }

    Console.ReadLine();
}

ちなみに、修正前のメソッドで検証してみても、問題は中々発生しません。簡単に発生させるためには、オブジェクトの型ハンドルを書き換えている箇所にブレークポイントを貼ってデバック実行します。ブレークと再開を何回か繰り返せば簡単に発生します。
もちろん、修正後のメソッドならこの方法で検証しても問題が発生しません。
スポンサーサイト



2008/05/15 23:24
PowerShell でメソッドの引数に列挙値を指定する時に文字列で指定できるってのは知っていたけど、ビットフィールドの列挙値の組み合わせは文字列で指定できないと思い込んでた。でも、実はカンマ区切りで繋げて記述すればいけるってことに今更気付いた。

[Object].GetMethods("Public, NonPublic, Instance") | % { $_.ToString(); };


これは知らないと損だなぁ。
タグ: .NET PowerShell
2008/05/11 01:53
オブジェクトは型をインスタンス化したものです。オブジェクトは必ず一つの型と結びついています。オブジェクトに結びついている型は、Object.GetType メソッドにて取得することができます。
通常、オブジェクトに結びついている型を変更することはできません。例えば String オブジェクトを Object クラスにキャストしても、オブジェクト自体は String オブジェクトのままです。

オブジェクトはメモリ上のデータです。このデータには、オブジェクトヘッダーと各フィールドの値が含まれます。オブジェクトヘッダーには、同期ブロックインデックスと型ハンドルが含まれます。
この型ハンドルが、オブジェクトと結びついている型を示します。型ハンドルは、ローダーヒープ (アプリケーションドメイン内にロードされた型情報を格納している領域) 内にある型情報 (Type クラスのインスタンスとは別物) へのポインタです。Object.GetType メソッドは、この型ハンドルを元に Type クラスのインスタンスを取得して返却しています。
つまり、オブジェクトヘッダー内の型ハンドルを書き換えると、オブジェクトに結びついている型を変換することができるわけです。

オブジェクトヘッダーへの直接のアクセスはサポートされていません。ただし、アンマネージドコードを用いれば、アンマネージポインタからメモリを直接参照することができますので、これでアクセスすることが可能です。ちなみに、オブジェクトの参照 (マネージポインタ) は、オブジェクトヘッダー内の型ハンドルのアドレスを示すようになっています。

下記のメソッドは、オブジェクトに結びついている型を変換します。(ビルドするにはアンセーフコードの許可が必要です。)

/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
    // 対象オブジェクトに対する GC ハンドルを生成
    GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
    try
    {
        // GC ハンドルの実際のエントリへのポインタを取得
        void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
        // エントリにはオブジェクトへの参照が格納されている
        void* targetPointer = *((void**)entryPointer);

        // オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
        IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
        // 型ハンドルを書き換えることで型を強制的に変換する
        *typeHandlePointer = newType.TypeHandle.Value;
    }
    finally
    {
        // GC ハンドルの解放
        targetGCHandle.Free();
    }
}


以下は使用例です。

static void Main(string[] args)
{
    object obj = new object();

    Console.WriteLine("obj is a {0}", obj.GetType());
    ConvertType(obj, typeof(IDisposable));
    Console.WriteLine("obj is a {0}", obj.GetType());

    Console.ReadLine();
}


以下は実行結果です。

obj is a System.Object
obj is a System.IDisposable


このメソッドを使えば、オブジェクトに結びついている型をどんな型にでも変換できます。抽象クラスやインターフェイスに変換することもできます。ただし、その結果予期せぬ動作を引き起こす可能性があります。例えば、変換前の型より変換後の型の方がサイズが大きいと、他のオブジェクトを破壊してしまう場合があります。

また、このメソッドには一つ欠点があります。オブジェクトへのポインタを取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動されてしまうと、型変換が正常に行われず、全く無関係なデータを型ハンドルで上書きしてしまう可能性があります。
Blittable 型と呼ばれる型に関しては、GC 時にオブジェクトが移動してしまわないよう固定化することができますが、非 Blittable 型では固定化ができません。
この問題を解決する方法は不明 こちら です。


言うまでもないと思いますが、これはとても危険でトリッキーなメソッドです。普通はまず使うことのないメソッドです。
このメソッドで何か面白いことができたら、是非教えてください。
タグ: .NET C# CLR