2012年2月26日日曜日

AppDomain、その幻想をぶち殺す - How to use unmanaged code as Illusion Killer against CLR -

前回の積み残しです。

AppDomain は便利な仕組みなんですが、なにぶんなんでもかんでも分離してしまうため、ちょっと融通が利かないところがあったりします。
前回の記事の終わりにも「パズルの最後の 1 ピース」みたいな書き方をしましたが、アンマネージコードの力を借りればなんとかなるだろうということを感じてはいつつ、それを具体的に実現する方法を公表されている方は、今現在もいらっしゃらないようでした。

私が探せていないだけだろうとは思うのですが・・・。Google 先生、私にはいつも厳しいんですよね (>_<)
仕方がないですので、これまで通り、つっこみどころ満載な自己流解決法で行きましょう!

というわけで、ゆるめの AppDomain 攻略講座、はっじまっるよー。

※文中にあるソリューションは以下のリンクからダウンロードできます。ビルド環境は、Visual Studio C++ 2010、Visual Studio C# 2010、Boost C++ Libraries Ver.1.48.0、C++ 側の自動テストに Google Test 1.6.0、あと C# 側の自動テストに NUnit 2.5.10.11092 を使っています。環境を整える際は、各ライブラリのインストールをお願いしますです。


こちらのページ/ソフトウェアを参考にさせていただきました。いつもお世話になり、頭が下がるばかりです<(_ _)>
Download Details - Microsoft Download Center - Shared Source Common Language Infrastructure 2.0 Release
Cecil - Mono
ECMA C# and Common Language Infrastructure Standards
Microsoft.ExtendedReflection
NUnit - Home
技術/Windows/メモリダンプ取得方法メモ - Glamenv-Septzen.net
How to use SafeHandle in a Resilient Library - NyaRuRuの日記
PEフォーマットを解釈せよ! - @IT
Detours - Microsoft Research
Debugging .Net framework source code within Windbg ≪ Naveen's Blog
SOSEX - A New Debugging Extension for Managed Code - Steve's Techspot
Need way to invoke full manual JIT on assembly | Microsoft Connect
C# によるプログラミング入門
ReSharper:: The Most Intelligent Extension for Visual Studio





目次

融通が利かないところ
前回の例にも挙げた通り、.NET 開発者でも、特にレガシーコードと戦う方々には、AppDomain の仕組みや目的を知っていただき、活用できるようになることは、私は非常に意義があることだと考えています。ただ、使い始めてすぐに気付く不便な部分があることも事実です。いくつか例を挙げてみましょう。


例 1: 必要な情報がすでに別の情報と紐づけられている
なんらかの自動化された Unit Test を書かれている方にはおなじみと思われる NUnit。GUI ツールがついててわかりやすいですよね。
コマンドラインで動きさせすれば、CI 環境には事足りるとは言え、やっぱり使い方が直観的にわかる GUI ツールがあるということは、導入には欠かせません。自分専用にカスタマイズできない職場の PC じゃ・・・ということで、マウス一筋派/どちらかというとマウス派の方も少なくないかと思いますし。

さてさて。Assert で出力値をチェックできているとは言え、実際に出力値を標準出力に出して見てみたいという場面もあるでしょう。
コマンドラインで動かす場合はともかく、NUnit の GUI ツールで確認する場合はどうしてるの?というわけですが、[Text Output] というタブがあり、ちゃんと標準出力がリダイレクトされて出てきます。
#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class ConsoleTest
    {
        [Test]
        public void Test()
        {
            Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
        }
    }
}
 


ただ、前回解説した AppDomainMixin.RunAtIsolatedDomain を使うと・・・。
#line 59 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class ConsoleTest
    {
        [Test]
        public void Test()
        {
            Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);

            AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
            {
                Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
            });

            Console.WriteLine("AppDomain: {0}", AppDomain.CurrentDomain.FriendlyName);
        }
    }
}
 


死ーん・・・
・・・2 つ目の WriteLine はどこに行った?(゚Д゚;≡;゚д゚)

ソースコードを読むとわかるのですが、NUnit の GUI ツールでは、テスト実行前に Console.SetOut を呼び出し、[Text Output] タブに出力するためのオリジナル TextWriter、EventListenerTextWriter に差し替え、標準出力をフックするようにしています。
で、Console.SetOut は何をやっているかというと、中で持ってる static メンバを、渡された TextWriter に入れ替えるんですね。
環境を分離するのが AppDomain の目的、というわけで、この依存もきれいさっぱり切り離してくれたわけでした。

たぶん、この例に限らず、ある一部の依存関係だけは持ち込みたいっていう場合は往々にしてあるかと思います。Console.Out の型である TextWriter などは、幸い MarshalByRefObject を継承していますので、引数から引き回せば良いのですが・・・。


例 2: 既存のライブラリ、標準のクラスが AppDomain 越えに対応していない
前述の TextWriter や、自分たちで手をいれているものでしたら、MarshalByRefObject や SerializableAttribute で修飾すれば良いのですが、困るのが既存のライブラリや標準の型。

ところで、AppDomain の作成というのは、いくら Process の作成より軽いといっても、Assembly の再ロードが発生するわけですから、それなりにコストがかかります。ここで、どのぐらいかかるのか測ってみましょう。あ・・・AppDomain 別にすると出力出なくなっちゃいましたっけ。とりあえず引数で引き回すことにしましょうか (^_^;)
#line 57 "CppTroll\ProfilingApiSample04FrameworkTest\StopwatchTest.cs"
using System;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class StopwatchTest
    {
        [Test]
        public void Test()
        {
            using (var sw = new StringWriter())
            {
                var stopwatch = new Stopwatch();
                stopwatch.Restart();

                sw.WriteLine("Elapsed: {0} ms", stopwatch.ElapsedMilliseconds);

                AppDomain.CurrentDomain.RunAtIsolatedDomain<StringWriter, Stopwatch>((sw_, stopwatch_) =>
                {
                    sw_.WriteLine("Elapsed: {0} ms", stopwatch_.ElapsedMilliseconds);
                }, sw, stopwatch);

                sw.WriteLine("Elapsed: {0} ms", stopwatch.ElapsedMilliseconds);

                Console.WriteLine(sw.ToString());
            }
        }
    }
}
 
時間を計ると言えば Stopwatch です。AppDomainMixin.RunAtIsolatedDomain は引数を受け取れるよう拡張しました。さて、これを実行すると・・・。


ああああああ (´Д`;)

Stopwatch の定義は・・・と見ると、見事になにも修飾されていないですね (T_T)
こういう場合、正攻法で行くなら、利用したいクラスの I/F をそっくりラップした MarshalByRefObject を作成することになるかと思いますが・・・いやはや、めんどうです。Code DOM や、T4 などを利用し、自動生成をする仕組みを作ってもよいのですが、こういうクラスが見つかるたびにラッパを作る必要がある、というのではなかなか気軽にはできません。ぐぬぬ・・・。


例 3: コンパイラが対象のクラスを自動生成する
もっともわかりにくいのがこのパターンです。

前回紹介し、今回も登場している AppDomainMixin.RunAtIsolatedDomain ですが、引数に引き渡される action について、static なメソッドのみを許していたのを、お気づきになられた方もいらっしゃったかもしれません。
今回のサンプルでは AppDomain を越えられる型であれば引数に渡せるようにしたりして、若干拡張していますがチェックは同様に行っています。再掲しましょう。
#line 107 "CppTroll\ProfilingApiSample04Framework\Mixin\System\AppDomainMixin.cs"
・・・
        static void RunAtIsolatedDomain(Evidence securityInfo, 
                            AppDomainSetup info, Delegate action, params object[] args)
        {
            if (action == null)
                throw new ArgumentNullException("action");
            
            if (!action.Method.IsStatic)
                throw new ArgumentException(
                          "The parameter must be designated a static method.", "action");

            
            var domain = default(AppDomain);
            try
            {
                domain = AppDomain.CreateDomain("Domain " + action.Method.ToString(),
                                               securityInfo, info);
                var type = typeof(MarshalByRefRunner);
                var runner = (MarshalByRefRunner)domain.CreateInstanceAndUnwrap(
                                                  type.Assembly.FullName, type.FullName);
                runner.Action = action;
                runner.Run(args);
            }
            catch (SerializationException e)
            {
                throw new ArgumentException("The parameter must be domain crossable. " +
                          "Please confirm that the type inherits MarshalByRefObject, " +
                          "or it is applied SerializableAttribute.", e);
            }
            finally
            {
                try
                {
                    if (domain != null)
                        AppDomain.Unload(domain);
                }
                catch { }
            }
        }
・・・
 
114 行目にある if 文で、メソッドが static でなければ ArgumentException をスローするようにしていますね。

ここで、デリゲートに関する薀蓄を一つ。
デリゲートは、作ると自動的に Delegate クラスを継承したクラスが生成されるのですが、Delegate クラス自体は MarshalByRefObject ではなく、SerializableAttribute が適用されており、同時に ISerializable を実装していることをご存じでしたでしょうか?
つまり、デリゲートは AppDomain を越えるときにコピーされます。従って、そのメンバとして保持される Target、Method も AppDomain を越えられる型でなくてはなりません。
Method は MethodInfo(RuntimeMethodInfo)ですので特に問題はないですが、Target となる型は要注意です。通常は、デリゲートに渡しているものが、参照透過なラムダ式か、外部の環境を取り込んだクロージャなのか、なんて気にすることはないと思いますが・・・。
args で渡される引数を全て調べきるのは効率が悪いため、SerializationException を一括して処理し、引数に原因があることにしていますが(130 行目)、ここにデリゲートを指定して試してみましょう。
こんなソースコードを・・・
#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class DelegateTest
    {
        [Test]
        public void Test()
        {
            var adder = default(Func<int, int, int>);
            adder = (x, y) => x + y;    // Referencial transparent lambda
            AppDomain.CurrentDomain.RunAtIsolatedDomain<Func<int, int, int>>(adder_ =>
            {
                Assert.AreEqual(2, adder_(1, 1));
            }, adder);
        }
    }
}
 


こんな風に書き換えれば・・・
#line 83 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class DelegateTest
    {
        [Test]
        public void Test()
        {
            var adder = default(Func<int, int, int>);
            var z = 1;
            adder = (x, y) => x + y + z;    // To closure(capture local variable z)
            AppDomain.CurrentDomain.RunAtIsolatedDomain<Func<int, int, int>>(adder_ =>
            {
                Assert.AreEqual(3, adder_(1, 1));
            }, adder);
        }
    }
}
 


オワタ\(^o^)/

Resharper のように、このような気付きにくい動作に対して警告してくれるアドオンもあるようですが、未導入の現場では、標準の ildasm 等、外部の逆アセンブラで確認するしかありません。
AppDomainMixin.RunAtIsolatedDomain では、このような動きを気に病む必要がないように、基本的に action に指定するデリゲートについては、static なメソッドしか許さないようにしたのでした(static なメソッドの場合、Target は必ず null になるため)。
もちろん、こちらもラッパを作れば動作の解決にはなるのですが、元々処理の流れ的にその場所にしか使われないし、使われたくもないがためにラムダ式を使っているのに、そんな解決策をとってしまっては本末転倒です。

そうそう、よく C# と比較に挙がる Java や C++ には、こういう「局所的だが状態を持つカプセル化された処理」を作る機能として局所クラスっていうのがあるんですよね。なんでそこパkr・・・インスパイアしなかったし (^_^;)




攻略準備
最後の例などは C# 初学者には意味が分からない恐怖の対象でしょう。LINQ 怖い。AppDomain 怖い。
AppDomain みたいなよくわからないものを使わない、のももちろんありです。要は初めからこんな工夫が必要ないように、保守しやすい/拡張しやすい実装を行えば良いだけですから。難しいようであれば、その旨ちゃんと上長やお客さんに伝えて、交渉してみるのも手かと思います。

まあ今回は、アンマネージの力をちょっとだけ借りて、境界チェックがゆるめな AppDomain 越えアクセッサを作ることで、前述の問題を解決してみます。毎度のことながら話の都合上ですが、そこは発信者特権ということで (^_^;)

さて、本題に入る前にマネージコードとアンマネージコードが連携するための準備をしましょう。


関数ポインタ
唐突ですが、関数ポインタの話です。
CLI では、全てのプログラミング言語は、まずメタデータ + IL という中間形式に変換され、それから実行時コンパイルされネイティブコードに変換されることになっています。MS の実装でそれを担うのが CLR の JIT 処理になるのですが、皆さんは CLR が何を使ってマネージコードからネイティブコードへの呼び出しを紐付けているのかご存じでしょうか。
私もつい最近まで知らなかったのですが、以下の書籍に詳細な流れが書き込まれていることを Twitter で知り、即注文しました。
Amazon.co.jp: Essential .NET ― 共通言語ランタイムの本質: ドン・ボックス, クリス・セルズ, Don Box, Chris Sells, 吉松 史彰: 本

英語版だったら、Google Book から冒頭部分を立ち読みできますね。
Essential.NET: The common language runtime - Don Box, Chris Sells - Google ブックス

結論から言うと、それにはネイティブコードへの関数ポインタが使われます。
同一プロセス上であれば、あるネイティブコードへの関数ポインタは基本変わることは無いですから、これを直接参照すれば AppDomain のような疑似的な境界は関係なくなるわけですね。

それでは、ここで、JIT の流れを確認しておきましょう。こんなプログラムがあったとすると、
class Program
{
    static void Main(string[] args)
    {
        A.DoSomething();
        A.DoSomething();
        B.DoSomething();
    }
}

class A
{
    public static void DoSomething() 
    { 
    }
}

class B
{
    public static void DoSomething()
    {
        A.DoSomething();
    }
}
 
JIT は AppDomain 毎に以下の順序で行われます。
  1. Main(エントリポイントとして呼ばれた時)
  2. A.DoSomething(5 行目に呼ばれた時)
  3. B.DoSomething(7 行目に呼ばれた時)

最初に実行されるまで、JIT は行われないというのがミソです。名前の通りと言えば名前の通りなのですが、実装は素直にはできません。
なぜなら、あるメソッドを JIT する際、そこから呼び出されているメソッドの呼び先を確定させなければ、ネイティブコードが作成できないからです。呼び先のネイティブコードはまだできあがっていないのに!
上記の例で言うと、Main から呼ばれている A.DoSomething は、Main の JIT が行われる時にはまだネイティブコードができていないため、call 命令(ネイティブ)のオペランドに指定する関数ポインタが決まりません。

CLR でこれをどのように解決しているかというと、スタブを介してネイティブコードへの関数ポインタにアクセスする形を取るようにしています。
call 命令(ネイティブ)に渡すアドレスに、一先ずスタブの関数ポインタを指定するのです。スタブは、そのメソッドのネイティブコードができているか確認し、できていなければ JIT します。そして、JIT したネイティブコードへの関数ポインタを呼び出します。
疑似コードで書くとこんな感じでしょうか。

1. の JIT 後
class Program
{
    static void (*MainNativePtr)(string[] args) = NULL;

    static void MainStub(string[] args)
    {
        if (MainNativePtr == NULL)
            MainNativePtr = Runtime.CreateNativeCode();
        MainNativePtr();
    }

    static void MainNative(string[] args)
    {
        A.DoSomethingStub();
        A.DoSomethingStub();
        B.DoSomethingStub();
    }

    static void Main(string[] args)
    {
        A.DoSomething();
        A.DoSomething();
        B.DoSomething();
    }
}

class A
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub() 
    { 
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }

    public static void DoSomething() 
    { 
    }
}

class B
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub()
    {
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }

    public static void DoSomething()
    {
        A.DoSomething();
    }
}
 
いきなりダイナミックに変わりましたが、まずは Main メソッドのネイティブコード(MainNative: 12 行目)から呼び出すスタブが一気に作成されるということが伝わればと思います。まだ、A.DoSomething や B.DoSomething に対応するネイティブコードは作成されていないことが確認できます。
そして、2. の JIT 後です。
class Program
{
    static void (*MainNativePtr)(string[] args) = NULL;

    static void MainStub(string[] args)
    {
        if (MainNativePtr == NULL)
            MainNativePtr = Runtime.CreateNativeCode();
        MainNativePtr();
    }

    static void MainNative(string[] args)
    {
        A.DoSomethingStub();
        A.DoSomethingStub();
        B.DoSomethingStub();
    }

    static void Main(string[] args)
    {
        A.DoSomething();
        A.DoSomething();
        B.DoSomething();
    }
}

class A
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub() 
    { 
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }
    
    public static void DoSomethingNative() 
    { 
    }

    public static void DoSomething() 
    { 
    }
}

class B
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub()
    {
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }

    public static void DoSomething()
    {
        A.DoSomething();
    }
}
 
Main メソッドのネイティブコードから、A.DoSomethingStub が呼ばれることで(14 行目)、A.DoSomething メソッドのネイティブコードが作成されます(A.DoSomethingNative: 38 行目)。
さらに処理が進み、3. の JIT が行われると、
class Program
{
    static void (*MainNativePtr)(string[] args) = NULL;

    static void MainStub(string[] args)
    {
        if (MainNativePtr == NULL)
            MainNativePtr = Runtime.CreateNativeCode();
        MainNativePtr();
    }

    static void MainNative(string[] args)
    {
        A.DoSomethingStub();
        A.DoSomethingStub();
        B.DoSomethingStub();
    }

    static void Main(string[] args)
    {
        A.DoSomething();
        A.DoSomething();
        B.DoSomething();
    }
}

class A
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub() 
    { 
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }
    
    public static void DoSomethingNative() 
    { 
    }

    public static void DoSomething() 
    { 
    }
}

class B
{
    public static void (*DoSomethingNativePtr)() = NULL;
    
    public static void DoSomethingStub()
    {
        if (DoSomethingNativePtr == NULL)
            DoSomethingNativePtr = Runtime.CreateNativeCode();
        DoSomethingNativePtr();
    }
    
    public static void DoSomethingNative() 
    { 
        A.DoSomethingNative();
    }

    public static void DoSomething()
    {
        A.DoSomething();
    }
}
 
こんな感じになります。B.DoSomething のネイティブコード(B.DoSomethingNative: 58 行目)の作成時には、もう A.DoSomething のネイティブコードができているのがわかりますので、そのままネイティブコードへの関数ポインタを使うことができます。

「関数ポインタを直接参照すれば~」ということでしたが、これは、RuntimeMethodHandle.GetFunctionPointer で取得できます。
ただし、上記のようなことから、JIT 前と JIT 後で値が変わります。JIT 前はスタブへの参照、JIT 後はネイティブコードへの参照ということですね。これを知らないと、Domain 越えアクセッサを設計する際、「あるマネージメソッドを表す関数ポインタは Process で一つ」と勘違いして扱って嵌ります・・・はい。嵌った人間がこちらになります (ToT)
キーには Process で一意になるもの、例えば実行前のメタデータが持つような情報を扱うのが良いでしょう。


calli 命令(IL)
関数ポインタを直接呼び出すにはこの命令を使います。C++/CLI でネイティブな関数ポインタを呼び出すコードを記述すると、コンパイルされた IL に現れますね。
"i" は Indirect method call の "i" らしいです・・・ldftn 命令(IL)もそうですが、どうも略し方がよくわかりません (^_^;)
なにはともあれ、これは DynamicMethod 経由で利用します。気を付けることがあるとすれば、ILGenerator.Emit ではなく、ILGenerator.EmitCalli を使って打ち込む必要があることぐらいです。


CLR の型チェックタイミング
上述の書籍にもありますが、CLR があるオブジェクトについて、ある AppDomain に属しているかどうかをチェックするのは、マネージコードで AppDomain を越えようとした時だけです。
あとはこれに加え、型は AppDomain 毎に一意になりますので、AppDomain A で使っていたオブジェクトを AppDomain B で同じ型にキャストすることはできないことに注意する必要があります。もしこれを行おうとすれば、「ハンドルされていない例外: System.InvalidCastException: 型 'MyClass' のオブジェクトを型 'MyClass' にキャストできません。」のようなエラーになってしまいます。わかりにくいですね (-_-;) エラーメッセージに型が属す AppDomain の Friendly Name か何かを付加してくれればありがたかったのですが・・・。
まあ、上述の問題を解決するようなアクセッサとして利用するだけでしたらほとんど障害にはなりませんので、もし何かに応用される際はちょっと気に留めておいていただければと思います。




問・題・解・決!
準備が整いましたのでさくっと実装しましょう!
まずは、関数ポインタを保存しておくリポジトリから。
#line 1 "CppTroll\ProfilingApiSample04\InstanceGetters.h"
#pragma once

#ifndef INDIRETIONINTERFACES_H
#define INDIRETIONINTERFACES_H

#ifdef URASANDESU_PRIG_EXPORTS
#define URASANDESU_PRIG_API __declspec(dllexport)
#else
#define URASANDESU_PRIG_API __declspec(dllimport)
#endif

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryAdd(LPCWSTR key, void const *pFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryGet(LPCWSTR key, void const **ppFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryRemove(LPCWSTR key, void const **ppFuncPtr);
EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(VOID) InstanceGettersClear();

#endif  // #ifndef INDIRETIONINTERFACES_H
 
#line 1 "CppTroll\ProfilingApiSample04\InstanceGetters.cpp"
#include "StdAfx.h"
#include "InstanceGetters.h"
#include "GlobalSafeDictionary.h"

typedef GlobalSafeDictionary<std::wstring, void const *> InstanceGetters;

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryAdd(LPCWSTR key, void const *pFuncPtr)
{
    InstanceGetters &ing = InstanceGetters::GetInstance();
    return ing.TryAdd(std::wstring(key), pFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryGet(LPCWSTR key, void const **ppFuncPtr)
{
    _ASSERTE(ppFuncPtr != NULL);
    InstanceGetters &ing = InstanceGetters::GetInstance();
    return ing.TryGet(std::wstring(key), *ppFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(BOOL) InstanceGettersTryRemove(LPCWSTR key, void const **ppFuncPtr)
{
    _ASSERTE(ppFuncPtr != NULL);
    InstanceGetters &ing = InstanceGetters::GetInstance();
    return ing.TryRemove(std::wstring(key), *ppFuncPtr);
}

EXTERN_C URASANDESU_PRIG_API STDMETHODIMP_(VOID) InstanceGettersClear()
{
    InstanceGetters &ing = InstanceGetters::GetInstance();
    ing.Clear();
}
 
マネージ側との I/F になる InstanceGetters** らです。解説するまでもなく、ただのガワですね・・・(^_^;)
本処理は GlobalSafeDictionary に任せます。
#line 1 "CppTroll\ProfilingApiSample04\GlobalSafeDictionary.h"
#pragma once
#ifndef GLOBAL_SAFE_DICTIONARY_H
#define GLOBAL_SAFE_DICTIONARY_H

template<
    typename Key, 
    typename Value, 
    typename Hash = boost::hash<Key>, 
    typename Pred = std::equal_to<Key>, 
    typename Alloc = std::allocator<std::pair<Key const, Value>> 
> 
class GlobalSafeDictionary : boost::noncopyable
{
public:
    typedef typename boost::call_traits<Key>::param_type in_key_type;
    typedef typename boost::call_traits<Value>::param_type in_value_type;
    typedef typename boost::call_traits<Value>::reference out_value_type;

    static GlobalSafeDictionary &GetInstance()
    {
        static GlobalSafeDictionary im;
        return im;
    }

    BOOL TryAdd(in_key_type key, in_value_type value)
    {
        m_lock.Lock();
        BOOST_SCOPE_EXIT((&m_lock))
        {
            m_lock.Unlock();
        }
        BOOST_SCOPE_EXIT_END


        if (m_map.find(key) == m_map.end())
        {
            m_map[key] = value;
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }

    BOOL TryGet(in_key_type key, out_value_type rValue)
    {
        m_lock.Lock();
        BOOST_SCOPE_EXIT((&m_lock))
        {
            m_lock.Unlock();
        }
        BOOST_SCOPE_EXIT_END


        if (m_map.find(key) == m_map.end())
        {
            return FALSE;
        }
        else
        {
            rValue = m_map[key];
            return TRUE;
        }
    }

    BOOL TryRemove(in_key_type key, out_value_type rValue)
    {
        m_lock.Lock();
        BOOST_SCOPE_EXIT((&m_lock))
        {
            m_lock.Unlock();
        }
        BOOST_SCOPE_EXIT_END


        if (m_map.find(key) == m_map.end())
        {
            return FALSE;
        }
        else
        {
            rValue = m_map[key];
            m_map.erase(key);
            return TRUE;
        }
    }

    void Clear()
    {
        m_lock.Lock();
        BOOST_SCOPE_EXIT((&m_lock))
        {
            m_lock.Unlock();
        }
        BOOST_SCOPE_EXIT_END

        
        m_map.clear();
    }

private:
    GlobalSafeDictionary() { }
    ATL::CComAutoCriticalSection m_lock;
    boost::unordered_map<Key, Value, Hash, Pred, Alloc> m_map;
};

#endif  // #ifndef GLOBAL_SAFE_DICTIONARY_H
 
GlobalSafeDictionary も大したことはしていなくて、boost::unordered_map をシングルトンかつスレッドセーフにラップしただけのものです。Boost.ScopeExit は Critical Section を Lock/Unlock するにも便利に使えますね。アンマネージコードはこれだけです。続いてマネージ側へ。
#line 1 "CppTroll\ProfilingApiSample04Framework\InstanceGetters.cs"
using System;
using System.Runtime.InteropServices;

namespace ProfilingApiSample04Framework
{
    public static class InstanceGetters
    {
        [DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryAdd")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool TryAdd([MarshalAs(UnmanagedType.LPWStr)] string key, IntPtr pFuncPtr);

        [DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryGet")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool TryGet([MarshalAs(UnmanagedType.LPWStr)] string key, out IntPtr ppFuncPtr);

        [DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersTryRemove")]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool TryRemove([MarshalAs(UnmanagedType.LPWStr)] string key, out IntPtr ppFuncPtr);

        [DllImport("ProfilingApiSample04.dll", EntryPoint = "InstanceGettersClear")]
        public static extern void Clear();
    }
}
 
P/Invoke で InstanceGetters** らを呼び出します。はい、それだけです (´・ω・`)
#line 1 "CppTroll\ProfilingApiSample04Framework\InstanceHolder.cs"
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04Framework
{
    public abstract class InstanceHolder<T> where T : InstanceHolder<T>
    {
        protected InstanceHolder() { }
        static T ms_instance = TypeMixin.ForciblyNew<T>();
        public static T Instance { get { return ms_instance; } }
    }
}
 
インスタンスを保持するだけのシンプルなクラスです。TypeMixin.ForciblyNew<T> (8 行目)は、単に非公開コンストラクタを強制的に呼び出すだけのメソッドですので解説はしません。だんだん「こんなので行けるのか?」と不安になってこられているかもしれませんが・・・なんと次で終わりです!(ぇー
#line 1 "CppTroll\ProfilingApiSample04Framework\LooseCrossDomainAccessor.cs"
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ProfilingApiSample04Framework
{
    public class LooseCrossDomainAccessor
    {
        protected LooseCrossDomainAccessor() { }

        public static void Register<T>() where T : InstanceHolder<T>
        {
            LooseCrossDomainAccessor<T>.Register();
        }

        public static void Unload<T>() where T : InstanceHolder<T>
        {
            LooseCrossDomainAccessor<T>.Unload();
        }

        public static T Get<T>() where T : InstanceHolder<T>
        {
            return LooseCrossDomainAccessor<T>.Holder;
        }

        public static T GetOrRegister<T>() where T : InstanceHolder<T>
        {
            var holder = default(T);
            if ((holder = LooseCrossDomainAccessor<T>.HolderOrDefault) == null)
            {
                LooseCrossDomainAccessor<T>.Register();
                holder = LooseCrossDomainAccessor<T>.Holder;
            }
            return holder;
        }

        public static bool TryGet<T>(out T holder) where T : InstanceHolder<T>
        {
            holder = LooseCrossDomainAccessor<T>.HolderOrDefault;
            return holder != null;
        }
    }

    public class LooseCrossDomainAccessor<T> where T : InstanceHolder<T>
    {
        static readonly object ms_lockObj = new object();
        static T ms_holder = null;
        static bool ms_ready = false;
        static readonly Type ms_t = typeof(T);
        static readonly string ms_key = ms_t.AssemblyQualifiedName;

        protected LooseCrossDomainAccessor() { }

        public static void Register()
        {
            var instance = ms_t.GetProperty("Instance", BindingFlags.Public |
                                                        BindingFlags.Static |
                                                        BindingFlags.FlattenHierarchy);
            var instanceGetter = instance.GetGetMethod();
            RuntimeHelpers.PrepareMethod(instanceGetter.MethodHandle);
            var funcPtr = instanceGetter.MethodHandle.GetFunctionPointer();
            InstanceGetters.TryAdd(ms_key, funcPtr);
        }

        static T GetHolder()
        {
            var funcPtr = default(IntPtr);
            if (!InstanceGetters.TryGet(ms_key, out funcPtr))
                throw new InvalidOperationException("T has not been registered yet. " + 
                                                    "Please call Register method.");

            return GetHolderCore(funcPtr);
        }

        static bool TryGetHolder(out T holder)
        {
            var funcPtr = default(IntPtr);
            if (!InstanceGetters.TryGet(ms_key, out funcPtr))
            {
                holder = null;
                return false;
            }
            else
            {
                holder = GetHolderCore(funcPtr);
                return true;
            }
        }

        static T GetHolderCore(IntPtr funcPtr)
        {
            var extractor = new DynamicMethod("Extractor", ms_t, null, ms_t.Module);
            var gen = extractor.GetILGenerator();
            if (IntPtr.Size == 4)
            {
                gen.Emit(OpCodes.Ldc_I4, funcPtr.ToInt32());
            }
            else if (IntPtr.Size == 8)
            {
                gen.Emit(OpCodes.Ldc_I8, funcPtr.ToInt64());
            }
            else
            {
                throw new NotSupportedException();
            }
            gen.EmitCalli(OpCodes.Calli, CallingConventions.Standard, ms_t, null, null);
            gen.Emit(OpCodes.Ret);
            return ((Func<T>)extractor.CreateDelegate(typeof(Func<T>)))();
        }

        public static T Holder
        {
            get
            {
                if (!ms_ready)
                {
                    lock (ms_lockObj)
                    {
                        if (!ms_ready)
                        {
                            ms_holder = GetHolder();
                            Thread.MemoryBarrier();
                            ms_ready = true;
                        }
                    }
                }
                return ms_holder;
            }
        }

        public static T HolderOrDefault
        {
            get
            {
                if (!ms_ready)
                {
                    lock (ms_lockObj)
                    {
                        if (!ms_ready)
                        {
                            var holder = default(T);
                            if (TryGetHolder(out holder))
                            {
                                ms_holder = holder;
                                Thread.MemoryBarrier();
                                ms_ready = true;
                            }
                        }
                    }
                }
                return ms_holder;
            }
        }

        public static void Unload()
        {
            if (ms_ready)
            {
                lock (ms_lockObj)
                {
                    if (ms_ready)
                    {
                        var funcPtr = default(IntPtr);
                        InstanceGetters.TryRemove(ms_key, out funcPtr);
                        ms_holder = null;
                        Thread.MemoryBarrier();
                        ms_ready = false;
                    }
                }
            }
        }
    }
}
 
今までで最長ですが、呼びやすいように I/F 追加したり、わかりやすいように例外投げたりしているだけで、コア部分は GetHolderCore でやっていることが全てです(92 行目~111 行目)。x86/x64 両対応のために、IntPtr.Size で処理を分けるという小細工をしていますが、基本「関数ポインタを calli 命令(IL)を使って直接呼び出す」という、準備でお話した方針そのままですね。

全部合わせても 400 ステップぐらいです。これで本当に AppDomain の呪縛から逃れられるの?と思われるかもしれませんが、論より Run です!それぞれの問題の解決して行きましょう!


例 1: 必要な情報がすでに別の情報と紐づけられている
そうそう、もはや MarshalByRefObject や SerializableAttribute とか関係ないですので、Generic なインスタンス持ち運び用のクラスを InstanceHolder から継承して作っておきましょう。
#line 1 "CppTroll\ProfilingApiSample04Framework\GenericHolder.cs"
namespace ProfilingApiSample04Framework
{
    public class GenericHolder<T> : InstanceHolder<GenericHolder<T>>
    {
        GenericHolder() { }
        public T Source { get; set; }
    }
}
 
で、こう書き換えます。
#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;
using LooseConsole = 
    ProfilingApiSample04Framework.LooseCrossDomainAccessor<
        ProfilingApiSample04Framework.GenericHolder<System.IO.TextWriter>>;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class ConsoleTest
    {
        [TestFixtureSetUp]
        public void FixtureSetUp()
        {
            LooseConsole.Unload();
            LooseConsole.Register();
            LooseConsole.Holder.Source = Console.Out;

            // Pre-call to run the action that was registered in this AppDomain, 
            // not in other AppDomain but in this AppDomain.
            // Because the event loop that is managed by NUnit GUI - contains calling 
            // Write or WriteLine method - runs in other thread. 
            Console.Write(string.Empty);
            Console.Out.Flush();
        }

        [TestFixtureTearDown]
        public void FixtureTearDown()
        {
            LooseConsole.Holder.Source = null;
            LooseConsole.Unload();
        }

        [Test]
        public void Test()
        {
            LooseConsole.Holder.Source.WriteLine("AppDomain: {0}", 
                                                   AppDomain.CurrentDomain.FriendlyName);

            AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
            {
                LooseConsole.Holder.Source.WriteLine("AppDomain: {0}", 
                                                   AppDomain.CurrentDomain.FriendlyName);
            });

            LooseConsole.Holder.Source.WriteLine("AppDomain: {0}", 
                                                   AppDomain.CurrentDomain.FriendlyName);
        }
    }
}
 
TestFixtureSetUp と TestFixtureTearDown で、持ち運び用のクラスのアンロード、再登録を行っています(22 行目~24 行目、37 行目~38 行目)。あとは一括置換ですね。
一つ気を付けなければならないところと言えば、最初の AppDomain で行われる処理と別の AppDomain で行われる処理が混合してはまずい、ということです。準備の時に触れた CLR の型チェックタイミングに運悪く引っかかると、ExecutionEngineException が吐かれてアプリケーションが異常終了してしまいますので。
NUnit の GUI ツールは、UI の更新のためのイベントループを独自に持ち、[Text Output] タブへの出力も非同期で行われています。30 行目で行っている Write メソッドの事前呼び出しはこれをあらかじめ実行させてしまうためのものです。
さて、実行してみましょう。


キタ━(゚∀゚)━!!

2 つ目の WriteLine 結果もうまく出力されるようになりました!
でもこれだけですと、TextWriter は MarshalByRefObject なのでうまく動いているだけにも思えてしまいますね・・・。次の例も試してみましょう。


例 2: 既存のライブラリ、標準のクラスが AppDomain 越えに対応していない
GenericHolder はそのまま利用できますので、テストの書き換えだけです。
#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\ConsoleTest.cs"
using System;
using System.Diagnostics;
using System.IO;
using NUnit.Framework;
using ProfilingApiSample04Framework.Mixin.System;
using LooseStopwatch = 
    ProfilingApiSample04Framework.LooseCrossDomainAccessor<
        ProfilingApiSample04Framework.GenericHolder<System.Diagnostics.Stopwatch>>;

namespace ProfilingApiSample04FrameworkTest
{
    [TestFixture]
    public class StopwatchTest
    {
        [TestFixtureSetUp]
        public void FixtureSetUp()
        {
            LooseStopwatch.Unload();
            LooseStopwatch.Register();
            LooseStopwatch.Holder.Source = new Stopwatch();
        }

        [TestFixtureTearDown]
        public void FixtureTearDown()
        {
            LooseStopwatch.Holder.Source = null;
            LooseStopwatch.Unload();
        }

        [Test]
        public void Test()
        {
            using (var sw = new StringWriter())
            {
                LooseStopwatch.Holder.Source.Restart();

                sw.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);

                AppDomain.CurrentDomain.RunAtIsolatedDomain<StringWriter>(sw_ =>
                {
                    sw_.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);
                }, sw);

                sw.WriteLine("Elapsed: {0} ms", LooseStopwatch.Holder.Source.ElapsedMilliseconds);

                Console.WriteLine(sw.ToString());
            }
        }
    }
}
 
AppDomainMixin.RunAtIsolatedDomain の引数で Stopwatch を引き回すのは無理ですので廃止し(45 行目)、代わりに GenericHolder に入れて持ち運びます。どうでしょうか・・・?


キマシタワ - .∵・(゚∀゚)・∵. - ッ!!

ちなみに、私の PC 環境(TOSHIBA ウルトラブック dynabook R631、OS: Windows 7 Home Premium 64 bit、CPU: Core i5-2467M、メモリ: 4GB、SSD)ですと、30 ~ 40 ms で AppDomain の生成~終了ができているようです。比べて、Process の起動~終了の場合、120 ms ~ 130 ms かかりましたので、3 ~ 4 倍は効率が良いようです。なるほどなるほど。


例 3: コンパイラが対象のクラスを自動生成する
Generic なエイリアスは作成できませんので、1 つ新しいクラスを切っていますが、やることはこれまでと同様です。
#line 7 "CppTroll\ProfilingApiSample04FrameworkTest\DelegateTest.cs"
using System;
using NUnit.Framework;
using ProfilingApiSample04Framework;
using ProfilingApiSample04Framework.Mixin.System;

namespace ProfilingApiSample04FrameworkTest
{
    class LooseFunc<T1, T2, TResult> : LooseCrossDomainAccessor<GenericHolder<Func<T1, T2, TResult>>> { }

    [TestFixture]
    public class DelegateTest
    {
        [TestFixtureSetUp]
        public void FixtureSetUp()
        {
            LooseFunc<int, int, int>.Unload();
            LooseFunc<int, int, int>.Register();
        }

        [TestFixtureTearDown]
        public void FixtureTearDown()
        {
            LooseFunc<int, int, int>.Holder.Source = null;
            LooseFunc<int, int, int>.Unload();
        }

        [SetUp]
        public void SetUp()
        {
            LooseFunc<int, int, int>.Holder.Source = null;
        }

        [TearDown]
        public void TearDown()
        {
            LooseFunc<int, int, int>.Holder.Source = null;
        }

        [Test]
        public void Test()
        {
            var adder = default(Func<int, int, int>);
            var z = 1;
            adder = (x, y) => x + y + z;    // To closure(capture local variable z)
            LooseFunc<int, int, int>.Holder.Source = adder;
            AppDomain.CurrentDomain.RunAtIsolatedDomain(() =>
            {
                Assert.AreEqual(3, LooseFunc<int, int, int>.Holder.Source(1, 1));
            });
        }
    }
}
 
Stopwatch の例と同様、引数で引き回すのはやめ、持ち運び用のクラスに入れて取りまわすことにしています(51 行目)。これも実行します!


キタ━━━━━ー二三ヘ( ゚∀゚)ノ━━━━━━!!!!!

もはやなんでもありですね (+_+) ただ、もう AppDomain では簡単に分離できなくなってしまうわけですから、これを使わざるを得なくなった状況の、二の舞を演じないように気を使う必要はあります。SetUp や TearDown でやっているように(36 行目、42 行目)、初期化や後始末には細心の注意を払いましょう。




終わりに
前回に引き続き、今回も AppDomain を取り上げましたが、他では見られないようなハックをしてみました。いかがでしたでしょうか?
かなりマニアックな内容で恐縮ですが、私が進めているような、言語(というかプラットフォーム)を拡張するようなライブラリを書こうとすると、やはりどうしても必要になってくる知識だと思います。

ところで、C# などは結構アグレッシブに言語が拡張されているように思いますが、過去の遺産を次の資産として作り直すための拡張って、なかなかないように思います(4.0 の時に「COM 相互運用時の特別処理」っていうのはありましたね)。まあ、C# に限ったことではないとは思いますが、やはり新しい機能のための拡張ということなのでしょう。
しかし、表面的な移行手順はあるとは言え、プログラムの作り方や、古いアーキテクチャをどうやって新しいアーキテクチャに持って行くかというノウハウが、私なんかはもっと言語機能に反映されてもいいと思うのですが、どうなんでしょうか。IT 業界でかかるコストの 7 割は運用・保守となる、ということがわかって、もう 10 年近くが経つのではないかと思うのですが、どうもまだ歴史は繰り返されそうな予感はしています。

私がやっている「Monkey Patching を CLR で動くあらゆる静的言語でも行えるようにする」という活動も、いかにして、既存のものを安全に一歩ずつ変更し、拡張しやすく次の技術へ対応しやすい形に持って行くか、という考えが根底にありますので、これに絡む色々な情報を、今後もウォッチしていきたいと思います。

・・・さて、パズルのピースは揃いました。今後は順次、ライブラリを拡充しながら、引き続き技術情報の発信をしていきたいと思います!



0 件のコメント:

コメントを投稿