原材料表示

kaza ブロッグより.

この記事はおもしろい。

C# is VB with semicolons!

そして、この記事も。

Several of the libraries in the CLR 1.x were written in VB, and there will be even more VB code in the CLR 2.0.とあります。CLR1.XよりもCLR2.0の方がVBで書かれたライブラリが多いそうだ。

meiさんのところで昔書いたコメント(id:akiramei:20040725)のフォローアップということで.
基本的には BCL が別にどの言語で書かれていたところで,利用者にとって直接影響が無い範囲内ではせいぜい話のネタ止まりです.ただ,たまにメタ推理の材料に使えるのも確かで,例えば Managed DirectX は Managed C++ (そのうち C++/CLI かな) で書かれているからこういう実装になってるかなぁとか,こういうバグはあるかもなぁというヒントにはなるでしょう.まあ話のネタとしても面白いですけどね.

Resource Acquisition Is Initialization (1)

C# での確定的デストラクタの不在や,D 言語の auto 属性についての議論は色々見てきましたが,最近は C++/CLI のデストラクタとファイナライザの扱いについて (主に好意的な) 意見をしばしば目にします.というわけで今回はその辺について.テスト環境には Visual C++ 2005 beta2,Windows XP Professional x64 Edition を用いています.

C++/CLI

C++ .NET 拡張として Managed C++ という言語が存在し,Visual C++ .NET 2002/2003 によってサポートされてきました.Managed C++ は Opaque Type *1 のサポートやマネージ/アンマネージ実行モードの混在をサポートしており,従来のネイティブコード資産を容易にマネージ型でラップすることが可能でした.例えばネイティブコードからなる DirectX をマネージ型にラップしたものが Managed DirectX ですが,この作業は Managed C++ によって行われています*2.このように .NET の普及にとって裏方的な役割が期待される Managed C++ ですが,一方でマネージコードとアンマネージコードを混在させた場合の制限が多く,また当初の Managed C++ には .NET 対応言語でありながら純粋なマネージコードのみからなるアセンブリを出力することが出来ないという制限さえありました.
こうして Managed C++ が「.NET に対応した C++ はここにあるよ」と矢面に立っている間に,Managed C++ の欠点を克服し,より実用的な言語を目指して策定が進められていたのが C++/CLI です.PJ Plauger 氏*3や Herb Sutter 氏*4といった著名人が策定作業に加わっていることで,C++/CLI について調べているとしばしば氏らの記事に出会います*5.これら C++ 業界の著名人による示唆に富んだ文章は,.NET 開発者以外の方にとっても一見の価値があるものかもしれません.
C++/CLI の最新ドラフトは以下で読むことが出来ます.
http://www.dinkumware.com/tc39-tg5-2005-019.pdf

*1:CLR が検査を行わない(CLR にとって不透明)な型で,C++ コンパイラ独自のデータ構造や ABI を持つことが出来る

*2:http://members.gamedev.net/managedworld/articles/interviews/tommiller.html

*3:Dinkumware STL で有名な Plauger 氏ですね.Visual C++ 使いはお世話になりまくりかと思います.

*4:The Exceptional C++ (asin:4894712709)で有名な氏は現在 Microsoft で Visual C++ の開発をされています.An interview with Microsoft's new Visual C++ .NET community liaison

*5:Plauger 氏のC++/CLIに関する翻訳記事がCマガジン2004年11月号に掲載されています.また,Herb 氏の blog には沢山の C++/CLI に関する資料があります

Resource Acquisition Is Initialization (2)

C++/CLI の特徴のひとつは,従来の C++ でしばしば用いられていた RAII イディオムを用いて .NET Framework が依拠する Dispose パターンを実装できることです.
http://c2.com/cgi/wiki?ResourceAcquisitionIsInitialization
Herb Sutter 氏は自らの blog で,(C++的な) Destructor と GC は対立するものではなく,協力することでメリットが得られると述べられています.
http://pluralsight.com/blogs/hsutter/archive/2004/11/23/3666.aspx
例として次のようなC++/CLIのコードを見てみます.

public ref class MyFileReader
{
    private:
        FileStream _file;
    public:
        MyFileReader( String^ path, FileMode mode, FileAccess access ) : _file(path, mode, access)
        {
        }
};

public ref class MyTest
{
public:
    static void Main()
    {
        MyFileReader reader( "hogehoge.txt", FileMode::Open, FileAccess::Read);
        // ....
    }
};

ここでは検証可能コードについて見てみたいので,/clr:safe オプションを使用してコンパイルします.Visual C++ 2005 beta 2 を用いました.生成されたアセンブリから Reflector for .NET を用いて C# での等価コードを得たものが以下です.生成された IL は,従来 C# で推奨されてきた形での Dispose パターンと同じであることが分かります.

public class MyFileReader : IDisposable
{
    // Methods
    public MyFileReader(string path, FileMode mode, FileAccess access)
    {
        FileStream modopt(IsConst) local1 
            = (FileStream modopt(IsConst)) new FileStream(path, mode, access);
        try
        {
            this._file = local1;
        }
        fault
        {
            this._file.Dispose();
        }
    }

    public void ~MyFileReader()
    {
        this._file.Dispose();
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose([MarshalAs(UnmanagedType.U1)] bool flag1)
    {
        if (flag1)
        {
            this._file.Dispose();
        }
        else
        {
            base.Finalize();
        }
    }

    // Fields
    private readonly FileStream modreq(IsByValue) _file;
}

public class MyTest
{
    public static void Main()
    {
        MyFileReader reader1;
        MyFileReader modopt(IsConst) local1 = 
        (MyFileReader modopt(IsConst)) new MyFileReader("hogehoge.txt", FileMode.Open, FileAccess.Read);
        try
        {
            reader1 = local1;
        }
        fault
        {
            reader1.Dispose();
        }
        reader1.Dispose();
    }
}

もちろん C++/CLI には従来 C++ で用いられてきた new やポインタをマネージ対応させたものも存在します.C++/CLI では変数の宣言方法の違いで出力される IL がどのように変化するかについて詳しく知りたければ,以下の記事を眺めてみるとよいでしょう.
http://www.bluebytesoftware.com/blog/PermaLink.aspx?guid=88e62cdf-5919-4ac7-bc33-20c06ae539ae
また,Reflector for .NET の結果からは,カスタム修飾子*1を駆使して他のマネージ言語との型互換性を確保していることが伺えます.C++/CLI コンパイラが自動変数や(埋め込まれた)メンバ変数のように扱う参照型変数/フィールドは,実際の IL レベルではいくつかの修飾子が付加されていることを除いて通常の参照型の変数/フィールドと代わりありません.

*1:IsByValueIsConstなどが用いられている模様

Quarantine

C++ との類似性は,あたかもスコープから脱出されるまでは常にオブジェクトが生存しているような錯覚を与えるかもしれません.実際上で見てきたように,自動変数記法で宣言された変数が IDisposable を実装している型の場合に限ればこれは真です.スコープ末尾に Dispose 用のコード片が挿入されることでオブジェクト参照が発生するため,確かにこの場合はスコープから脱出するまではオブジェクトは回収されません.
しかし,ここでもう一度思い出す必要があります.CLR の世界を支配する法則は,有資格観測者から到達できない参照型オブジェクトはいつかは消滅するというものです.そしてスコープなどという観測者は実際には存在しません.コンパイラが何か気を利かせない限り,ソースコードに現れるブラケットはオブジェクトと何ら結合を持っていないのです.
ここでもう一度整理すると,C++/CLI などのいわゆる(高級)マネージ言語のソースコードは,2 段階の変換を受けます.

  1. MSIL に変換された時点で既にスコープの概念は消滅し,オブジェクトは仮想スタックとローカル変数を行ったり来たりしている.
  2. 実際の寿命は実行環境での root 参照に支配され,JIT コンパイル後の実行コードに依存したスタックや CPU レジスタ状態の影響を受けることになる.

実際オブジェクトの回収タイミングを注意深く調べると,ソースコード上のスコープ範囲内での「回収」を観測できることがあります.菊池さんが分かりやすいサンプルを書かれているので,それを参考にさせて頂きましょう.
http://www.divakk.co.jp/blog/aoyagi/archive/2005/03/25/1892.aspx

using namespace System::Collections::Generic;
public ref class Hoge
{
public:
    List<int> list;
    !Hoge()
    {
        // ファイナライザ中でのこのようなアクセスは本来禁止されている
        // ファイナライザ中でのマネージ型へのアクセスには様々な致命的なシナリオが存在するため,
        // よほどの理由がない限り避けるべきである
        list.Clear(); 
    }
};
public ref class Program
{
    static void Main()
    {
        Hoge hoge;
        List<int>^ list;
        list = %(hoge.list);
        list->Add(1);
        list->Add(2);
        GC::Collect();
        System::Threading::Thread::Sleep(0);
        Console::WriteLine(list->Count); // debug -> 2, release -> 0
    }
};

最適化の On/Off で生成される IL が異なっていたことや,デバッグビルド時の JIT 最適化の抑制など様々な要因が考えられるものの,現に Release ビルドではスコープ範囲内でのオブジェクトの回収を観測することが出来ました.
このことからも分かるように,ファイナライザは「てきとー」に書けばいいというものではありません.きちんとルールに従って「適当」に実装する必要があります.C++/CLI に惚れ込むのもいいですが,その辺は理解しておく必要があるでしょう.
もっとも C++/CLI が活用されるようなシナリオでは,gcroot を初めとして考慮すべきことは沢山あり,そもそも GC については気を遣って当たり前なはずです.きっとファイナライザも注意深く実装してくれることでしょう.きっと.まあネタを振った以上一応フォロー*1ということで……

*1:例えば CriticalFinalizerObject 派生型は通常のファイナライザ内からアクセス可能なマネージオブジェクトという側面を持つ,と言えるかもしれませんね.詳しい理由は MSDN 等で熟知すべし書いた当時の意図がよく思い出せないので一旦削除

GC Threading Problem on Managed DirectX

Managed DirectX での早期導入にも期待しておきます.C++/CLI から SafeHandles を使うときはまたちょこっと印象変わるかもしれませんね.

以前,菊池さんところ経由で SafeHandle について書いたときに Managed DirectX に SafeHandles が使えないかという話が出ていましたが,後でふと気になったことがあって試してみたところ,スレッド周りで問題が起きそうなことを確認しました.
DirectX Graphics は Win32 Native Thread と密に結合しています*1.試しに手元の DirectX 9.0c Debug Runtiem 環境で以下のようなコードを実行してみました.

  1. 通常通りデバイスを作成
  2. 別スレッドを作って,そのスレッドでデバイスを解放

すると,デバッグメッセージには次のように表示されました.

Direct3D9: (ERROR) :Final Release for a device can only be called from the thread that the device was created from.

このことから,Managed DirectX で SafeHandle を使ったとしても解放作業自体はせいぜい ReliabilityContract(Consistency.MayCorruptProcess, Cer.MayFail) ということになるかと思います.
このようにスレッドに密結合するアンマネージリソースを .NET に対応させる場合,2つの点で厄介と考えられます.

  1. CLR Hosting API は,ソフトスレッドとハードスレッドの対応をカスタマイズすることを許している
  2. Finalizer は専用のスレッドから呼ばれる

まあ現実には(1)が問題になる局面ではそもそも WinForms からして問題が出るかもしれません.また,IDisposable を実装しているということのみからスレッド依存性を知ることは無理でしょうから,単純に IDisposable.Dispose を呼べば解決,とは言えない例のひとつでもあるでしょう.

*1:Win32 ウィンドウのサブクラス化を行うことが影響しているのかもしれません