[C++] [WMI] Windows Storage Management

CreateFIle API を使うとき、多くの場合では第一引数にファイル パスを渡してファイルを開きます。CreateFile の処理は内部的に Windows カーネルのオブジェクト マネージャーが第一引数の文字列をデバイス オブジェクトとして解釈し、I/O マネージャーが適当なドライバーにリダイレクトするという流れになっています。例えば、指定したファイルのあるボリュームが NTFS フォーマットだったら ntfs.sys が処理し、UNC パスだったら mrxsmb.sys が処理したりという感じです。厳密には誤りがあるかもしれませんが、大枠はこんな感じです。

CreateFile は、ファイル システムだけでなくデバイスへ直接アクセスすることもできます。ボリュームやディスクへのダイレクト I/O を行なえるわけです。シリアル ケーブルの通信なんかもそうですね。詳しくは MSDN で CreateFile を見て下さい。

CreateFile function
http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx

ダイレクト I/O を使うと、フォーマットされていないボリュームや、パーティショニングされていないディスクに対して読み書きが可能になります。いわゆる RAW ディスクというやつで、ファイル システム ドライバーを必要としないわけです。この場合、上記 MSDN ページに書いてありますが CreateFile の第一引数に \\.\C: とか \\.\PhysicalDrive0 というような文字列を渡します。前者がボリューム デバイスを開く場合で、後者がディスク デバイスを開く場合です。

この文字列は、デバイス オブジェクトを直接示しているのではなく、デバイス オブジェクトへのシンボリック リンクの名前を示しています。デバイス オブジェクトについては、お馴染み Winobj で見ることができます。

image

Winobj
http://technet.microsoft.com/en-us/sysinternals/bb896657

上の画面だと、例えば以下のようなシンボリック リンクを確認できます。

\GLOBAL??\C: ⇒ \Device\HarddiskVolume1
\GLOBAL??\PhysicalDrive0 ⇒ \Device\Harddisk0\DR0

\GLOBAL?? というのは、オブジェクト マネージャーの解釈する名前空間です。が、Windows API はこの名前空間を解釈することができません。このため、\\.\ という接頭辞をつけるルールになっていて、CreateFile はこれを \GLOBAL?? という名前空間に変換してオブジェクト マネージャーに渡します。上の例で行けば、以下のような流れがあるわけです。

\\.\PhysicalDrive0 ⇒ \GLOBAL??\PhysicalDrive0 ⇒ \Device\Harddisk0\DR0

オブジェクト マネージャーが管理するオブジェクトについては、カーネル デバッガーの !object コマンドでも確認可能です。上記画面キャプチャと同じ環境でのカーネル デバッガーの出力を抜粋します。OS は Windows 7 SP1 64bit です。

まずは C: について。シンボリック リンクが HarddiskVolume0 を指していて、HarddiskVolume0 は volmgr.sys のデバイス オブジェクトであることが分かります。

0: kd> !driveinfo c:
Drive c:, DriveObject fffff8a00029c420
Directory Object: fffff8a000008060 Name: C:
Target String is ‘\Device\HarddiskVolume1’
Drive Letter Index is 3 (C:)
Volume DevObj: fffffa8007cc39a0
Vpb: fffffa8007cc0820 DeviceObject: fffffa8007fd0030
FileSystem: \FileSystem\Ntfs
*************************************************************************
*** ***
*** ***
*** Your debugger is not using the correct symbols ***
*** ***
*** In order for this command to work properly, your symbol path ***
*** must point to .pdb files that have full type information. ***
*** ***
*** Certain .pdb files (such as the public OS symbols) do not ***
*** contain the required information. Contact the group that ***
*** provided you with these symbols if you need this command to ***
*** work. ***
*** ***
*** Type referenced: ntfs!VOLUME_DEVICE_OBJECT ***
*** ***
*************************************************************************
Cannot get ntfs!VOLUME_DEVICE_OBJECT.Vcb @ fffffa8007fd0030

0: kd> !object \GLOBAL??\C:
Object: fffff8a00029c420  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a00029c3f0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a000008060  Name: C:
    Target String is ‘\Device\HarddiskVolume1’
    Drive Letter Index is 3 (C:)

0: kd> !object \device\harddisk1\partition1
Object: fffff8a00029cd10  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a00029cce0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a0001f8060  Name: Partition1
    Target String is ‘\Device\HarddiskVolume1’

0: kd> !object \Device\HarddiskVolume1
Object: fffffa8007cc39a0  Type: (fffffa8006d32c90) Device
    ObjectHeader: fffffa8007cc3970 (new version)
    HandleCount: 0  PointerCount: 9
    Directory Object: fffff8a000010920  Name: HarddiskVolume1

0: kd> !devobj fffffa8007cc39a0
Device object (fffffa8007cc39a0) is for:
HarddiskVolume1*** ERROR: Module load completed but symbols could not be loaded for spgu.sys
\Driver\volmgr DriverObject fffffa8007b617c0
Current Irp 00000000 RefCount 30480 Type 00000007 Flags 00201150
Vpb fffffa8007cc0820 Dacl fffff9a10033f0d0 DevExt fffffa8007cc3af0 DevObjExt fffffa8007cc3c58 Dope fffffa8007cc4820 DevNode fffffa8007ccda90
ExtensionFlags (0x00000800)
                             Unknown flags 0x00000800
AttachedDevice (Upper) fffffa8007ccea40 \Driver\fvevol
Device queue is not busy.

次に PhysicalDrive0 について。
PhysicalDrive0 が \Device\Harddisk0\DR0 という disk.sys のデバイス オブジェクトにリンクしていることが分かります。この DR0 というのがディスクのデバイス オブジェクトになるわけですが、NT4 の命名規約との互換性のためか、\Device\Harddisk0\Partition0 というシンボリック リンクも DR0 にリンクしています。このため、パーティション番号は 1 から始まることになります。DR が何の略なのかちょっと調べましたが出てきませんでした。知っている人教えて下さいー。

0: kd> !object \GLOBAL??\PhysicalDrive0
Object: fffff8a000154fe0  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a000154fb0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a000008060  Name: PhysicalDrive0
    Target String is ‘\Device\Harddisk0\DR0’

0: kd> !object \Device\Harddisk0\DR0
Object: fffffa8007e7c060  Type: (fffffa8006d32c90) Device
    ObjectHeader: fffffa8007e7c030 (new version)
    HandleCount: 0  PointerCount: 4
    Directory Object: fffff8a0001f8eb0  Name: DR0

0: kd> !devobj fffffa8007e7c060
Device object (fffffa8007e7c060) is for:
DR0 \Driver\Disk DriverObject fffffa8007cbce70
Current Irp 00000000 RefCount 0 Type 00000007 Flags 01002050
Vpb fffffa8007cbdb80 Dacl fffff9a100303ae0 DevExt fffffa8007e7c1b0 DevObjExt fffffa8007e7c858 Dope fffffa8007cbdb10
ExtensionFlags (0x00000800)
                             Unknown flags 0x00000800
AttachedDevice (Upper) fffffa8007e7cb90 \Driver\partmgr
AttachedTo (Lower) fffffa8007abd420 \Driver\ACPI
Device queue is not busy.

0: kd> !object \Device\Harddisk0\Partition0
Object: fffff8a000154200  Type: (fffffa8006ca8de0) SymbolicLink
    ObjectHeader: fffff8a0001541d0 (new version)
    HandleCount: 0  PointerCount: 1
    Directory Object: fffff8a0001f8eb0  Name: Partition0
    Target String is ‘\Device\Harddisk0\DR0’

なお、この辺の動作は「インサイド Windows 第四版 第 10 章 ストレージ管理」 に書いてあります。

ダイレクト I/O 周りの動作をデバッグしてもいいのですが、それは今後検討するとして、今回はシンボリック リンク名とドライブ文字のマッピングについてのプログラムを書きました。例えば、C: ドライブは PhysicalDrive 何番なのか、逆に PhysicalDrive1 にはどのドライブ文字が割り当てられているのか、という点について調べようとすると、ビルトインでいいツールがないのです。いちいち Winobj やらデバッガーを使うのも鶏を割くのに牛刀を用いる感じがします。

プログラムはこんな感じです。
ご覧のとおり WMI を使いました。WMI だと間接的に情報を取ってくることになり、一次情報を取っていない気がして本当は嫌なのですが、他にシンプルな方法が思い浮かばなくて仕方なく、です。VDS である程度の情報は取ってこれるんですけどね。
なお、このプログラムでは CD-ROM ドライブや、ドライブ文字が割り当てられていないボリュームが出力されません。ダメダメです。

//
// wmidata.cpp
//

#include <Windows.h>
#include <stdio.h>
#include <strsafe.h>

#include <comdef.h>
#include <Wbemidl.h>

#pragma comment(lib, "wbemuuid.lib")

typedef struct _DRIVEINFO {
    WCHAR DriveLetter;
    UINT DiskIndex;
    UINT PartitionIndex;
} DRIVEINFO, *PDRIVEINFO;

class CWmiService {
private:
    IWbemLocator *mLocator;
    IWbemServices *mService;

public:
    CWmiService();
    ~CWmiService();

    BOOL Initialize();

    inline operator IWbemServices*() const {
        return mService;
    }

    IWbemClassObject *GetObject(IWbemClassObject*, LPCWSTR);
    UINT GetUint32(IWbemClassObject*, LPCWSTR);
    VOID GetString(IWbemClassObject*, LPCWSTR, PWSTR, ULONG);

    ULONG GetCount(LPCWSTR);

};

CWmiService::CWmiService()
    : mLocator(NULL), mService(NULL)
{}

CWmiService::~CWmiService() {
    if ( mLocator ) mLocator->Release();
    if ( mService ) mService->Release();
    CoUninitialize();
}

// http://technet.microsoft.com/ja-jp/library/aa390423(v=vs.85).aspx
BOOL CWmiService::Initialize() {
    HRESULT Result;
   
    Result= CoInitializeEx(0, COINIT_MULTITHREADED);
    if ( FAILED(Result) ) {
        wprintf(L"CoInitializeEx failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= CoInitializeSecurity(NULL, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_DEFAULT,
        RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"CoInitializeSecurity failed – 0x%08x\n", Result);
        return FALSE;
    }
   
    Result = CoCreateInstance(CLSID_WbemLocator,
        0, CLSCTX_INPROC_SERVER,
        IID_IWbemLocator, (LPVOID *)&mLocator);
    if ( FAILED(Result) ) {
        wprintf(L"CoCreateInstance failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= mLocator->ConnectServer(_bstr_t(L"ROOT\\CIMV2"),
         NULL, NULL, 0, NULL, 0, 0,&mService);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemLocator::ConnectServer failed – 0x%08x\n", Result);
        return FALSE;
    }

    Result= CoSetProxyBlanket(mService,
       RPC_C_AUTHN_WINNT,
       RPC_C_AUTHZ_NONE,
       NULL,
       RPC_C_AUTHN_LEVEL_CALL,
       RPC_C_IMP_LEVEL_IMPERSONATE,
       NULL, EOAC_NONE);
    if ( FAILED(Result) ) {
        wprintf(L"CoSetProxyBlanket failed – 0x%08x\n", Result);
        return FALSE;
    }

    return TRUE;
}

IWbemClassObject *CWmiService::GetObject(IWbemClassObject *Object, LPCWSTR Property) {
    HRESULT Result;
    IWbemClassObject *Ret= NULL;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);
        return NULL;
    }

    Result= mService->GetObject(Value.bstrVal,
        WBEM_FLAG_RETURN_WBEM_COMPLETE,
        NULL, &Ret, NULL);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::GetObject failed – 0x%08x\n", Result);
        Ret= NULL;
    }

    VariantClear(&Value);

    return Ret;
}

UINT CWmiService::GetUint32(IWbemClassObject *Object, LPCWSTR Property) {
    LRESULT Result;
    UINT Ret= 0;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( SUCCEEDED(Result) )
        Ret= Value.uintVal;
    else
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);

    VariantClear(&Value);

    return Ret;
}

VOID CWmiService::GetString(IWbemClassObject *Object, LPCWSTR Property,
                            PWSTR Buffer, ULONG BufferLength) {
    LRESULT Result;
    VARIANT Value;

    Result= Object->Get(Property, 0, &Value, NULL, NULL);
    if ( SUCCEEDED(Result) ) {
        StringCchCopy(Buffer, BufferLength, Value.bstrVal);
        VariantClear(&Value);
    }
    else {
        wprintf(L"IWbemClassObject::Get failed – 0x%08x\n", Result);
        return;
    }
}

ULONG CWmiService::GetCount(LPCWSTR Query) {
    LRESULT Result;
    ULONG Ret= 0;
    CONST INT BatchCount= 100;
    IWbemClassObject *Object[BatchCount];
    IEnumWbemClassObject *Enumerator= NULL;
   
    Result= mService->ExecQuery(bstr_t("WQL"), bstr_t(Query),
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
        NULL, &Enumerator);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::ExecQuery failed – 0x%08x\n", Result);
        return 0;
    }

    // http://msdn.microsoft.com/en-us/library/windows/desktop/aa394175(v=vs.85).aspx
    while ( Enumerator ) {
        ULONG Count= 0;
        Result= Enumerator->Next(WBEM_INFINITE, BatchCount, Object, &Count);
        if ( Count==0 ) break;
        if ( FAILED(Result) ) {
            wprintf(L"IEnumWbemClassObject::Next failed – 0x%08x\n", Result);
            break;
        }
        Ret+= Count;

        for ( ULONG i=0 ; i<Count ; ++i )
            Object[i]->Release();
    }

    Enumerator->Release();

    return Ret;
}

typedef int (__cdecl *PCOMPAREFUNC)(const void *, const void *);

int CompareDriveInfo_Index(CONST PDRIVEINFO p1, CONST PDRIVEINFO p2) {
    if ( p1->DiskIndex==p2->DiskIndex )
        return p1->PartitionIndex-p2->PartitionIndex;
    else
        return p1->DiskIndex-p2->DiskIndex;
}

int CompareDriveInfo_Drive(CONST PDRIVEINFO p1, CONST PDRIVEINFO p2) {
    return p1->DriveLetter-p2->DriveLetter;
}

// http://msdn.microsoft.com/en-us/library/windows/desktop/aa394175(v=vs.85).aspx
VOID DumpDiskDriveMapping() {
    CONST WCHAR QUERY_MAPPING[]= L"SELECT * FROM Win32_LogicalDiskToPartition";
    LRESULT Result;
    ULONG Count= 0;
    IEnumWbemClassObject *Enumerator= NULL;
    IWbemClassObject **Object= NULL;
    IWbemClassObject *Partition= NULL; // Antecedent
    IWbemClassObject *LogicalDisk= NULL; // Dependent
    PDRIVEINFO DriveInfo= NULL;
    ULONG i;

    CWmiService wmi;
    wmi.Initialize();
   
    ULONG MappedDrives= wmi.GetCount(QUERY_MAPPING);
    if ( MappedDrives==0 ) {
        wprintf(L"No drives?\n");
        goto cleanup;
    }

    DriveInfo= new DRIVEINFO[MappedDrives];
    Object= new IWbemClassObject*[MappedDrives];
    if ( !DriveInfo || !Object ) {
        wprintf(L"Memory allocation error – 0x%08x\n", GetLastError());
        goto cleanup;
    }

    Result= ((IWbemServices*)wmi)->ExecQuery(
        bstr_t("WQL"), bstr_t(QUERY_MAPPING),
        WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY,
        NULL, &Enumerator);
    if ( FAILED(Result) ) {
        wprintf(L"IWbemServices::ExecQuery failed – 0x%08x\n", Result);
        goto cleanup;
    }

    Result= Enumerator->Next(WBEM_INFINITE, MappedDrives, Object, &Count);
    if ( FAILED(Result) ) {
        wprintf(L"IEnumWbemClassObject::Next failed – 0x%08x\n", Result);
        goto cleanup;
    }
   
    for ( i=0 ; i<Count ; ++i ) {
        Partition= wmi.GetObject(Object[i], L"Antecedent");
        if ( Partition ) {
            //
http://msdn.microsoft.com/en-us/library/windows/desktop/aa394135(v=vs.85).aspx
            DriveInfo[i].PartitionIndex= wmi.GetUint32(Partition, L"Index");
            DriveInfo[i].DiskIndex= wmi.GetUint32(Partition, L"DiskIndex");
            Partition->Release();
        }

        LogicalDisk= wmi.GetObject(Object[i], L"Dependent");
        if ( LogicalDisk ) {
            //
http://msdn.microsoft.com/en-us/library/windows/desktop/aa394173(v=vs.85).aspx
            WCHAR Drive[3];
            wmi.GetString(LogicalDisk, L"DeviceID", Drive, 2);
            DriveInfo[i].DriveLetter= Drive[0];
            LogicalDisk->Release();
        }

        Object[i]->Release();
    }

    qsort(DriveInfo, Count, sizeof(DRIVEINFO), (PCOMPAREFUNC)CompareDriveInfo_Index);

    // http://otndnld.oracle.co.jp/document/products/oracle10g/102/windows/B25695-02/ap_raw.htm

    int diskindex= -1;
    for ( i=0 ; i<Count ; ++i ) {
        if ( diskindex!=DriveInfo[i].DiskIndex ) {
            wprintf(L"\n\\\\.\\PhysicalDrive%d\n", DriveInfo[i].DiskIndex);
            diskindex= DriveInfo[i].DiskIndex;
        }

        wprintf(L"  \\\\.\\%c: => \\Device\\Harddisk%d\\Partition%d\n",
            DriveInfo[i].DriveLetter,
            DriveInfo[i].DiskIndex,
            DriveInfo[i].PartitionIndex+1,
            DriveInfo[i].DiskIndex);
    }

    //putwchar(L’\n’);
    //qsort(DriveInfo, Count, sizeof(DRIVEINFO), (PCOMPAREFUNC)CompareDriveInfo_Drive);
    //
    //for ( i=0 ; i<Count ; ++i ) {
    //    wprintf(L"  %c: => \\Device\\Harddisk%d\\Partition%d : \\PhysicalDrive%d\n",
    //        DriveInfo[i].DriveLetter,
    //        DriveInfo[i].DiskIndex,
    //        DriveInfo[i].PartitionIndex+1,
    //        DriveInfo[i].DiskIndex);
    //}

cleanup:
    if ( Object ) delete [] Object;
    if ( DriveInfo ) delete [] DriveInfo;
    if ( Enumerator ) Enumerator->Release();

}

出力結果はこんな感じになります。

出力例 1.

\\.\PhysicalDrive0
  \\.\V: => \Device\Harddisk0\Partition2

\\.\PhysicalDrive1
  \\.\C: => \Device\Harddisk1\Partition1
  \\.\H: => \Device\Harddisk1\Partition2
  \\.\T: => \Device\Harddisk1\Partition3

\\.\PhysicalDrive2
  \\.\E: => \Device\Harddisk2\Partition1
  \\.\W: => \Device\Harddisk2\Partition2

\\.\PhysicalDrive3
  \\.\F: => \Device\Harddisk3\Partition1

出力例 2.

\\.\PhysicalDrive0
  \\.\C: => \Device\Harddisk0\Partition2
  \\.\N: => \Device\Harddisk0\Partition4
  \\.\O: => \Device\Harddisk0\Partition4

\\.\PhysicalDrive1
  \\.\E: => \Device\Harddisk1\Partition1
  \\.\F: => \Device\Harddisk1\Partition2
  \\.\G: => \Device\Harddisk1\Partition3
  \\.\H: => \Device\Harddisk1\Partition4
  \\.\I: => \Device\Harddisk1\Partition5
  \\.\J: => \Device\Harddisk1\Partition6
  \\.\K: => \Device\Harddisk1\Partition7
  \\.\L: => \Device\Harddisk1\Partition8
  \\.\M: => \Device\Harddisk1\Partition9

パーティション番号が抜けているところは、ドライブ文字が割り当てられていないボリュームです。
出力例 2. で \Device\Harddisk0\Partition4 が 2 つあるのは、これが拡張パーティションで、論理ドライブが 2 つあるという意味です。MBR の制限ですね。一方 PhysicalDrive1 は GPT パーティショニングなので、9 つのパーティションでも問題なしです。

今回は C++ プログラムで処理しましたが、WMI については、やはり PowerShell でアクセスするのが一番楽です。他にも wmic やら wbemtest、VB スクリプトを使うなどの方法もあります。しかし、Windows 7 (2008 R2) なら PowerShell の Get-WMIObject と Set-WmiInstance コマンドレットを使わない手はありません。例え起動が遅いと言われようとも、これだけのために PowerShell を使う価値はあります。

ソース中のコメントに入れてありますが、今回のプログラムは Win32_DiskDriveToDiskPartition クラスのインスタンスを適当に成形しているだけです。

PS > Get-WmiObject Win32_DiskDriveToDiskPartition | fl Antecedent,Dependent

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #0"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #1"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE1"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #1, Partition #2"

Antecedent : \\ALANINE\root\cimv2:Win32_DiskDrive.DeviceID="\\\\.\\PHYSICALDRIVE0"
Dependent  : \\ALANINE\root\cimv2:Win32_DiskPartition.DeviceID="Disk #0, Partition #0"

(2015/1/8 追記)
意外とこの記事へのアクセス数が多いこともあり、3 年近く前の記事への追記です。とても便利なビルトインのツールがありました。Windows Server 2003 のサポート ツールに含まれている dmdiag.exe です。

Windows Server 2003 Service Pack 2 32-bit Support Tools
http://www.microsoft.com/en-us/download/details.aspx?id=15326

管理者権限で dmdiag.exe に -v オプションをつけて実行すると、ストレージ周りの情報をかなり詳細にダンプしてくれます。古いツールですが、少なくとも Windows 8.1 (Server 2012 R2) までなら動作します。以下、出力の抜粋です。

d:\MSWORK>.\dmdiag  -v
---------- Computer Name and OS Version ---------- 
       Computer name: GUANOSINE
            NT build: 9200
            CPU Type: x86
      DMDIAG Version: 5.2.3790.0 shp
---------- LDM File Versions ----------
(..snip..)
---------- Mount Points ----------
---------- Drive Letter Usage, Drive Type ----------
C: = \Device\HarddiskVolume6 [Fixed]
D: = \Device\HarddiskVolume8 [Fixed]
E: = \Device\HarddiskVolume4 [Fixed]
Q: = \Device\CdRom0 [CDRom]
U: = \Device\HarddiskVolume3 [Fixed]
V: = \Device\HarddiskVolume2 [Fixed]
W: = \Device\HarddiskVolume7 [Fixed]
X: = \Device\HarddiskVolume5 [Fixed]
---------- Consolidated LDM Configuration Data ----------
(..snip..)
---------- \Device\Harddisk0 ----------
\Device\Harddisk0\DR0            (Device)
\Device\Harddisk0\Partition0     (SymbolicLink) -> \Device\Harddisk0\DR0
\Device\Harddisk0\Partition1     (SymbolicLink) -> \Device\HarddiskVolume1
\Device\Harddisk0\Partition2     (SymbolicLink) -> \Device\HarddiskVolume2
---------- \Device\Harddisk1 ----------
\Device\Harddisk1\DR1            (Device)
\Device\Harddisk1\Partition0     (SymbolicLink) -> \Device\Harddisk1\DR1
\Device\Harddisk1\Partition1     (SymbolicLink) -> \Device\HarddiskVolume3
---------- \Device\Harddisk2 ----------
\Device\Harddisk2\DR2            (Device)
\Device\Harddisk2\Partition0     (SymbolicLink) -> \Device\Harddisk2\DR2
\Device\Harddisk2\Partition1     (SymbolicLink) -> \Device\HarddiskVolume4
\Device\Harddisk2\Partition2     (SymbolicLink) -> \Device\HarddiskVolume5
---------- \Device\Harddisk3 ----------
\Device\Harddisk3\DR3            (Device)
\Device\Harddisk3\Partition0     (SymbolicLink) -> \Device\Harddisk3\DR3
\Device\Harddisk3\Partition1     (SymbolicLink) -> \Device\HarddiskVolume6
\Device\Harddisk3\Partition2     (SymbolicLink) -> \Device\HarddiskVolume7
\Device\Harddisk3\Partition3     (SymbolicLink) -> \Device\HarddiskVolume8
---------- Partition Table Info Disk 0 ---------- 
          14,593 Cylinders
             255 Tracks/Cylinder
              63 Sectors/Track
             512 Bytes/Sector
              12 MediaType
     234,436,545 Sectors (total)
120,031,511,040 Bytes (total)
     117,218,273 KB
         114,471 MB
           111.8 GB

               0 StartingOffset
120,034,123,776 PartitionLength
               0 HiddenSectors
               0 PartitionNumber
               0 PartitionType
               0 BootIndicator
               0 RecognizedPartition
               0 RewritePartition

             MBR PartitionStyle
               4 PartitionCount
        3afc7bc6 Signature

        Starting        Partition     Hidden       Total  Partition    Partition       Boot Recognized    Rewrite
  Offset (bytes)   Length (bytes)    Sectors     Sectors     Number   Type (HEX)  Indicator  Partition  Partition

       1,048,576      104,857,600      2,048     204,800          0         0x07          1          1          0
     105,906,176  119,925,637,120    206,848 234,229,760          1         0x07          0          1          0
               0                0          0           0          2         0x00          0          0          0
               0                0          0           0          3         0x00          0          0          0

120,031,511,040 Bytes (234436545 sectors) Geometric size
120,034,123,776 Bytes (234441648 sectors) True size (measured)
120,034,123,776 Bytes (234441648 sectors) Reported size (Partition0)
               0 Bytes (       0 sectors) missing/wasted

---------- Partition Table Info Disk 1 ----------
(..snip..)
---------- Partition Table Info Disk 2 ----------
(..snip..)
---------- Partition Table Info Disk 3 ----------
(..snip..)
---------- DMIO Kernel List ----------
(..snip..)

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #4

これまでの記事で、RPC サーバーと RPC クライアントができました。
サーバー間で RPC 通信ができるようになったので、とりあえず Network Monitor でパケット キャプチャーを取って見てみます。

まずは名前付きパイプによる通信から。

image

ごちゃごちゃしていますが、よく見れば内容は単純です。

サーバー間の名前付きパイプは、SMB による通信が行われます。上のパケットは Windows Server 2008 R2 の環境で取ったので、SMB 2.0 による通信が行われています。したがって、サーバー側のポート番号は 445/tcp です。

ファイル共有にアクセスするときと同様に、Negotiate → Session Setup → Tree Connect → Create というように SMB コマンドが実行されていきます。

一つ目のポイントは、Tree Connect コマンドでの接続先エンド ポイントが \\サーバー名\IPC$ となることです。ファイル共有の時は \\サーバー名\共有名 がエンドポイントになりますが、名前付きパイプの時は必ず IPC$ を見に行きます。ちなみに IPC は Interprocess Communications の略です。

Create するときのファイル名がパイプ名になります。\pipe\test というパイプを使った通信では、test というファイルを開きます。イメージとしては、\\サーバー名\IPC$ という共有フォルダーの中に test というファイルがあって、それを CreateFile するイメージです。

SMB Create の後に、RPC のバインド処理が行われます。プロトコルの階層を見ると、これは SMB Write コマンドによって行われていることがわかります。ようは WriteFile です。後の通信も、SMB の Write やら Read やらで行われます。これが名前付きパイプによる RPC 通信です。

image

それともう一つ、これは SMB 通信なので、Session Setup ではユーザー認証が行われます。今回の検証は Active Directory ドメイン環境で行っていて、AsyncClient の接続先には FQDN を入力したので Kerberos 認証が行われます。IP アドレスの場合は NTLM 認証が行われます。Session Setup の要求パケットを見ていくと分かりますが、通常の SMB 通信と同様に cifs のサービス チケットを提示しています。

では次に、TCP/IP による RPC を見てみます。
もちろん名前付きパイプの通信も TCP/IP なので、ネイティブ TCP/IP とでも言いましょうか。

image

表示の上では余計にごちゃごちゃしていますが、短くてシンプルなのが分かると思います。
このキャプチャーは、メソッドを続けて 2 回呼び出したときのものなので、要求1 → 要求2 → 応答1 → 応答2 という順番になっています。1 回のメソッド実行の流れは非常に単純です。TCP セッション確立 → RPC Bind → Request → Response という 4 段階だけです。

Network Monitor のサマリーにおいて、名前付きパイプのときもプロトコル名が MSRPC になっていましたが、今回の場合とはプロトコル階層が異なります。もちろん今回は SMB 通信は一切関係ありません。ポート番号はアプリケーションが指定しています。RPC の動的ポート割り当ても使うことができます。そのときは RPC サーバーでポートをバインドするときに RpcServerUseProtseqEx API を使います。

image

名前付きパイプでもネイティブ TCP/IP でもいいのですが、RPC Bind メッセージを見ると、3 種類の GUID がクライアントからの要求に含まれているのが分かります。下の抜粋がそうです。

– PContElem [0]
  – AbstractSyntax
    + IfUuid: {161B9AB8-1A96-40A6-BF8B-AA2D7EC94B6D}
      IfVersion: 1 (0x1)
  – TransferSyntaxes
    + IfUuid: {8A885D04-1CEB-11C9-9FE8-08002B104860}
      IfVersion: 2 (0x2)

– PContElem [1]
    PContId: 1 (0x1)
    NTransferSyn: 1 (0x1)
    Reserved: 0 (0x0)
  – AbstractSyntax
    + IfUuid: {161B9AB8-1A96-40A6-BF8B-AA2D7EC94B6D}
      IfVersion: 1 (0x1)
  – TransferSyntaxes
    + BTFNUuid: {6CB71C2C-9812-4540-0300000000000000}
      IfVersion: 1 (0x1)

image

既にお気づきと思いますが、この中の 2 つは、インターフェース定義に登場しています。プログラムを書く最初に uuidgen で idl ファイルのひな型を作成しましたが、AbstractSyntax の IfUuid は、この時の IDL に埋め込まれていた UUID に一致します。

[
uuid(161b9ab8-1a96-40a6-bf8b-aa2d7ec94b6d),
version(1.0)
]
interface pipo
{
    void RpcSleep(int Duration);
    void RpcSleepAsync(int Duration);
    void Shutdown();
}

midl でコンパイルして生成されたクライアント用ソース ファイル pipo_c.cpp には以下のような定数がありました。

static const RPC_CLIENT_INTERFACE pipo___RpcClientInterface = {
  sizeof(RPC_CLIENT_INTERFACE),
  {{0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}},
    {1,0}},
  {{0x8A885D04,0x1CEB,0x11C9,{0x9F,0xE8,0x08,0x00,0x2B,0x10,0x48,0x60}},
    {2,0}},
  0,
  0,
  0,
  0,
  0,
  0x00000000
};
RPC_IF_HANDLE pipo_v1_0_c_ifspec = (RPC_IF_HANDLE)& pipo___RpcClientInterface;

AbstractSyntax と TransferSyntaxes の IfUuid に加え、バージョン番号もここで定義されています。
「Transfer Syntax って何よ」って話ですが、それはここに書いてあります。

RPC transfer syntax: A method for encoding messages defined in an Interface Definition Language (IDL) file. Remote procedure call (RPC) can support different encoding methods or transfer syntaxes.
http://msdn.microsoft.com/en-us/library/cc232140(v=prot.10).aspx#rpc_transfer_syntax

ということで、エンコードの方法を示しているようです。ということは、インターフェース UUID と違って、ランダムに生成されているものではないということです。

今回使われている {8A885D04-1CEB-11C9-9FE8-08002B104860} は NDR (Network Data Representation) 2.0 という形式であることを示しています。まあ、これ以上深追いするのは止めておきましょう。すべて [MS-RPCE] の仕様書に書いてあるので、時間があるときにお読み下さい。残念ながら私は一部しか読んでいません・・。

2.2.4.12 NDR Transfer Syntax Identifier
http://msdn.microsoft.com/en-us/library/cc243843(v=PROT.13).aspx

http://msdn.microsoft.com/en-us/library/33b94545-9ae1-4cc8-9ce5-4be893b7bec3(v=prot.13)#NDR

最後に残った {6CB71C2C-9812-4540-0300000000000000} についても、仕様書に書いてあります。これも固定値のようですね。

3.3.1.5.3 Bind Time Feature Negotiation
http://msdn.microsoft.com/en-us/library/cc243715(v=PROT.13).aspx

以上が RPC Bind に含まれる GUID でした。

名前付きパイプのときとは異なり、今回のようなシンプルなメソッドでは認証 (+認可) 動作が発生しません。

わりと後半はぐだぐだになってしまいました (力尽きた・・・) が、サーバー間の非同期 RPC のシリーズはこのへんにしておきます。
他に遊ぶとすれば、デバッガーをアタッチして RPC メソッド呼び出し時のモジュールの動きを、カーネル/ユーザー モード、サーバー/クライアントのそれぞれで見てみると面白いと思います。

例えば、RPC サーバーのメソッドは以下のようなスタックで呼び出されています。

0:007> k
Child-SP          RetAddr           Call Site
00000000`02bcf1c8 000007fe`fe5b23d5 AsyncServer!RpcSleepAsync
00000000`02bcf1d0 000007fe`fe65f695 RPCRT4!Invoke+0x65
00000000`02bcf220 000007fe`fe5a50f4 RPCRT4!NdrAsyncServerCall+0x29c
00000000`02bcf300 000007fe`fe5a4f56 RPCRT4!DispatchToStubInCNoAvrf+0x14
00000000`02bcf330 000007fe`fe59d879 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x146
00000000`02bcf450 000007fe`fe59d6de RPCRT4!OSF_SCALL::DispatchHelper+0x159
00000000`02bcf570 000007fe`fe6527b4 RPCRT4!OSF_SCALL::ProcessReceivedPDU+0x18e
00000000`02bcf5e0 000007fe`fe5ec725 RPCRT4!OSF_SCALL::BeginRpcCall+0x134
00000000`02bcf610 000007fe`fe59d023 RPCRT4!Invoke+0x2adf9
00000000`02bcf6c0 000007fe`fe59d103 RPCRT4!CO_ConnectionThreadPoolCallback+0x123
00000000`02bcf770 000007fe`fe15898f RPCRT4!CO_NmpThreadPoolCallback+0x3f
00000000`02bcf7b0 00000000`77c5098a KERNELBASE!BasepTpIoCallback+0x4b
00000000`02bcf7f0 00000000`77c5feff ntdll!TppIopExecuteCallback+0x1ff
00000000`02bcf8a0 00000000`779e652d ntdll!TppWorkerThread+0x3f8
00000000`02bcfba0 00000000`77c6c521 kernel32!BaseThreadInitThunk+0xd
00000000`02bcfbd0 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #3

次に RPC クライアントです。

4. RPC クライアントを書く

同期 RPC のときは、適当なプロトコルを選んでバインドしてから、自動生成されたスタブを呼び出すだけでしたが、非同期 RPC ではもっと面倒です。コールバックの仕組みを自分で用意しなけれななりません。最初に書いたように、今回は I/O 完了ポートを使ってみます。面倒です。

ファイルは、RPC クライアントと同じく 3 つ。ですが、クラスは 2 つ用意します。

  • AsyncClient.h ・・・ AsyncRpcHandler クラス、CAsyncClient クラスの宣言
  • AsyncClient.cpp ・・・ AsyncRpcHandler クラス、CAsyncClient クラスの定義
  • main.cpp ・・・ WinMain、ウィンドウ処理

IDL ファイルのインターフェース定義で書いたように、今回実装する非同期 RPC メソッドは RpcSleepAsync の 1 つだけです。AsyncRpcHandler クラスは、この RpcSleepAsync のスタブを呼び出す処理と、コールバック処理を実装します。

CAsyncClient クラスは、I/O 完了ポートを使ったコールバックの処理を実装します。具体的には、I/O 完了ポート用のワーカー スレッドを準備し、RPC サーバーからコールバックが来たら AsyncRpcHandler の街頭メソッドを呼び出すという処理を行ないます。

RPC クライアントを C 言語だけで書くのは辛そうです。

AsyncClient.h

上に書いたように、2 つのクラスのプロトタイプ宣言です。
NUMBER_OF_THREADS で、I/O 完了ポートで使う待機スレッドの最大数を指定します。

//
// AsyncClient.h
//

#pragma once

#include "resource.h"
#include "..\AsyncCommon.h"

extern HWND g_Dlg;

#define NUMBER_OF_THREADS 5

typedef struct _METHOD_CONTEXT {
    DWORD SessionID;
    DWORD Status;
    OVERLAPPED Overlapped;
} METHOD_CONTEXT, *PMETHOD_CONTEXT;

class AsyncRpcHandler {
private:
    HANDLE mCompletionPort;

    RPC_ASYNC_STATE mAsyncState;
    METHOD_CONTEXT mContext;

public:
    AsyncRpcHandler(HANDLE);
    ~AsyncRpcHandler() {}

    VOID Sleep(DWORD);
    BOOL ProcessComplete();

};

class CAsyncClient {
private:
    HANDLE mCompletionPort;
    HANDLE mThreads[NUMBER_OF_THREADS];
   
    RPC_PROTOCOL_TYPE mProtocol;

    static DWORD CALLBACK WorkerThreadStart(PVOID);
    DWORD WorkerThread();

public:
    WCHAR mEndpoint[MAX_ENDPOINT];
    WCHAR mServer[MAX_ENDPOINT];

    CAsyncClient();
    ~CAsyncClient();

    inline operator HANDLE() const { return mCompletionPort; }
   
    BOOL InitializeThreadPool();
    BOOL Bind();
   
    inline VOID SetProtocolType(LRESULT l) {
        mProtocol= (RPC_PROTOCOL_TYPE)min(l, Rpc_NotSupported);
    }

};

AsyncClient.cpp

クラスを実装します。このファイルにエッセンスがいろいろ詰まっています。

まず、AsyncRpcHandler::Sleep がスタブを呼び出す処理です。同期 RPC と違うのは RPC_ASYNC_STATE をスタブに渡す必要がある点です。ACF ファイルで指定した [async] 属性によって、非同期のスタブとしてプロトタイプ宣言が生成されています。
RpcAsyncInitializeHandle API で RPC_ASYNC_STATE 構造体の Size, Signature, Lock, StubInfo メンバーを埋めてもらいます。その他のメンバーは自分で埋める必要があります。ここで、コールバックの種類や、ユーザー定義データを設定します。今回は I/O 完了ポートを使うので NotificationTypeIoc に RpcNotificationTypeIoc を指定します。
非同期 RPC なので、RPC サーバーの処理に関係なく AsyncRpcHandler::Sleep の処理は滞りなく終了します。

コールバックを受け取った後の処理が AsyncRpcHandler::ProcessComplete  です。これは CAsyncServer クラスの処理として、コールバックが来たときに ProcessComplete メンバーを呼び出すように実装しています。ProcessComplete で重要なのは、RpcAsyncCompleteCall API の実行です。この API は RPC サーバーにおける RPC メソッド本体の RpcSleepAsync 関数でも呼び出していました。
クライアント側で RpcAsyncCompleteCall を実行することで、サーバー側の RpcAsyncCompleteCall に第二引数として渡した戻り値を受け取ることができます。したがって基本的には、クライアントが RpcAsyncCompleteCall を呼び出すのはサーバー側の処理後である必要があります。もし、サーバーが RpcAsyncCompleteCall を呼び出していない段階でクライアントが RpcAsyncCompleteCall を呼ぶと、戻り値が RPC_S_ASYNC_CALL_PENDING となり、判別できます。

CAsyncClient クラスは、I/O 完了ポート関連の処理です。InitializeThreadPool で CreateIoCompletionPort API を実行し、ワーカー スレッドを必要なだけ (ここでは NUMBER_OF_THREADS 定数で指定した分だけ) 作ります。
CAsyncClient::WorkerThread が I/O 完了ポートのワーカースレッドであり、GetQueuedCompletionStatus API で待機に入ります。サーバー側の RPC 処理が完了すると GetQueuedCompletionStatus から制御が返ってくるので、上で説明した完了ルーチンである AsyncRpcHandler::ProcessComplete を実行します。
ここでのポイントは、AsyncRpcHandler クラス インスタンスへのポインターを Overlapped を使って取得している点です。上の説明では飛ばしましたが、クライアントのコールバック関数で RpcAsyncCompleteCall を呼び出す場合に、第一引数に RPC_SYNC_STATE 構造体を渡す必要があります。このとき、メソッドを呼び出す際に指定した RPC_ASYNC_STATE と Signature などの値が一致していないとおかしな動作になります。つまり、まだ実行中のメソッドや、そもそも呼び出してさえいない Signature である RPC_ASYNC_STATE を使って RpcAsyncCompleteCall を呼び出すと、例外が発生します。そのため、AsyncRpcHandler::Sleep の中でメソッド実行時に mAsyncState.u.IOC.lpOverlapped に this ポインターを渡しています。コールバックが来たときに GetQueuedCompletionStatus によって取得される Overlapped には、メソッド呼び出し時の this ポインターが含まれているため、これを使って ProcessComplete を呼び出すことで、メソッド呼び出し時と同じ AsyncRpcHandler クラス インスタンスを保証することができます。

//
// AsyncClient.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncClient.h"
#include "..\idl\pipo.h"

#pragma comment(lib, "rpcrt4.lib")

void __RPC_FAR * __RPC_API midl_user_allocate(size_t len) {
    return(malloc(len));
}

void __RPC_API midl_user_free(void __RPC_FAR * ptr) {
    free(ptr);
}

AsyncRpcHandler::AsyncRpcHandler(HANDLE Port)
    : mCompletionPort(Port)
{}

VOID AsyncRpcHandler::Sleep(DWORD Duration) {
    RPC_STATUS Status= RPC_S_OK;

    Status = RpcAsyncInitializeHandle(&mAsyncState, sizeof(RPC_ASYNC_STATE));
    if (Status) {
        LOGERROR(L"RpcAsyncInitializeHandle failed – 0x%08x", Status);
        return;
    }

    mContext.SessionID= rand();

    mAsyncState.UserInfo = NULL;
    mAsyncState.NotificationType = RpcNotificationTypeIoc;
    mAsyncState.u.IOC.hIOPort= mCompletionPort;
    mAsyncState.u.IOC.lpOverlapped= (LPOVERLAPPED)this;
    mAsyncState.u.IOC.dwCompletionKey= 1;
    mAsyncState.u.IOC.dwNumberOfBytesTransferred= sizeof(AsyncRpcHandler);

     RpcTryExcept
        RpcSleepAsync(&mAsyncState, Duration);
    RpcExcept( EXCEPTION_EXECUTE_HANDLER )
        LOGERROR(L"RPC exception – 0x%08x", RpcExceptionCode());
    RpcEndExcept

    LOGINFO(L"(SleepAsync) invoked. sessid:0x%08x", mContext.SessionID);
}

BOOL AsyncRpcHandler::ProcessComplete() {
    RPC_STATUS Status;
    PVOID Reply= NULL;

    Status= RpcAsyncCompleteCall(&mAsyncState, Reply);
    if ( Status==RPC_S_ASYNC_CALL_PENDING )
        return TRUE;

    if ( Status!=RPC_S_OK ) {
        LOGERROR(L"RpcAsyncCompleteCall failed – 0x%08x", Status);
        return FALSE;
    }

    LOGINFO(L"(SleepAsync) done. sessid:0x%08x", mContext.SessionID);

    delete this;

    return TRUE;
}

CAsyncClient::CAsyncClient()
    : mCompletionPort(NULL) {
    ZeroMemory(mThreads, NUMBER_OF_THREADS*sizeof(HANDLE));
}

CAsyncClient::~CAsyncClient() {
    if ( pipo_IfHandle )
        RpcBindingFree(&pipo_IfHandle);
   
    if ( mCompletionPort!=NULL )
        CloseHandle(mCompletionPort);

    WaitForMultipleObjects(NUMBER_OF_THREADS, mThreads, TRUE, INFINITE);

    for ( int i=0 ; i<NUMBER_OF_THREADS ; ++i ) {
        if ( mThreads[i] )
            CloseHandle(mThreads[i]);
    }
}

DWORD CALLBACK CAsyncClient::WorkerThreadStart(PVOID Param) {
    if ( Param )
        return ((CAsyncClient*)Param)->WorkerThread();

    return 0;
}

DWORD CAsyncClient::WorkerThread() {
    BOOL Ret= FALSE;
    DWORD BytesTransferred= 0;
    ULONG_PTR CompletionKey= NULL;
    LPOVERLAPPED Overlapped= NULL;

    do {
        Ret= GetQueuedCompletionStatus(
            mCompletionPort,
            &BytesTransferred,
            &CompletionKey,
            &Overlapped,
            INFINITE);
        if ( !Ret ) {
            LOGERROR(L"GetQueuedCompletionStatus failed – 0x%08x\n",
              GetLastError());
            goto cleanup;
        }
       
        if ( !((AsyncRpcHandler*)Overlapped)->ProcessComplete() )
            break;
    } while (1);

cleanup:
    ExitThread(0);
    return 0;
}

BOOL CAsyncClient::InitializeThreadPool() {
    BOOL Ret= FALSE;

    if ( !mCompletionPort ) {
        mCompletionPort= CreateIoCompletionPort(INVALID_HANDLE_VALUE,
            NULL, NULL, NUMBER_OF_THREADS);
        if ( mCompletionPort==NULL ) {
            LOGERROR(L"CreateIoCompletionPort failed – 0x%08x",
              GetLastError());
            goto cleanup;
        }
    }

    for ( int i=0 ; i<NUMBER_OF_THREADS ; ++i ) {
        if ( mThreads[i]==NULL ) {
            mThreads[i]= CreateThread(NULL, 0, WorkerThreadStart,
              this, 0, NULL);
            if ( mThreads[i]==NULL )
                LOGERROR(L"CreateThread failed – 0x%08x", GetLastError());
        }
    }
   
    LOGERROR(L"Thread Pool initiated. (%d threads)", NUMBER_OF_THREADS);

    Ret= TRUE;

cleanup:
    return Ret;
}

BOOL CAsyncClient::Bind() {
    BOOL Ret= FALSE;
    RPC_STATUS Status= RPC_S_OK;
    RPC_PROTOCOL &Protocol=
      SupportedProtocols[min(mProtocol, Rpc_NotSupported)];
    RPC_WSTR BindStr= NULL;

    Status= RpcStringBindingCompose(NULL,
        (RPC_WSTR)Protocol.Name,
        (RPC_WSTR)mServer,
        (RPC_WSTR)mEndpoint, NULL, &BindStr);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcStringBindingCompose failed – 0x%08x\n", Status);
        goto cleanup;
    }

    if ( pipo_IfHandle ) {
        Status= RpcBindingFree(&pipo_IfHandle);
        if ( Status!=RPC_S_OK )
            LOGERROR(L"RpcBindingFree failed – 0x%08x\n", Status);
        pipo_IfHandle= NULL;
    }

    Status= RpcBindingFromStringBinding(BindStr, &pipo_IfHandle);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcBindingFromStringBinding failed – 0x%08x\n", Status);
        goto cleanup;
    }

    Ret= TRUE;

cleanup:
    if ( BindStr )
        RpcStringFree(&BindStr);

    return Ret;
}

main.cpp

最後のファイルです。RPC サーバーとほぼ同じです。

RPC の処理とは直接関係ありませんが、WM_INITDIALOG メッセージを受け取った時に CAsyncClient::InitializeThreadPool を呼び出して、I/O 完了ポートのワーカー スレッドを作成します。これは別に WinMain 関数に書いてもいいのですが、ワーカースレッドの初期化がうまくいったというログをダイアログボックスに表示させたいという理由で、ここに書いています。

エンドポイントにバインドする処理は CAsyncClient::Bind ですが、これは IDOK ボタンがクリックされたときに呼び出します。RPC クライアントを複数のプロトコルやエンドポイントに対応させるため、ボタンを押すたびに アンバインド→バインド を実行するようにしています。

最後にポイントが 1 つあります。IDOK のクリック処理の中で、AsyncRpcHandler を new 演算子で動的確保してから AsyncRpcHandler::Sleep 関数を呼び出しています。上で説明したように、メソッド呼び出し時とコールバック時に同じポインターを Overlapped として使えなければならないため、AsyncRpcHandler  をローカル インスタンスとしては使うことができない、というのがその理由です。インスタンスの解放処理は、AsyncRpcHandler::ProcessComplete の中で delete this として実行されます。このデザインが適切かどうかはあまり検証していません。

//
// main.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncClient.h"

CAsyncClient *g_AsyncClient= NULL;
HWND g_Dlg= NULL;

INT_PTR CALLBACK DlgProc(HWND Dlg, UINT Msg, WPARAM w, LPARAM l) {
    HWND Control= NULL;

    switch ( Msg ) {
    case WM_INITDIALOG:
        g_Dlg= Dlg;
       
        Control= GetDlgItem(Dlg, IDC_COMBO_PROTOCOL);

        for ( PRPC_PROTOCOL p= SupportedProtocols ;
              p->Protocol!=Rpc_NotSupported ; ++p )
            SendMessage(Control, CB_ADDSTRING, NULL,
              (LPARAM)p->FriendlyName);

        PostMessage(Control, CB_SETCURSEL, 0, NULL);
        PostMessage(Dlg, WM_COMMAND,
          MAKELONG(IDC_COMBO_PROTOCOL, CBN_SELCHANGE), (LPARAM)Control);

        g_AsyncClient->InitializeThreadPool();

        return TRUE;

    case WM_COMMAND:
        switch ( LOWORD(w) ) {
        case IDCANCEL:
            EndDialog(Dlg, IDOK);
            break;
        case IDOK:
            GetDlgItemText(Dlg, IDC_EDIT_ENDPOINT,
              g_AsyncClient->mEndpoint, MAX_ENDPOINT);
            GetDlgItemText(Dlg, IDC_EDIT_SERVER,
              g_AsyncClient->mServer, MAX_ENDPOINT);
            g_AsyncClient->SetProtocolType(SendMessage(
              GetDlgItem(Dlg, IDC_COMBO_PROTOCOL), CB_GETCURSEL, 0, 0));

            if ( g_AsyncClient->Bind() ) {
                AsyncRpcHandler *Rpc= new AsyncRpcHandler(*g_AsyncClient);
                Rpc->Sleep(1000);
            }
            break;
        case IDC_COMBO_PROTOCOL:
            if ( HIWORD(w)==CBN_SELCHANGE ) {
                LRESULT Selected= SendMessage((HWND)l, CB_GETCURSEL, 0, 0);
                SetWindowText(GetDlgItem(Dlg, IDC_EDIT_ENDPOINT),
                  SupportedProtocols[
                    min(Selected, Rpc_NotSupported)].DefaultEndpoint);
            }
            break;
        }
        break;
    }
    return FALSE;
}

int WINAPI wWinMain(HINSTANCE hInstance,
                    HINSTANCE,
                    PWSTR pCmdLine,
                    int nCmdShow) {
    g_AsyncClient= new CAsyncClient();
    if ( g_AsyncClient ) {
        DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
        delete g_AsyncClient;
    }

    return 0;
}

ダイアログボックスの外観

最後に、RPC クライアントの外観です。RPC サーバーとほとんど同じです。コピペが冴えます。

image

ここまでがコードの説明でした。
次回の記事で、作成したプログラムを使っていろいろ遊んでみます。

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #2

続きです。今回は RPC サーバーを書きます。
が、その前にクライアントとサーバーの共通コードを貼っておきます。RPC とは直接関係ないです。

2. クライアント/サーバー共通のコード

これがヘッダー。
これだけを見ても意味不明かと思いますが、、、完成までお待ちください。

//
// AsyncCommon.h
//

#pragma once

#define MAX_LOGGING 1000

extern WCHAR ErrorMsg[];

#define LOGINFO(text, code) \
if ( g_Dlg ) { \
    StringCchPrintf(ErrorMsg, MAX_LOGGING, text, code); \
    AppendWindowText(GetDlgItem(g_Dlg, IDC_EDIT1), ErrorMsg); \
}

#define LOGERROR LOGINFO

//#define LOGERROR(text, code) { \
//    StringCchPrintf(ErrorMsg, MAX_LOGGING, text, code); \
//    MessageBox(g_Dlg, ErrorMsg, L"Error", MB_ICONERROR); }

#define MAX_ENDPOINT 32

enum RPC_PROTOCOL_TYPE : unsigned int  {
    Rpc_Tcpip,
    Rpc_NamedPipe,
    Rpc_Lpc,
    Rpc_NotSupported
};

typedef struct _RPC_PROTOCOL {
    RPC_PROTOCOL_TYPE Protocol;
    WCHAR Name[MAX_ENDPOINT];
    WCHAR FriendlyName[MAX_ENDPOINT];
    WCHAR DefaultEndpoint[16];
} RPC_PROTOCOL, *PRPC_PROTOCOL;

extern RPC_PROTOCOL SupportedProtocols[];

BOOL AppendWindowText(HWND Textbox, LPCTSTR Message);

次にソースファイル。
テキストボックスへのログ表示用の関数です。あとは RPC プロトコル用の定数。まあ・・・これも完成するまでは意味不明ですね。

//
// AsyncCommon.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncCommon.h"

WCHAR ErrorMsg[MAX_LOGGING]; // used in LOGINFO, LOGERROR

RPC_PROTOCOL SupportedProtocols[]= {
    {Rpc_Tcpip,        L"ncacn_ip_tcp", L"TCP/IP",     L"50000" },
    {Rpc_NamedPipe,    L"ncacn_np",     L"Named Pipe", L"\\pipe\\asyncrpc" },
    {Rpc_Lpc,          L"ncalrpc",      L"LPC",        L"asyncrpc_lpc" },
    {Rpc_NotSupported, L"N/A",          L"N/A",        L"N/A" }
};

BOOL AppendWindowText(HWND Textbox, LPCTSTR Message) {
    if ( Message==NULL || Textbox==NULL )
        return FALSE;

    size_t Length= 0;
    if ( FAILED(StringCbLength(Message, MAX_LOGGING, &Length)) )
        Length= 0;

    Length= min(Length, MAX_LOGGING);
   
    PWSTR Buffer1= new WCHAR[MAX_LOGGING+1];
    PWSTR Buffer2= new WCHAR[MAX_LOGGING+1];

    if ( !Buffer1 || !Buffer2 )
        return FALSE;
   
    GetWindowText(Textbox, Buffer1, MAX_LOGGING);

    SYSTEMTIME st;
    GetLocalTime(&st);

    StringCchPrintf(Buffer2, MAX_LOGGING,
        L"[%d/%02d/%02d %02d:%02d:%02d.%03d] %s\r\n%s",
        st.wYear,
        st.wMonth,
        st.wDay,
        st.wHour,
        st.wMinute,
        st.wSecond,
        st.wMilliseconds,
        Message,
        Buffer1);

    return SetWindowText(Textbox, Buffer2);
}

 

3. RPC サーバーを書く

いよいよ RPC サーバーです。ファイルは 3 つです。

  • AsyncServer.h ・・・ CAsyncServer クラスの宣言
  • AsyncSercer.cpp ・・・ CAsyncServer クラス、RPC メソッド本体の定義
  • main.cpp ・・・ WinMain、ウィンドウ処理

今回は真っ当に C++ で書きました。C だけだとけっこう面倒なことになると思います。
CAsyncServer クラスは、待機スレッドの処理がメインです。GUI なので、クライアントからの要求を待機するスレッドを作らないとウィンドウがフリーズしてしまうのです。

AsyncServer.h

今回は RPC で使うプロトコルを TCP/IP、名前付きパイプ、LPC の 3 つを選べるようにしたので、その情報をメンバー変数として持たせています。それが RPC_PROTOCOL_TYPE  列挙型です。

//
// AsyncServer.h
//

#pragma once

#include "resource.h"

#include "..\AsyncCommon.h"

extern HWND g_Dlg;

class CAsyncServer {
private:
    HANDLE mThread;
    DWORD WINAPI RpcServerThread();
    static DWORD WINAPI StartRpcServerThread(LPVOID);
   
    RPC_PROTOCOL_TYPE mProtocol;

    VOID StopAndDestroyThread();

public:
    WCHAR mEndpoint[MAX_ENDPOINT];
    int mMaxInstances;

    CAsyncServer();
    ~CAsyncServer();

    inline VOID SetProtocolType(LRESULT l) {
        mProtocol= (RPC_PROTOCOL_TYPE)min(l, Rpc_NotSupported);
    }

    VOID StartStopRpcServer();
};

AsyncServer.cpp

RpcServerThread が待機スレッドです。これは同期でも非同期でも変わりません。

RpcSleepAsync が、今回のメインとなる非同期 RPC メソッドの本体です。ほとんどを MSDN からコピペしています。同期 RPC メソッドとは異なり、第一引数に RPC_ASYNC_STATE 構造体へのポインターを受け取ります。ここで重要なのは RpcAsyncCompleteCall API の実行です。この API は、クライアントとサーバーの両方のメソッドから呼び出す必要があるのがミソです。サーバー側で RpcAsyncCompleteCall を呼び出すことで、クライアント側にコールバックが発生します。

RpcAsyncCompleteCall function
http://msdn.microsoft.com/en-us/library/windows/desktop/aa375572(v=vs.85).aspx

//
// AsyncServer.cpp
//

#include <Windows.h>
#include <strsafe.h>

#include "AsyncServer.h"
#include "..\idl\pipo.h"

#pragma comment(lib, "rpcrt4.lib")

DWORD CAsyncServer::RpcServerThread() {
    RPC_STATUS Status= RPC_S_OK;
    RPC_PROTOCOL &Protocol=
      SupportedProtocols[min(mProtocol, Rpc_NotSupported)];

    if ( Protocol.Protocol==Rpc_Tcpip ) {
        RPC_POLICY Policy;
        Policy.Length= sizeof(RPC_POLICY);
        Policy.NICFlags= 0;
        Policy.EndpointFlags= 0;
        //Policy.EndpointFlags= RPC_C_USE_INTRANET_PORT;
        //Policy.EndpointFlags= RPC_C_USE_INTERNET_PORT;

        //Status = RpcServerUseProtseqEx((RPC_WSTR)Protocol.Name, mMaxInstances, NULL, &Policy);
        //if ( Status!=RPC_S_OK ) {
        //    LOGERROR(L"RpcServerUseProtseqEpEx failed – 0x%08x", Status);
        //    goto cleanup;
        //}

    }

    Status = RpcServerUseProtseqEp((RPC_WSTR)Protocol.Name,
      mMaxInstances, (RPC_WSTR)mEndpoint, NULL);
    if ( Status==RPC_S_DUPLICATE_ENDPOINT ) {
        LOGINFO(L"The endpoint ‘%s’ is already registered.", mEndpoint);
    }
    else if ( Status!=RPC_S_OK ) {
        LOGERROR(L"RpcServerUseProtseqEp failed – 0x%08x", Status);
        goto cleanup;
    }
 
    Status= RpcServerRegisterIf(pipo_v1_0_s_ifspec, NULL, NULL);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcServerRegisterIf failed – 0x%08x", Status);
        goto cleanup;
    }
 
    LOGINFO(L"RPC Server listening…", 0);

    Status = RpcServerListen(1, mMaxInstances, 0);
    if (Status!=RPC_S_OK) {
        LOGERROR(L"RpcServerListen failed – 0x%08x", Status);
       
        Status= RpcServerUnregisterIf(NULL, NULL, FALSE);
        if ( Status!=RPC_S_OK )
            LOGERROR(L"RpcServerUnregisterIf failed – 0x%08x", Status);

        goto cleanup;
    }

cleanup:
    ExitThread(Status);
    return Status;
}

DWORD CAsyncServer::StartRpcServerThread(LPVOID Param) {
    if ( Param==NULL )
        return 0;
    return ((CAsyncServer*)Param)->RpcServerThread();
}

CAsyncServer::CAsyncServer()
    : mThread(NULL),
      mProtocol(Rpc_NotSupported),
      mMaxInstances(1) {
    mEndpoint[0]= 0;
}

CAsyncServer::~CAsyncServer() {
    StopAndDestroyThread();
}

VOID CAsyncServer::StopAndDestroyThread() {
    if ( mThread ) {
        Shutdown();
        WaitForSingleObject(mThread, INFINITE);

        CloseHandle(mThread);
        mThread= NULL;
    }
}

VOID CAsyncServer::StartStopRpcServer() {
    if ( mThread )
        StopAndDestroyThread();
    else {
        mThread= CreateThread(NULL, 0, CAsyncServer::StartRpcServerThread,
          this, 0, NULL);
        if ( mThread==NULL )
            LOGERROR(L"CreateThread failed – 0x%08x", GetLastError());
    }
}

void __RPC_FAR * __RPC_USER midl_user_allocate(size_t len) {
    return malloc(len);
}

void __RPC_USER midl_user_free(void __RPC_FAR * ptr) {
    free(ptr);
}

void RpcSleep(int Duration) {
    LOGINFO(L"(Sleep) start. duration:%umsec…", Duration);
    Sleep(Duration);
    LOGINFO(L"(Sleep) done.", 0);
}

void Shutdown() {
    RPC_STATUS Status= RPC_S_OK;

    Status= RpcMgmtStopServerListening(NULL);
    if ( Status!=RPC_S_OK )
        LOGERROR(L"(Shutdown) RpcMgmtStopServerListening failed – 0x%08x",
          Status);

    Status = RpcServerUnregisterIf(NULL, NULL, FALSE);
    if ( Status!=RPC_S_OK )
        LOGINFO(L"(Shutdown) RpcServerUnregisterIf failed – 0x%08x", Status);

    LOGINFO(L"(Shutdown) done.", 0);
}

//
//
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378667(v=vs.85).aspx
//

#define ASYNC_CANCEL_CHECK  100
#define DEFAULT_ASYNC_DELAY 10000

void RpcSleepAsync(IN PRPC_ASYNC_STATE pAsync, IN int Duration) {
    int nReply = 1;
    RPC_STATUS Status;
    unsigned long nTmpAsychDelay;
 
    LOGINFO(L"(SleepAsync) start. duration:%umsec…", Duration);

    if (Duration < 0)
        Duration = DEFAULT_ASYNC_DELAY;
    else if (Duration < 100)
        Duration = 100;

    // We only call RpcServerTestCancel if the call takes longer than ASYNC_CANCEL_CHECK ms
    if(Duration > ASYNC_CANCEL_CHECK){
        nTmpAsychDelay= Duration/100;
        for ( int i=0 ; i<100 ; ++i ){
            Sleep(nTmpAsychDelay);
 
            if (i%5 == 0){
                //LOGINFO(L"(SleepAsync) %lu ms…", Duration);
 
                Status=  RpcServerTestCancel(RpcAsyncGetCallHandle(pAsync));
                if ( Status==RPC_S_OK ) {
                    LOGINFO(L"(SleepAsync) canceled.", 0);
                    break;
                }
                else if ( Status!=RPC_S_CALL_IN_PROGRESS ) {
                    LOGINFO(L"(SleepAsync) RpcAsyncInitializeHandle returned 0x%x", Status);
                    exit(Status);
                }
            }
        }
    }
    else
        Sleep(Duration);
 
    Status= RpcAsyncCompleteCall(pAsync, &nReply);
    LOGINFO(L"(SleepAsync) done.", 0);

    if ( Status!=RPC_S_OK ) {
        LOGERROR(L"(SleepAsync) RpcAsyncCompleteCall failed – 0x%08x",
          Status);
        exit(Status);
    }
}

main.cpp

最後のファイル。ほとんど UI 部分の処理です。
IDOK ボタンがクリックされると CAsyncServer::StartStopRpcServer を実行し、待機スレッドを開始します。他には特に何もしません。

//
// main.cpp
//

#include <Windows.h>

#include "AsyncServer.h"

HWND g_Dlg= NULL;
CAsyncServer *g_AsyncServer= NULL;

INT_PTR CALLBACK DlgProc(HWND Dlg, UINT Msg, WPARAM w, LPARAM l) {
    HWND Control= NULL;

    switch ( Msg ) {
    case WM_INITDIALOG:
        g_Dlg= Dlg;
       
        Control= GetDlgItem(Dlg, IDC_COMBO_PROTOCOL);

        for ( PRPC_PROTOCOL p= SupportedProtocols ;
              p->Protocol!=Rpc_NotSupported ; ++p )
            SendMessage(Control, CB_ADDSTRING, NULL,
               (LPARAM)p->FriendlyName);

        SetDlgItemInt(Dlg, IDC_EDIT_INSTANCES, 10, FALSE);
        PostMessage(Control, CB_SETCURSEL, 0, NULL);
        PostMessage(Dlg, WM_COMMAND,
          MAKELONG(IDC_COMBO_PROTOCOL, CBN_SELCHANGE), (LPARAM)Control);
        return TRUE;

    case WM_COMMAND:
        switch ( LOWORD(w) ) {
        case IDCANCEL:
            EndDialog(Dlg, IDOK);
            break;
        case IDOK:
            g_AsyncServer->SetProtocolType(SendMessage(
               GetDlgItem(Dlg, IDC_COMBO_PROTOCOL), CB_GETCURSEL, 0, 0));
            g_AsyncServer->mMaxInstances=
               GetDlgItemInt(Dlg, IDC_EDIT_INSTANCES, NULL, FALSE);
            GetDlgItemText(Dlg, IDC_EDIT_ENDPOINT,
               g_AsyncServer->mEndpoint, MAX_ENDPOINT);           
            g_AsyncServer->StartStopRpcServer();
            break;
        case IDC_COMBO_PROTOCOL:
            if ( HIWORD(w)==CBN_SELCHANGE ) {
                LRESULT Selected= SendMessage((HWND)l, CB_GETCURSEL, 0, 0);
                SetWindowText(GetDlgItem(Dlg, IDC_EDIT_ENDPOINT),
                  SupportedProtocols[
                  min(Selected, Rpc_NotSupported)].DefaultEndpoint);
            }
            break;
        }
        break;
    }
    return FALSE;
}

int WINAPI wWinMain(HINSTANCE hInstance,
                    HINSTANCE,
                    PWSTR pCmdLine,
                    int nCmdShow) {
    g_AsyncServer= new CAsyncServer();
    if ( g_AsyncServer ) {
        DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, DlgProc);
        delete g_AsyncServer;
    }
    return 0;
}

ダイアログボックスの外観

すっかり忘れていました。作成したダイアログボックスはこんな外観です。
起動して、[Named Pipe] を選択して [Start/Stop] をクリックするとこんな感じに待機スレッドが開始されたことが表示されます。それぞれのコントロールの ID は、、、main.cpp からお察し下さい。

image

[Win32] [C++] Asynchronous RPC with I/O Completion Port – #1

以前、同一マシン内での RPC について、名前付きパイプと LPC のそれぞれの方法で通信するクライアントとサーバーを作りました。このときは同期 RPC、すなわちクライアントがメソッドを呼び出すと、サーバー側での処理が終わるまで制御が返ってこない RPC でした。今回は非同期 RPC 通信についてプログラムを書いたので記事にします。

[Win32] [C++] Local RPC over Named Pipe and LPC
https://msmania.wordpress.com/2011/07/10/win32-c-local-rpc-over-named-pipe-and-lpc/

4 回に分けて書くことになりました。今回が #1 のインターフェース定義編です。

#1 – インターフェース定義編
https://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port/

#2 – RPC サーバー編
https://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-2/

#3 – RPC クライアント編
https://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-3/

#4 – ネットワーク キャプチャー編
https://msmania.wordpress.com/2012/03/08/win32-c-asynchronous-rpc-with-io-completion-port-4/

非同期 RPC では、クライアントがメソッドを呼び出しても、RPC サーバーの処理に関係なく制御がすぐに返ってきます。このため、実際に RPC サーバーでメソッドの処理が終わったときにコールバックを受ける必要が出てきます。このときのコールバック方法には複数の選択肢があり、いずれかをクライアント側が提示することができます。正確には、メソッドを呼び出すときのパラメーターである RPC_ASYNC_STATE 構造体の RPC_NOTIFICATION_TYPES 列挙値で指定します。

  • コールバックなし
  • イベント オブジェクト
  • APC (Asynchronous Procedure Call)
  • I/O 完了ポート
  • ウィンドウ メッセージ
  • コールバック関数

種類が豊富ですね。APC は使ったことがないのであまり知りませんが、それ以外は何となく想像がつきます。
さて、この中で比較的実装が複雑になりそうな I/O 完了ポートを使ってサンプルプログラムを作ります。ちなみに、MSDN に載っている非同期 RPC のサンプルはイベント オブジェクトを使うものでした。

RPC_ASYNC_STATE structure
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378490(v=vs.85).aspx

RPC_NOTIFICATION_TYPES enumeration
http://msdn.microsoft.com/en-us/library/windows/desktop/aa378638(v=vs.85).aspx

一つの記事で書くにはちょっと複雑なプログラムになってしまったので、まず最初にプログラムの全体像を紹介します。

プロジェクトのフォルダー構造は抜粋するとこんな感じです。
今回は GUI で書きました。64bit ネイティブでビルドしましたが、32bit でも普通にビルドできます。たぶん。

AsyncRpc
│  AsyncCommon.cpp … クライアント/サーバー共通コード
│  AsyncCommon.h
│ 
├─AsyncClient
│      main.cpp
│      AsyncClient.h
│      AsyncClient.cpp
│      resource.h
│      AsyncClient.rc
│             
├─AsyncServer
│  │  main.cpp
│  │  AsyncServer.h
│  │  AsyncServer.cpp
│  │  resource.h
│  │  AsyncServer.rc
│  │ 
│  └─x64
│      └─Debug
│              AsyncClient.exe
│              AsyncClient.pdb
│              AsyncServer.exe
│              AsyncServer.pdb
│             
└─idl
        pipo.idl … インターフェース定義関連
        pipo.acf
        pipo.h
        pipo_c.c
        pipo_s.c

1. インターフェース定義を作る (IDL, ACF ファイル)

まずは短いところから。IDL ファイルと ACF ファイルをテキスト エディターで書きます。ひな型の作成に uuidgen /i コマンドを使うこともできます。(前回の記事参照)

//
// pipo.idl
//
// generated with ‘uuidgen /i /opipo.idl’
// compile with ‘midl pipo.idl’
//
//
http://msdn.microsoft.com/en-us/library/aa367088
//

[
uuid(161b9ab8-1a96-40a6-bf8b-aa2d7ec94b6d),
version(1.0)
]
interface pipo
{
    void RpcSleep(int Duration);
    void RpcSleepAsync(int Duration);
    void Shutdown();
}

IDL ファイルは普通ですね。ACF ファイルはこんな感じです。

//
// pipo.acf
//
//
http://msdn.microsoft.com/en-us/library/aa366717(v=VS.85).aspx
//
 
[
implicit_handle(handle_t pipo_IfHandle)
]
interface pipo
{
    [async] RpcSleepAsync();
}

非同期 RPC にしたい関数には、ACF ファイル内で [async] 属性を付けておきます。詳細はそれぞれのファイルの先頭に書いた MSDN のページを参考にして下さい。

ファイルが書けたら、Windows SDK に含まれる midl.exe で IDL ファイルをコンパイルします。 ACF ファイルは midl が自動的に読み込みます。

> midl pipo.idl
Microsoft (R) 32b/64b MIDL Compiler Version 7.00.0555
Copyright (c) Microsoft Corporation. All rights reserved.
64 bit Processing .\pipo.idl
pipo.idl
64 bit Processing .\pipo.acf
pipo.acf

これで、インターフェースについてのヘッダーとソース ファイルが自動生成されました。

後で書くプログラムの仕様上、クライアント用ソース ファイルの pipo_c.c に含まれるインターフェース ハンドルのグローバル変数を、NULL で初期化しておきます。

これが修正前。

/* Standard interface: pipo, ver. 1.0,
   GUID={0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}} */

handle_t pipo_IfHandle;

修正後。

/* Standard interface: pipo, ver. 1.0,
   GUID={0x161b9ab8,0x1a96,0x40a6,{0xbf,0x8b,0xaa,0x2d,0x7e,0xc9,0x4b,0x6d}} */

handle_t pipo_IfHandle= NULL;

この記事はここまで。
次回は RPC サーバーを作ります。

[Win32] [COM] VDS Object Enumeration

NTFS、パーティション マネジメントあたりがマイブームなので、VDS の COM インターフェースを使って各種オブジェクトを列挙するプログラムを書いてみた。

サンプルはこのへん。

Loading VDS (Windows)
http://msdn.microsoft.com/en-us/library/aa383037.aspx

Working with Enumeration Objects (Windows)
http://msdn.microsoft.com/en-us/library/aa383988.aspx

コードは下に貼りますが、特に捻りもない普通のコードです。
ConsumeVDS で IVdsService インターフェースを取得して、EnumVdsObjects でソフトウェア プロバイダーの,インターフェース (IVdsPack) を取得して EnumVdsVolumes, EnumVdsDisks でそれぞれボリュームとディスクを列挙。
Dump***Prop 関数は、GetProperties で取ってきた構造体をパースして表示しているだけなので省略。

そう言えば、衝撃の事実が上記 MSDN ページに。
VDS とダイナミック ディスクは星になるようです。

[Both the Virtual Disk Service and dynamic disks are deprecated as of Windows 8 Consumer Preview and Windows Server 8 Beta, and may be unavailable in subsequent versions of Windows. For more information, see Windows Storage Management API.]

VDS サービスはスタートアップ種別が手動であり、IVdsVolume インターフェースを取得するため、IVdsServiceLoader::LoadService を呼んだときに起動され、インターフェースを破棄するとサービスが停止します。このデザインはパフォーマンス的にあまりよくない気がします。

そんなこんなでソース。

//
// vds.cpp
//
//
http://msdn.microsoft.com/en-us/library/aa383037.aspx
// http://msdn.microsoft.com/en-us/library/aa383988.aspx
//

#include <initguid.h>
#include <vds.h>
#include <stdio.h>

#pragma comment( lib, "rpcrt4.lib" )

void Logging(LPCWSTR fmt, DWORD err) {
    //SYSTEMTIME st;
    //GetSystemTime(&st);
    //wprintf(L"[%d/%02d/%02d %02d:%02d:%02d.%03d] ",
    //    st.wYear,
    //    st.wMonth,
    //    st.wDay,
    //    st.wHour,
    //    st.wMinute,
    //    st.wSecond,
    //    st.wMilliseconds);

    wprintf(fmt, err);
}

#define GET_LODWORD(ll) (((PLARGE_INTEGER)&ll)->LowPart)
#define GET_HIDWORD(ll) (((PLARGE_INTEGER)&ll)->HighPart)

void EnumVdsVolumes(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumVolumes= NULL;

    Ret= VdsPack->QueryVolumes(&EnumVolumes);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryVolumes failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsVolume *Volume= NULL;
        IVdsVolumeMF *VolumeMF= NULL;
               
        VDS_VOLUME_PROP PropVol;
        VDS_FILE_SYSTEM_PROP PropFs;

        Ret= EnumVolumes->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsVolume, (void**)&Volume);
        Unknown->Release();
        if ( FAILED(Ret) ) {
            Logging(L"IID_IVdsVolume::QueryInterface failed – 0x%08x\n",
              Ret);
            continue;
        }

        Ret= Volume->GetProperties(&PropVol);
        if ( Ret==S_OK || Ret==VDS_S_PROPERTIES_INCOMPLETE )
            DumpVolumeProp(&PropVol);
        if ( Ret==VDS_S_PROPERTIES_INCOMPLETE )
            wprintf(L"      ** IID_IVdsVolume::GetProperties returned VDS_S_PROPERTIES_INCOMPLETE(0x%08x)\n\n", VDS_S_PROPERTIES_INCOMPLETE);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolume::GetProperties failed – 0x%08x\n", Ret);

        Ret= Volume->QueryInterface(IID_IVdsVolumeMF, (void**)&VolumeMF);
        Volume->Release();
        if ( Ret!=S_OK )
            Logging(L"IID_IVdsVolumeMF::QueryInterface failed – 0x%08x\n",
              Ret);

        Ret= VolumeMF->GetFileSystemProperties(&PropFs);
        if ( Ret==VDS_E_NO_MEDIA )
            wprintf(L"      ** IID_IVdsVolumeMF::GetProperties returned VDS_E_NO_MEDIA(0x%08x)\n\n", VDS_E_NO_MEDIA);
        else if ( FAILED(Ret) )
            Logging(L"IID_IVdsVolumeMF::GetFileSystemProperties failed – 0x%08x\n", Ret);
        else
            DumpFileSystemProp(&PropFs);

    } while(1);
   
cleanup:
    if ( EnumVolumes )
        EnumVolumes->Release();

    return;
}

void EnumVdsDisks(IVdsPack *VdsPack) {
    HRESULT Ret;
    ULONG Fetched= 0;
    IUnknown *Unknown= NULL;
    IEnumVdsObject *EnumDisks= NULL;

    Ret= VdsPack->QueryDisks(&EnumDisks);
    if ( FAILED(Ret) ) {
        Logging(L"IVdsPack::QueryDisks failed – 0x%08x\n", Ret);
        goto cleanup;
    }

    do {
        IVdsDisk *Disk= NULL;
        VDS_DISK_PROP DiskProp;

        Ret= EnumDisks->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsDisk, (void**)&Disk);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Disk->GetProperties(&DiskProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsDisk::GetProperties failed – 0x%08x\n", Ret);
        else
            DumpDiskProp(&DiskProp);

    } while (1);

cleanup:
    if ( EnumDisks )
        EnumDisks->Release();

    return;
}

void EnumVdsObjects(IVdsService *VdsService) {
    HRESULT Ret;

    ULONG Fetched= 0;
    IEnumVdsObject *EnumSwProviders= NULL;
    IEnumVdsObject *EnumPacks= NULL;
    IUnknown *Unknown= NULL;

    IVdsProvider *Provider= NULL;
    IVdsSwProvider *SwProvider= NULL;
    IVdsPack *VdsPack= NULL;

    VDS_PROVIDER_PROP ProviderProp;
    VDS_PACK_PROP PackProp;

    Ret= VdsService->QueryProviders(VDS_QUERY_SOFTWARE_PROVIDERS,
      &EnumSwProviders);
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::QueryProviders failed – 0x%08x\n", Ret);
        return;
    }

    do {
        Ret= EnumSwProviders->Next(1, &Unknown, &Fetched);
        if ( Ret==S_FALSE ) break;
        if ( FAILED(Ret) ) goto cleanup;

        Ret= Unknown->QueryInterface(IID_IVdsProvider, (void**)&Provider);
        Unknown->Release();
        if ( FAILED(Ret) ) continue;

        Ret= Provider->GetProperties(&ProviderProp);
        if ( FAILED(Ret) )
            Logging(L"IID_IVdsProvider::GetProperties failed – 0x%08x\n",
              Ret);
        else
            DumpProviderProp(&ProviderProp);

        Ret= Provider->QueryInterface(IID_IVdsSwProvider,
          (void**)&SwProvider);
        Provider->Release();
        if ( FAILED(Ret) ) continue;

        Ret= SwProvider->QueryPacks(&EnumPacks);
        SwProvider->Release();
        if ( FAILED(Ret) ) continue;

        do {
            Ret= EnumPacks->Next(1, &Unknown, &Fetched);
            if ( Ret==S_FALSE ) break;
            if ( FAILED(Ret) ) goto cleanup;

            Ret= Unknown->QueryInterface(IID_IVdsPack, (void**)&VdsPack);
            Unknown->Release();
            if ( FAILED(Ret) ) continue;

            Ret= VdsPack->GetProperties(&PackProp);
            if ( FAILED(Ret) )
                Logging(L"IID_IVdsPack::GetProperties failed – %08x\n", Ret);
            else
                DumpPackProp(&PackProp);
           
            EnumVdsDisks(VdsPack);
            EnumVdsVolumes(VdsPack);

        } while (1);

        EnumPacks->Release();
        EnumPacks= NULL;

    } while (1);

cleanup:
    if ( EnumPacks )
        EnumPacks->Release();

    if ( EnumSwProviders )
        EnumSwProviders->Release();

    return;
}

void ConsumeVDS() {
    HRESULT Ret= 0;
    IVdsServiceLoader *VdsServiceLoader= NULL;
    IVdsService *VdsService= NULL;
   
    Ret = CoInitialize(NULL);
    if ( Ret!=S_OK ) {
        Logging(L"CoInitialize failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= CoCreateInstance(CLSID_VdsLoader,
        NULL,
        CLSCTX_LOCAL_SERVER,
        IID_IVdsServiceLoader,
        (void **) &VdsServiceLoader);
    if ( Ret!=S_OK ) {
        Logging(L"CoCreateInstance(IVdsServiceLoader) failed – 0x%08x\n",
          Ret);
        goto cleanup;
    }
   
    Ret= VdsServiceLoader->LoadService(NULL, &VdsService);
    VdsServiceLoader->Release();
    VdsServiceLoader= NULL;
   
    if ( Ret!=S_OK ) {
        Logging(L"IVdsServiceLoader::LoadService failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    Ret= VdsService->WaitForServiceReady();
    if ( Ret!=S_OK ) {
        Logging(L"IVdsService::WaitForServiceReady failed – 0x%08x\n", Ret);
        goto cleanup;
    }
   
    EnumVdsObjects(VdsService);

cleanup:
    if ( VdsService )
        VdsService->Release();

    if ( VdsServiceLoader )
        VdsServiceLoader->Release();

    CoUninitialize();

    return;
}