[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] [C++] Local RPC over Named Pipe and LPC

Microsoft の分散コンピューティング関連技術は入り乱れていて把握が難しいのが現状です。その中でも外せないのが RPC (=Remote Procedure Call) でしょうか。

  • MSRPC = DCE/RPC の MS 実装
  • DCOM = MSRPC を使った COM
  • COM+ = COM + MTS
  • ローカルでの RPC では多くの場合 Windows カーネルが提供する LPC (=Local Procedure Call) が使われる

DCOM じゃない COM のメソッド呼び出しは RPC を使っているのではないかという気がしますが、ちょっと自信がないです。間違っていたら誰かコメント下さい。これもカーネルデバッグすれば分かるのでしょうが。

COM については、以下のページを参考にして下さい。

COM+ (Component Services)
http://www.microsoft.com/com/default.mspx

今回は、ローカルでの RPC を検証するサンプル プログラムを作ってみました。
RPC が対応しているプロトコルの一覧は以下のページにありますが、そのうちの名前付きパイプと LPC  を使えるようにしてあります。

Protocol Sequence Constants
http://msdn.microsoft.com/en-us/library/aa374395(VS.85).aspx

ベースは、以下のページにあるサンプル プログラムを使っています。(コピペともいう)

RPC Sample Application
http://msdn.microsoft.com/en-us/library/dd418893(v=prot.10).aspx

作る順番としては、こんな感じになるでしょうか。全部 Visual Studio からできますが、uuidgen と MIDL をコマンドラインから直接実行する方が面白いです。

  1. uuidgen で IDL ファイルのひな型を作る
  2. IDL ファイルと ACF ファイルを書く
  3. MIDL でコンパイル
  4. RPC サーバーを書く
  5. RPC クライアントを書く
  6. とりあえず動かす

1. uuidgen で IDL ファイルのひな型を作る

IDL (=Interface Definition Language) は、RPC インターフェースを定義するためのプログラミング言語で、Windows に限らず、Linux でも動くコンパイラはあるようです。

スタートメニューから、 "Visual Studio Command Prompt (2010)" を実行し、以下のコマンドを実行します。

> uuidgen /i /opipo.idl

以下のようなひな型が生成されます。要は UUID が生成されただけです。

[
uuid(6504fa96-8126-401b-adfd-18580a6e9d86),
version(1.0)
]
interface INTERFACENAME
{

}

2. IDL ファイルと ACF ファイルを書く

IDL ファイルには、RPC メソッドのスタブを C/C++ のプロトタイプ宣言として記述します。INTERFACENAME も適当なものに変えておきます。今回は 3 つのメソッドを宣言しました。

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

[
uuid(6504fa96-8126-401b-adfd-18580a6e9d86),
version(1.0)
]
interface pipo
{
    void RpcSleep(int Duration);
    void Hello([in, string] const wchar_t *String);
    void Shutdown();
}

コメントにも入れましたが、以下のページに MIDL のリファレンスがあるので、適宜参照してください。
http://msdn.microsoft.com/en-us/library/aa367088

ACF ファイルは、メモリや例外など、メソッドに関する属性を記述するのに使われるファイル、だそうです。

今回は RPC バインディング ハンドルを宣言するのに使います。これを書くことで、ハンドルを自動的に定義/宣言してくれるので楽です。

//
// pipo.acf
//
//
http://msdn.microsoft.com/en-us/library/aa366717(v=VS.85).aspx
//

[
implicit_handle(handle_t pipo_IfHandle)
]
interface pipo
{
}

3. MIDL でコンパイル

IDL と ACF ファイルをコンパイルします。 2 つのファイルを同じフォルダーにおいて、以下のコマンドを実行して下さい。

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

これにより、以下 3 つのファイルが生成されますので、これから作るクライアントやサーバー プログラムのプロジェクトにコピーしておきます。

  • pipo.h – クライアント/サーバー共通のヘッダー
  • pipo_c..c – クライアント用ソース ファイル (スタブの定義など)
  • pipo_s.c – サーバー用ソース ファイル

RPC 基盤では、RPC メソッドに渡すパラメーターのメモリ領域を動的に確保/解放するための関数を必要としており、その定義はクライアント/サーバーの双方に自分で書かなけれないけません。関数名は midl_user_allocate と midl_user_free です。とはいっても、malloc と free を呼び出すだけにするのが慣習のようです。もし大きなメモリブロックが必要だったら、VirtualAlloc を使った方がいいかもしれません、たぶん。

4. RPC サーバーを書く

コマンドライン引数を処理して、指定されたプロトコルで待機するだけのプログラムです。
Shutdown は、クライアントからサーバーを終了するためのメソッドで、これを用意しておくのは慣習みたいなので実装しておきました。midl_user_allocate と midl_user_free を定義し忘れると [未解決のシンボル] エラーが出るので注意。もちろん rpcrt4.lib をリンカに追加するのも忘れないように。

//
// piposerver.cpp
//
//
http://msdn.microsoft.com/en-us/library/dd418893(v=prot.10).aspx
// http://msdn.microsoft.com/en-us/library/aa374395(VS.85).aspx
//

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

#include "pipo.h"

#define PROT_LPC    ((RPC_WSTR)L"ncalrpc")
#define PROT_NP     ((RPC_WSTR)L"ncacn_np")

void RpcSleep(int Duration) {
    wprintf(L"[Pipo:0x%x] Duration: %u msec…\n",
      GetCurrentThreadId(), Duration);
    Sleep(Duration);
    wprintf(L"[Pipo:0x%x] done.\n", GetCurrentThreadId());
}

void Hello(LPCWSTR String) {
    wprintf(L"[Pipo:0x%x] %s\n", GetCurrentThreadId(), String);

void Shutdown() {
    RPC_STATUS Status= RPC_S_OK;

    Status= RpcMgmtStopServerListening(NULL);
    if ( Status!=RPC_S_OK )
        wprintf(L"[Shutdown:0x%x] RpcMgmtStopServerListening failed (0x%08x)\n",
          GetCurrentThreadId(), Status);

    Status = RpcServerUnregisterIf(NULL, NULL, FALSE);
    if ( Status!=RPC_S_OK )
        wprintf(L"[Shutdown:0x%x] RpcServerUnregisterIf failed (0x%08x)\n",
          GetCurrentThreadId(), Status);

    wprintf(L"[Shutdown:0x%x] done.\n", GetCurrentThreadId());
}

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);
}

#define MAX_PROTOCOL 10
static wchar_t upperstr[MAX_PROTOCOL+1];
const wchar_t *ToUpper(const wchar_t *s) {
    for ( int i=0 ; i<MAX_PROTOCOL+1 ; ++i ) {
        upperstr[i]= toupper(s[i]);
        if ( s[i]==0 )
            return upperstr;
    }
    upperstr[MAX_PROTOCOL]= 0;
    return upperstr;
}

/*
   usage: piposerver [PIPE|LPC] [endpoint] [maxinstance]
*/

int wmain(int argc, wchar_t *argv[]) {
    if ( argc<4 ) {
        wprintf(L"\nusage: piposerver [PIPE|LPC] [endpoint] [maxinstance]\n");
        exit(ERROR_INVALID_PARAMETER);
    }
   
    LPCWSTR UpperProt= ToUpper(argv[1]);
    RPC_WSTR Protocol= NULL;
    if ( wcscmp(UpperProt, L"PIPE")==0 )
        Protocol= PROT_NP;
    else if ( wcscmp(UpperProt, L"LPC")==0 )
        Protocol= PROT_LPC;
    else {
        wprintf(L"Unknown procotol.\n");
        return ERROR_INVALID_PARAMETER;
    }

    RPC_STATUS Status= RPC_S_OK;
    RPC_WSTR Endpoint= (RPC_WSTR)argv[2];
    unsigned int MaxInstance= min(_wtoi(argv[3]),
      RPC_C_LISTEN_MAX_CALLS_DEFAULT);
   
    Status = RpcServerUseProtseqEp(Protocol, MaxInstance, Endpoint, NULL);
    if ( Status!=RPC_S_OK ) {
        wprintf(L"RpcServerUseProtseqEp failed (0x%08x)\n", Status);
        exit(Status);
    }
 
    Status= RpcServerRegisterIf(pipo_v1_0_s_ifspec, NULL, NULL);
    if (Status!=RPC_S_OK) {
        wprintf(L"RpcServerRegisterIf failed (0x%08x)\n", Status);
        exit(Status);
    }
 
    wprintf(L"Protocol:      %s\n", Protocol);
    wprintf(L"Endpoint:      %s\n", Endpoint);
    wprintf(L"Max instances: %u\n", MaxInstance);
    wprintf(L"RPC Server listeing (TID:0x%x) …\n\n", GetCurrentThreadId());

    Status = RpcServerListen(1, MaxInstance, 0);
    if (Status!=RPC_S_OK) {
        wprintf(L"RpcServerListen failed (0x%08x)\n", Status);
       
        Status= RpcServerUnregisterIf(NULL, NULL, FALSE);
        if ( Status!=RPC_S_OK )
            wprintf(L"RpcServerUnregisterIf failed (0x%08x)\n", Status);

        exit(Status);
    }

    return 0;
}

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

次に RPC クライアントを書きます。こちらも単一ファイルで。
サーバーと同じく、midl_user_allocate と midl_user_free の定義と、rpcrt4.lib のリンカへの追加を忘れないように。

//
// pipoclient.cpp
//

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

#include "pipo.h"

#define PROT_LPC    ((RPC_WSTR)L"ncalrpc")
#define PROT_NP     ((RPC_WSTR)L"ncacn_np")

enum METHODTYPE {
    method_Shutdown,
    method_Sleep,
    method_Hello,

    method_EOL  // End-Of-List
};

#define MAX_METHODNAME 16

struct METHOD {
    METHODTYPE Type;
    WCHAR Name[MAX_METHODNAME];
    int    MinParameter;
};

const METHOD Methods[]= {
    {method_Shutdown, L"SHUTDOWN", 4},
    {method_Sleep, L"SLEEP", 5},
    {method_Hello, L"HELLO", 5},

    {method_EOL, L"", 0}
};

/*
usage: pipoclient [PIPE|LPC] [endpoint] [option]
           shutdown
               shutdown RPC server
           sleep [duration]
               sleep
           hello [message]
               show message
*/

void ShowUsage() {
    wprintf(L"usage: pipoclient [PIPE|LPC] [endpoint] [method] [option]\n");
    wprintf(L"           shutdown\n");
    wprintf(L"               shutdown RPC server\n");
    wprintf(L"           sleep [duration]\n");
    wprintf(L"               sleep\n");
    wprintf(L"           hello [message]\n");
    wprintf(L"               show message\n");
}

static wchar_t upperstr[MAX_METHODNAME+1];
wchar_t *ToUpper(const wchar_t *s) {
    for ( int i=0 ; i<MAX_METHODNAME+1 ; ++i ) {
        upperstr[i]= toupper(s[i]);
        if ( s[i]==0 )
            return upperstr;
    }
    upperstr[MAX_METHODNAME]= 0;
    return upperstr;
}

int wmain(int argc, wchar_t *argv[]) {
    if ( argc<4 ) {
        ShowUsage();
        return ERROR_INVALID_PARAMETER;
    }

    LPWSTR UpperString= NULL;
    RPC_WSTR Protocol= NULL;
    UpperString= ToUpper(argv[1]);
    if ( wcscmp(UpperString, L"PIPE")==0 )
        Protocol= PROT_NP;
    else if ( wcscmp(UpperString, L"LPC")==0 )
        Protocol= PROT_LPC;
    else {
        ShowUsage();
        wprintf(L"Unknown procotol.\n");
        return ERROR_INVALID_PARAMETER;
    }

    UpperString= ToUpper(argv[3]);
    int MethodIndex= -1;
    for ( int i=0 ; Methods[i].Type!=method_EOL ; ++i ) {
        if ( wcscmp(Methods[i].Name, UpperString)==0 ) {
            MethodIndex= i;
            break;
        }
    }

    if ( MethodIndex<0 || argc<Methods[MethodIndex].MinParameter ) {
        ShowUsage();
        wprintf(L"Bad parameter.\n");
        return ERROR_INVALID_PARAMETER;
    }

    RPC_STATUS Status= RPC_S_OK;
    RPC_WSTR Binding= NULL;
 
    Status= RpcStringBindingCompose(
      NULL, Protocol, NULL, (RPC_WSTR)argv[2], NULL, &Binding);
    if (Status!=RPC_S_OK) {
        wprintf(L"RpcStringBindingCompose failed (0x%08x)\n", Status);
        return Status;
    }

    Status= RpcBindingFromStringBinding(Binding, &pipo_IfHandle);
    if (Status!=RPC_S_OK) {
        wprintf(L"RpcBindingFromStringBinding failed (0x%08x)\n", Status);
        return Status;
    }
 
     RpcTryExcept {
        wprintf(L"%s is invoked.\n", Methods[MethodIndex].Name);

        switch ( Methods[MethodIndex].Type ) {
        case method_Shutdown:
            Shutdown();
            break;
        case method_Hello:
            Hello(argv[4]);
            break;
        case method_Sleep:
            RpcSleep(_wtoi(argv[4]));
            break;
        }
    }
    RpcExcept( EXCEPTION_EXECUTE_HANDLER ) {
        printf("Caught exception: 0x%08x\n", RpcExceptionCode());
    }
    RpcEndExcept
 
    Status = RpcStringFree(&Binding);
    if (Status!=RPC_S_OK)
        wprintf(L"RpcStringFree failed (0x%08x)\n", Status);
 
    Status= RpcBindingFree(&pipo_IfHandle);
    if (Status!=RPC_S_OK)
        wprintf(L"RpcBindingFree failed (0x%08x)\n", Status);
 
    return 0;
}

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);
}

6. とりあえず動かす

せっかくなので動かしてみましょう。

まずは、RPC サーバーを起動します。パイプは \pipe\パイプ名 という名前じゃないと RPC_S_INVALID_ENDPOINT_FORMAT エラーになるので注意。LPC ポート名も、円記号などを含めると同じエラーが発生します。

image

実際にパイプや LPC ポートが作られたかどうかは、Sysinternals ツールで調べることができます。

Sysinternals Suite は便利なので、必ずダウンロードしておきましょう。
http://technet.microsoft.com/ja-jp/sysinternals/bb842062

名前付きパイプは、pipelist.exe で一覧を見ることができます。(結果は一部抜粋)

e:\dropbox\pipo> pipelist

PipeList v1.01
by Mark Russinovich
http://www.sysinternals.com

Pipe Name                                    Instances       Max Instances
———                                    ———       ————-
InitShutdown                                      3               -1     
lsass                                             4               -1     
protected_storage                                 3               -1     
ntsvcs                                            3               -1     
scerpc                                            3               -1     
plugplay                                          3               -1     
epmapper                                          3               -1     
LSM_API_service                                   3               -1     
ExtEventPipe_Service                              1               30     
eventlog                                          3               -1     
Winsock2\CatalogChangeListener-80-0               1                1     
atsvc                                             3               -1     
wkssvc                                            4               -1     
msmania\pipe                                      3               -1     

ちなみに同じパイプ名でサーバーを起動してもエラーになることはなく、インスタンスが 3 から 6 になりました。

LPC ポートは Winobj.exe から見ることができます。RPC Control のところにあります。

image

LPC ポートの場合は、同じポート名でサーバーの起動を試みると、RPC_S_DUPLICATE_ENDPOINT エラーが発生します。

この状態で、次に RPC クライアントを実行します。
image

何回かメソッドを呼ぶと、スレッドが切り替わる様子も確認できます。
image

RPC サーバーを起動するときに最大インスタンス数を 5 に設定しましたので、5 セッションを枯渇させると RPC_S_SERVER_TOO_BUSY (0x000006bb) エラーが発生します。

image

他にも、RPC エラーを発生させるパターンはいろいろ考えられるので、簡単に確認できるツールがあると便利です。

遊び終わったら、Shutdown メソッドでも呼んでおきます。

image