April 2009

Volume 24 Number 04

Windows と C++ - Windows 7 の仮想ディスク API

Kenny Kerr | April 2009

この記事は、Windows Server 7 のプレリリース版に基づいています。記載されている内容は変更されることがあります。

目次

VHD 形式
仮想ディスクの作成
仮想ディスクの起動
仮想ディスクのアタッチ
仮想ディスクに対するクエリ
次のステップ

この記事の執筆時点では、Windows 7 ベータ版がリリースされて数日経ったところですが、確かに、優れた機能が多数用意されているようです。いつものように、Windows SDK の新しい機能について確認してみました。Windows 7 では、SDK に関して大きな変更はありません。これは良いことです。Windows 7 でネイティブの C++ アプリケーションを書くときの基本事項は、Windows Vista の場合と違って、ほとんど変更されていません。とは言うものの、Windows 7 には、まったく新しい機能もいくつか追加されています。これらの機能は、Windows 7 の導入を検討しているユーザーにとって興味深いものです。

そうした機能の 1 つが仮想ディスク API です。Windows 7 ベータ版の仮想ハード ディスク API は、Microsoft 仮想ハード ディスク (VHD) 以外の形式にも対応できるように設計されているものの、Hyper-V や Virtual PC などのマイクロソフト製の仮想化製品によって普及した VHD への対応を重視した API となっています。

筆者は長年、仮想化技術とかかわってきたので、VHD と聞くとこれまでの経緯を思い出します。仮想化技術に出会ったのは 10 年近く前になります。当時は、VMware が市場をほぼ独占していました。ところが、2003 年に、マイクロソフトが Connectix 社から仮想マシン技術を獲得すると、すべてが一変します。突如として、大手 2 社が仮想化市場を奪い合う形となったのです。マイクロソフトが Connectix 社から獲得した VHD 形式は、マイクロソフトが求めている仮想化技術の方向性、つまり仮想化をプラットフォームにしたいという意向とよく合致していました。VMware の仮想ディスク形式は同社専用の形式であり、複雑で、リリースごとに大幅に変更されることがよくありましたが、VHD 形式は、もともとの設計が非常にシンプルかつ柔軟で、将来生き残っていくことができる性質を十分に備えていました。その後、VHD 形式は、マイクロソフトの製品だけでなく、大手から小規模にいたる他のソフトウェア企業の製品や技術によって次々に採用され、その優位性を実証してきました。

ですから、Windows 7 が VHD 形式をネイティブでサポートすることになって喜んでいます。これで、ユーザーと管理者は、仮想ディスクを物理記憶装置と同じように簡単に作成およびアタッチできるようになります。しかも、サードパーティ製のドライバやツールをインストールする必要は一切ありません。たとえば、ディスク管理 MMC (Microsoft 管理コンソール) スナップインや DISKPART コマンド ライン ツールを使用して、仮想ディスクを作成およびアタッチすることができます。アタッチされた仮想ディスクは、コンピュータ上の他のハード ディスクと同じように、パーティションを作成し、フォーマットして使用できます。

また、仮想ディスクの作成と管理を細かいレベルで制御できます。この機能を実現しているのは VirtDisk.dll です。この DLL は、仮想ディスクを作成および管理するための低レベルの C API (仮想ディスク API) を提供します。この API は、記憶域サブシステム内でディスクとボリュームを表現するために必要な多数のカーネルモード ドライバに対するアクセスを提供しています。上位階層のファイル システム ドライバは、実際の記憶域の物理特性を意識せずに済みます。

ディスク管理ツールは通常、仮想ディスク サービス (VDS) を介して仮想ディスクとやり取りします。VDS 自体も、仮想ディスク API を使用して VHD ベースの記憶域にアクセスします。もちろん Hyper-V も仮想マシンを作成および操作するための独自の Windows Management Instrumentation (WMI) ベースの API を提供していますが、この API も仮想ディスク API に依存しています。

仮想ディスク API の使用方法の説明に入る前に、VHD 形式の基本事項について簡単に説明しておきます。この記事を読めば、この新しい API を使用することで、これまで仮想ディスクを管理するために必要だったコードの大部分が不要になることがおわかりいただけると思います。

VHD 形式

VHD 形式には、3 つのイメージの種類、つまり、固定ディスク、ダイナミック ディスク、差分ディスクがあります。これらのディスクの種類はすべて、ディスク ファイルの末尾に 512 バイトの VHD フッタを含んでいます。これについては後ほど詳しく説明します。

固定ディスクは最も単純なディスクの種類で、パフォーマンスも最も高くなります。なぜなら、ディスクの作成時に要求されたサイズに対応するディスク ファイルがフルに割り当てられるからです。ですから、500 MB の固定ディスクのサイズは、正確には 500 x 1024 x 1024 + 512 バイトになります。フッタはディスクの末尾にあるため、ディスクの記憶域がファイルの先頭と一致し、簡単で高速なランダムアクセスが可能になります。

ダイナミック ディスクは疎ディスクと呼ばれます。この名前は、ダイナミック ディスクの性質をよく表しています。ダイナミック ディスクでは、ディスク ファイルを最初に作成するとき、VHD フッタ、および記憶域の動的な割り当てを管理するために必要なメタデータを格納するために必要な領域だけが確保されます。その後、データがディスクに書き込まれると、追加の記憶域ブロックがディスク ファイルの末尾に割り当てられます。ダイナミック ディスクの利点は、最大容量を使用しない限り、使用ディスク領域がはるかに少なくて済むことです (実際、ほとんどの場合、最大容量を使用することはありません)。欠点は、オンデマンドでダイナミック ディスクの読み出し、書き込み、拡張を行うために間接的な処理とメタデータ管理が必要になるので、パフォーマンスが低下することです。このため、ダイナミック ディスクはテスト環境に適しています。一方、固定ディスクは、パフォーマンスが重視される本稼働環境に適しています。

差分ディスクは内部的にはダイナミック ディスクとほぼ同じですが、その他の特性は大きく異なります。差分ディスクは、ブロックが動的に割り当てられるという点ではダイナミック ディスクと同じですが、それらのブロックには親ディスクを基準として、変更された部分だけが格納されます。したがって、差分ディスクは親ディスクがないと機能しません。差分ディスクの親ディスクは、固定ディスクでもダイナミック ディスクでもかまいません。別の差分ディスクを親ディスクにすることもできます。これにより、ディスク チェーンまたはディスク ツリーを構築できます。リーフノードに当たるディスクには自由に書き込むことができますが、非リーフノードは読み取り専用として扱う必要があります。なぜなら、子孫の差分ディスクは自分の親に当たる非リーフノードのディスクとセットで初めてディスク全体を構成できるからです。非リーフノードのディスクの内容が変更されると、仮想ディスクが破壊される可能性が十分にあります。

前述のとおり、各ディスクは、ディスクの種類に関係なく同じフッタを共有します。このフッタには、ディスクの論理サイズ、ディスク ジオメトリ、ディスクの種類、ディスクのグローバル一意識別子などの情報が格納されます。ダイナミック ディスクおよび差分ディスクの場合は、フッタにオフセット値も含まれます。オフセット値は、動的ヘッダのディスクの先頭からの相対位置を示します。この 2 番目の構造体には、主に、親ディスクの場所を特定し識別するために必要な情報、およびディスクのブロック アロケーション テーブル (BAT) の存在場所を示す別のオフセット値が含まれています。BAT は、割り当て済みのブロックと、各ブロックのディスク ファイル内での絶対オフセットを示します。

VHD の基本的な説明はこれくらいにして、仮想ディスク API について見ていきましょう。

仮想ディスク API は、将来、VHD 以外の形式もサポートできるように設計されています。実際、ISO 光ディスク イメージ形式のサポートが検討されていましたが、Windows 7 ベータ版には追加されませんでした。正式リリースでは、必要な仮想ディスクのサポート プロバイダが追加されることを期待しましょう。ISO 光ディスク イメージ形式がサポートされれば、ISO イメージを読み取り専用のハード ドライブとしてアタッチおよびマウントできるようになります。

仮想ディスクの作成

仮想ディスクは、ファイル、レジストリ キーなど、他のシステム オブジェクトと同様に、非透過ハンドルによって表されます。仮想ディスク ハンドルをクローズするときも、おなじみの CloseHandle 関数を使用できます。仮想ディスクの管理には、Active Template Library (ATL) の CHandle クラスが最適です。CHandle から "仮想ディスク" クラスを派生すると、仮想ディスクの操作に必要な一部の定型コードを含むクラスが生成されます。

仮想ディスクのオープンや作成を実行するには常に、ディスクの記憶域の種類を指定する必要があります。記憶域の種類は、VIRTUAL_STORAGE_TYPE 構造体に指定します。記憶域の種類として、ISO 形式と VHD 形式が定義されています。この構造体には、特定の記憶域の種類の実装を提供するベンダーも指定します。VHD 記憶域の種類は、次のように記述します。

VIRTUAL_STORAGE_TYPE storageType =
{
    VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
    VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT
};

CreateVirtualDisk 関数は、3 種類の仮想ディスクをすべて作成できます。その際、記憶域の種類だけでなく、CREATE_VIRTUAL_DISK_PARAMETERS 構造体も設定する必要があります。この構造体の設定方法は、作成する仮想ディスクの種類によって異なります。大半の仮想ディスク API 構造体では、将来、API が更新されても対応できるようにバージョン管理スキームを使用しています。各構造体は、Version という名前のメンバで始まり、その後に、構造体のユニオン (共用体) が続きます。たとえば、仮想ディスクを作成するための共通フィールドは次のように設定します。

CREATE_VIRTUAL_DISK_PARAMETERS parameters =
{
    CREATE_VIRTUAL_DISK_VERSION_1
};
parameters.Version1.MaximumSize = size;
parameters.Version1.BlockSizeInBytes = CREATE_VIRTUAL_DISK_PARAMETERS_DEFAULT_BLOCK_SIZE;
parameters.Version1.SectorSizeInBytes = CREATE_VIRTUAL_DISK_PARAMETERS_DEFAULT_SECTOR_SIZE;

仮想ディスクのサイズはバイト単位で指定し、512 の倍数でなければなりません。ブロック サイズとセクタ サイズも設定可能ですが、異なる実装間で最大限の互換性を維持する必要があるなら、既定値のままにしておくのが最善の選択です。Version1 構造体には UniqueId メンバを指定できますが、このメンバを初期値のゼロのままにしておくと、CreateVirtualDisk 関数によって GUID が生成されます。差分ディスクを除くすべてのディスクの種類では、SourcePath メンバも指定できます。SourcePath メンバを指定すると、CreateVirtualDisk によって、ソース ディスクの内容が新しく作成される仮想ディスクにコピーされます。コピー元のソース ディスクとコピー先の仮想ディスクは同じ種類である必要はありません。実際、ソース ディスクは、仮想ディスクである必要もなく、物理ディスクでもかまいません。

図 1 に、VirtualDisk ラッパー クラスの先頭部分を示します。このクラスには、固定仮想ディスクを作成するための CreateFixed メンバ関数が定義されています。固定ディスクの場合は、CREATE_VIRTUAL_DISK_FLAG_FULL_PHYSICAL_ALLOCATION フラグを指定する必要がある点に注意してください。ダイナミック ディスクを作成する場合は、このフラグを削除し、代わりに CREATE_VIRTUAL_DISK_FLAG_NONE フラグを指定します。それ以外は固定ディスクを作成するときと同じです。差分ディスクを作成する場合もほぼ同じです。ただし、差分ディスクの場合、サイズは親ディスクから推測されるため、設定しないでください。その代わり、Version1 構造体の ParentPath メンバに親ディスクを指定する必要があります。また、当然ですが、差分ディスクの場合、SourcePath を設定することはできません。

図 1 固定ディスクの作成

class VirtualDisk : public CHandle
{
public:

    DWORD CreateFixed(PCWSTR path,
                      ULONGLONG size,
                      VIRTUAL_DISK_ACCESS_MASK accessMask,
                      __in_opt PCWSTR source,
                      __in_opt PSECURITY_DESCRIPTOR securityDescriptor,
                      __in_opt OVERLAPPED* overlapped)
    {
        ASSERT(0 == m_h);
        ASSERT(0 != path);
        ASSERT(0 == size % 512);

        VIRTUAL_STORAGE_TYPE storageType =
        {
            VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
            VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT
        };

        CREATE_VIRTUAL_DISK_PARAMETERS parameters =
        {
            CREATE_VIRTUAL_DISK_VERSION_1
        };

        parameters.Version1.MaximumSize = size;
        parameters.Version1.BlockSizeInBytes = CREATE_VIRTUAL_DISK_        PARAMETERS_DEFAULT_BLOCK_SIZE;
        parameters.Version1.SectorSizeInBytes = CREATE_VIRTUAL_DISK_        PARAMETERS_DEFAULT_SECTOR_SIZE;
        parameters.Version1.SourcePath = source;

        return ::CreateVirtualDisk(&storageType,
                                   path,
                                   accessMask,
                                   securityDescriptor,
                                   CREATE_VIRTUAL_DISK_FLAG_FULL_                                   PHYSICAL_ALLOCATION,
                                   0, // no provider-specific flags
                                   &parameters,
                                   overlapped,
                                   &m_h);
    } 

CreateVirtualDisk 関数には、他にも重要なパラメータがいくつかあります。VIRTUAL_DISK_ACCESS_MASK 列挙体は、この関数が、生成されたハンドルを介して呼び出し元に許可するアクセスを制御するためのフラグのセットです。このパラメータには VIRTUAL_DISK_ACCESS_ALL を指定することもできますが、そうすると、現在、記憶装置としてアタッチされている仮想ディスクに対するクエリなどの操作を実行できなくなるため、多くの場合、望ましくありません。その他にも、OVERLAPPED 構造体を指定できる便利な機能があります。この機能は、大部分の仮想ディスク API 関数でサポートされており、ご想像のとおり、操作を非同期に実行できるようにします。手動でイベントをリセットすると、完了時にシグナルで通知されます。

仮想ディスクの起動

仮想ディスクを起動するには OpenVirtualDisk 関数を使用します。仮想ディスクの作成時と同様、VIRTUAL_STORAGE_TYPE 構造体を設定して記憶域の種類を特定する必要があります。OPEN_VIRTUAL_DISK_PARAMETERS 構造体をオプションで指定することもできますが、この構造体が必要になるのは、通常、差分ディスクの関係を操作するときだけです。

図 2 に、図 1 に示した VirtualDisk ラッパー クラスに追加する Open メンバ関数を示します。一般に、仮想ディスクの起動は、仮想ディスクの作成よりも簡単ですが、仮想ディスクのマージやアタッチなどの保守操作を実行できるようにするには、一部のフラグやオプションを特殊な方法で使用する必要があります。

図 2 仮想ディスクの起動

DWORD Open(PCWSTR path,
           VIRTUAL_DISK_ACCESS_MASK accessMask,
           OPEN_VIRTUAL_DISK_FLAG flags, // OPEN_VIRTUAL_DISK_FLAG_NONE
           ULONG readWriteDepth) // OPEN_VIRTUAL_DISK_RW_DEPTH_DEFAULT
{
    ASSERT(0 == m_h);
    ASSERT(0 != path);

    VIRTUAL_STORAGE_TYPE storageType =
    {
        VIRTUAL_STORAGE_TYPE_DEVICE_VHD,
        VIRTUAL_STORAGE_TYPE_VENDOR_MICROSOFT
    };

    OPEN_VIRTUAL_DISK_PARAMETERS parameters =
    {
        OPEN_VIRTUAL_DISK_VERSION_1
    };

    parameters.Version1.RWDepth = readWriteDepth;

    return ::OpenVirtualDisk(&storageType,
                             path,
                             accessMask,
                             flags,
                             &parameters,
                             &m_h);
}

仮想ディスクのアタッチ

Windows 7 ベータ版では、ディスクをオペレーティング システム内の記憶装置としてアタッチすることを表すのに、"surface (または surfacing)" という用語を使用しています。この用語は、その後、より明確な用語 "attach" に変更されました。AttachVirtualDisk (ベータ版では SurfaceVirtualDisk と呼ばれる) 関数は、仮想ディスクをアタッチします。仮想ディスクは、CreateVirtualDisk または OpenVirtualDisk を呼び出して取得したハンドルによって識別されます。仮想ディスクをアタッチするには、ハンドルに、適切なアクセス権が与えられていることを確認する必要があります。仮想ディスクをアタッチまたはデタッチするには、トークンが SE_MANAGE_VOLUME_NAME 特権を保有している必要があります。この特権は、ユーザー アカウント制御の使用中は管理者のトークンから削除されるため、この特権を含む無制限トークンにアクセスするには、アプリケーションを昇格させる必要があります。

図 3 に、VirtualDisk ラッパー クラスに追加する Attach メンバ関数を示します。ATTACH_VIRTUAL_DISK_FLAG (ベータ版では SURFACE_VIRTUAL_DISK_FLAG) パラメータは、仮想ディスクをアタッチする方法を制御するためのパラメータです。

図 3 ディスクのアタッチ

DWORD Attach(ATTACH_VIRTUAL_DISK_FLAG flags,
              __in_opt PSECURITY_DESCRIPTOR securityDescriptor,
              __in_opt OVERLAPPED* overlapped)
{
    ASSERT(0 != m_h);


    return ::AttachVirtualDisk(m_h,
                                securityDescriptor,
                                flags,
                                0, // no provider-specific flags
                                0, // no parameters
                                overlapped);
}

ATTACH_VIRTUAL_DISK_FLAG_READ_ONLY (ベータ版では SURFACE_VIRTUAL_DISK_FLAG_READ_ONLY) パラメータは、アタッチされたディスクを書き込み禁止にするときに指定します。このパラメータは、VDS でディスクを書き込み可能にしても上書きされません。

ATTACH_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER (ベータ版では SURFACE_VIRTUAL_DISK_FLAG_NO_DRIVE_LETTER) パラメータを指定すると、仮想ディスク内に存在するボリュームに対するドライブ文字の自動的な割り当てが行われなくなります。これにより、ボリュームをプログラムでマウントするのか、一切マウントしないのかを、適宜選択できます。GetVirtualDiskPhysicalPath 関数を使用すると、仮想ディスクがアタッチされた物理パスを取得できます。

ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME (ベータ版では SURFACE_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME) パラメータを指定すると、仮想ディスクがクローズされた後も、仮想ディスクがアタッチされた状態が維持されます。このフラグを指定しないと、ハンドルがクローズされたとき、仮想ディスクが自動的にデタッチされます。ATTACH_VIRTUAL_DISK_FLAG_PERMANENT_LIFETIME を指定した場合に、仮想ディスクをデタッチするには、DetachVirtualDisk (ベータ版では UnsurfaceVirtualDisk) 関数を呼び出す必要があります。

仮想ディスクに対するクエリ

GetVirtualDiskInformation 関数を使用すると、仮想ディスクに対して、さまざまな情報を取得するクエリを発行できます。これらの情報は GET_VIRTUAL_DISK_INFO 構造体として返されます。この構造体でも、他の多くの仮想ディスク API 構造体で使用されているのと同じバージョン パターンを使用します。たとえば、ディスクのサイズ情報を取得するには、この構造体のバージョンに GET_VIRTUAL_DISK_INFO_SIZE を設定します。これにより、対応する Size 共用体メンバが設定されます。図 4 をご覧ください。

図 4 サイズ情報を取得する

DWORD GetSize(__out ULONGLONG& virtualSize,
              __out ULONGLONG& physicalSize,
              __out ULONG& blockSize,
              __out ULONG& sectorSize) const
{
    ASSERT(0 != m_h);

    GET_VIRTUAL_DISK_INFO info =
    {
        GET_VIRTUAL_DISK_INFO_SIZE
    };

    ULONG size = sizeof(GET_VIRTUAL_DISK_INFO);

    const DWORD result = ::GetVirtualDiskInformation(m_h,
                                                     &size,
                                                     &info,
                                                     0); // fixed size

    if (ERROR_SUCCESS == result)
    {
        virtualSize = info.Size.VirtualSize;
        physicalSize = info.Size.PhysicalSize;
        blockSize = info.Size.BlockSize;
        sectorSize = info.Size.SectorSize;
    }

    return result;
} 

GetVirtualDiskInformation 関数は仮想ディスク ハンドルを操作するため、使用するには適切なアクセス権が必要です。具体的には、VIRTUAL_DISK_ACCESS_GET_INFO アクセス許可が必要になります。GetVirtualDiskInformation を使用して取得できる情報には可変長のものがあるため、最初に確保される記憶域のサイズと最終的に使用された記憶域のサイズを指定する 2 つのパラメータが用意されています。クエリを実行する大半の情報は、前もってサイズがわかっているため、最後のパラメータは省略できます (図 4 の例でもそうなっています)。

例外は、親ディスクの場所 (パス) について差分ディスクに対してクエリを実行する場合です。この場合は、まず、GetVirtualDiskInformation を最初に呼び出すときに必要となるメモリ容量を調べる必要があります。この最初の呼び出し自体は ERROR_INSUFFICIENT_BUFFER で失敗するのですが、これによって割り当てる必要のあるバッファ サイズがわかります。そして 2 回目に呼び出すときに、実際に情報を取得します。親ディスクの場所を取得するには、GET_VIRTUAL_DISK_INFO_PARENT_LOCATION バージョン フラグを使用します。実際には、この方法はもう少し複雑です。親ディスクへの参照を維持することは、差分ディスクを操作するうえできわめて重要なので、VHD 形式では、ある程度の冗長性を持たせて、万一親ディスクへのリンクが破損した場合に対応できるようにしています。ここでは、親ディスクの場所についてのクエリでは、Null で終わる一連の文字列の最後に空文字列が追加された形式の文字列の並びを解析する必要があるというだけにとどめておきます。これは、REG_MULTI_SZ レジストリ値型と同じ形式です。図 5 のコードに、この解析方法を示します。この例では、ATL の CString クラスだけでなく、ATL の CAtlArray コレクション クラスも使用しています。

図 5 親ディスクの場所を取得する

DWORD GetParentLocation(__out bool& resolved,
                        __out CAtlArray<CString>& paths) const
{
    ASSERT(0 != m_h);

    GET_VIRTUAL_DISK_INFO info =
    {
        GET_VIRTUAL_DISK_INFO_PARENT_LOCATION
    };

    ULONG size = sizeof(GET_VIRTUAL_DISK_INFO);

    DWORD result = ::GetVirtualDiskInformation(m_h,
                                               &size,
                                               &info,
                                               0); // not used

    if (ERROR_INSUFFICIENT_BUFFER != result)
    {
        return result;
    }

    CAtlArray<BYTE> buffer;

    if (!buffer.SetCount(size))
    {
        return ERROR_NOT_ENOUGH_MEMORY;
    }

    GET_VIRTUAL_DISK_INFO* pInfo = reinterpret_cast<GET_VIRTUAL_DISK_    INFO*>(buffer.GetData());
    pInfo->Version = GET_VIRTUAL_DISK_INFO_PARENT_LOCATION;

    result = ::GetVirtualDiskInformation(m_h,
                                         &size,
                                         pInfo,
                                         0); // not used

    if (ERROR_SUCCESS == result)
    {
        resolved = 0 != pInfo->ParentLocation.ParentResolved;
        PCWSTR path = pInfo->ParentLocation.ParentLocationBuffer;

        while (0 != *path)
        {
            paths.Add(path);

            path += paths[paths.GetCount() - 1].GetLength() + 1;
        }
    }

    return result;
}

次のステップ

仮想ディスク API には、主に仮想ディスクの保守または修復を行うための関数もいくつか用意されています。MergeVirtualDisk 関数を使用すると、差分ディスクに対するすべての変更を特定の親ディスクにマージできます。チェーン内の任意の親ディスクにマージできます。また、仮想ディスクの圧縮/展開を行う関数、差分ディスクの関係メタデータを更新する関数も用意されています。

Windows 7 ベータ版の Windows SDK には、仮想ディスク API 関数をリンクするために必要な VirtDisk.lib ファイルは含まれていません。この点は、正式リリースで修正される予定です。ベータ版を使用している開発者は、LoadLibrary 関数と GetProcAddress 関数を使用することで、関数をロードしてアドレス指定したり、lib ファイルを自分で作成したりできます。

ご意見やご質問は mmwincpp@microsoft.com まで英語でお送りください。

Kenny Kerr は、Windows 向けのソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は weblogs.asp.net/kennykerr です。