[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

広告

One Response to [Win32] [C++] Local RPC over Named Pipe and LPC

  1. ピンバック: [Win32] [C++] Asynchronous RPC with I/O Completion Port « すなのかたまり

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。