[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 サーバーを作ります。

広告

[SAP] [C#] SAP GUI Scripting from .NET

SAP GUI Scripting という機能があります。eCATT (まともに使ったことはない) のように、SAP GUI を外部から操作できる機能です。テスト シナリオの実行やテスト データ入力などを大量処理する場合に使えそうです。HP Quality Center やら HP Load Runner はこの機能を利用しているんでしょうかね。

これはあくまでも SAP GUI を操作する機能ですので、アプリケーション サーバーを直接操作する BAPI や Enterprise Services とは全く別の機能です。

この SAP GUI Scripting、実際どのぐらい使われているかどうかは不明です。古くからある機能なので、公開されている情報も古いものが多い気がします。VBScript やネイティブ アプリ、.NET からも操作できるような記述がありますが、SAP Note に添付されているサンプルが VB6 で書かれたとおぼしきものだったり。いいサンプルなんですけど。

そんなわけで、.NET で SAP GUI Scripting を操作してみました。

まず、SAP GUI Scripting の実体は単なる ActiveX コントロールです。VBA から SAP にアクセスするときに、SAP GUI の ActiveX コントロールを使っている人はそこそこいるような、いないような。ファイルは sapfewse.ocx です。SAP GUI をインストールすると以下のパスに作成されます。

C:\Program Files (x86)\SAP\FrontEnd\SAPgui\sapfewse.ocx

image

それさえ分かれば、あとは普通に使えます。手順は以下の通り。

  1. TypeLib を使って OCX ファイルから IDL ファイルを作成
  2. IDL をコンパイルして TLB ファイルを作成
  3. TLBIMP を使って TLB ファイルから .NET アセンブリを作成
  4. .NET アプリを書く

環境はこんな感じ。SDK は新しいの入れておかないと駄目だなー。

OS: Windows 7 SP1
IDE: Visual Studio 2010 SP1
SDK: Windows SDK  v7.0A (まずい 7.1 入れてない・・・)
SAP GUI: 720 PL3

1. TypeLib を使って OCX ファイルから IDL ファイルを作成

Windows SDK に付属している OLE-COM Object Viewer を管理者特権で開いて下さい。Visual Studio をインストールすると勝手にインストールされると思います。

image

メニューから File > View TypeLib を選んで下さい。

image

[ファイルを開く] ダイアログが表示されるので、前述の sapfewse.ocx を開いて下さい。

image

メニューから File > Save As… を選択し、適当なところに IDL ファイルを保存して下さい。ファイルの末尾が切れる場合は、右ペインのテキストをコピペしてテキスト ファイルとして保存して下さい。

image

ファイルはこんな感じです。以前の記事で RPC サーバー / クライアントを作ったときにも IDL ファイルは出てきました。あのときは手で書きましたが、このように既存のオブジェクトから自動生成することもできます。

image

 

2. IDL をコンパイルして TLB ファイルを作成

ネイティブだったら IDL ファイルを使ってそのまま RPC クライアントを作ればいいのですが、.NET の場合はもう数ステップ必要です。

次に IDL ファイルをコンパイルして TLB ファイルを作る必要があります。Windows SDK に含まれている midl.exe を使います。これも RPC のときに使いました。Visual Studio Command Prompt を管理者特権で開き、以下のコマンドを実行します。

> midl sapfewse.IDL

image

が、エラーとなります。おーん。

.\sapfewse.IDL(601) : error MIDL2025 : syntax error : expecting a type specification near "GuiComponent"

.\sapfewse.IDL(601) : error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation

syntax error とか・・・自動生成したものをそのまま使ってるんですがね。
エラーとなっている 601 行付近はこんな感じです。

    [
      uuid(93A37525-9118-4731-92A7-B93DF1E34455)
    ]
    dispinterface _Dsapfewse {
        properties:
            [id(0x00007d01), readonly           
]
            BSTR Name;
            [id(0x00007d0f), readonly           
]
            BSTR Type;
            [id(0x00007d20), readonly           
]
            long TypeAsNumber;
            [id(0x00007d21), readonly           
]
            VARIANT_BOOL ContainerType;
            [id(0x00007d19), readonly           
]
            BSTR Id;
            [id(0x00007d26), readonly           
]
            GuiComponent* Parent;
            [id(0x00007d13), readonly      ← 601 行目     
]

インターフェース _Dsapfewse のメンバーを定義しているところです。GuiComponent というキーワードが認識されていないようなので、定義を探してみます。すると、701 行目に定義が見つかります。原因はこれですね。物事には順番ってものがあります。

[
  uuid(ABCC907C-3AB1-45D9-BF20-D3F647377B06),
  noncreatable
]
coclass GuiComponent {
    [default] dispinterface ISapComponentTarget;
};

GuiComponent の定義を _Dsapfewse の定義の上に移動させます。こんな感じ。

[
  uuid(ABCC907C-3AB1-45D9-BF20-D3F647377B06),
  noncreatable
]
coclass GuiComponent {
    [default] dispinterface ISapComponentTarget;
};

[
  uuid(93A37525-9118-4731-92A7-B93DF1E34455)
]
dispinterface _Dsapfewse {
    properties:
        [id(0x00007d01), readonly           

さて、再チャレンジ。またエラーでございます。

image

.\sapfewse.IDL(612) : error MIDL2025 : syntax error : expecting a type specification near "GuiComponentCollection"
.\sapfewse.IDL(612) : error MIDL2026 : cannot recover from earlier syntax errors; aborting compilation

今度は GuiComponentCollection か。というわけで、今度は GuiComponentCollection の定義を _Dsapfewse の上に移動します。で、また違う箇所でエラ・・・。

試行錯誤の上、以下のような修正をしてコンパイル エラーがクリアされました。やれやれ手間のかかる・・・。でもまあ、定義の順番を入れ替えるだけでよかったから幸運なのかも。

  • GuiComponent の定義を _Dsapfewse の前に移動
  • GuiComponentCollection の定義を _Dsapfewse の前に移動
  • GuiUtils の定義を _Dsapfewse の前に移動
  • GuiCollection の定義を _Dsapfewse の前に移動
  • GuiConnection の定義を _Dsapfewse の前に移動
  • GuiSession の定義を _DsapfewseEvents の前に移動
  • GuiFrameWindow の定義を ISapSessionTarget の前に移動
  • GuiSessionInfo の定義を ISapSessionTarget の前に移動
  • GuiVComponent の定義を ISapWindowTarget の前に移動
  • GuiScrollbar の定義を ISapScreenTarget の前に移動
  • GuiContextMenu の定義を ISapScreenTarget の前に移動
  • GuiComboBoxEntry の定義を ISapComboBoxTarget の前に移動
  • GuiTab の定義を ISapTabbedPane の前に移動

image

コンパイルが通ると、sapfewse.tlb というファイルが生成されます。これはバイナリ ファイルです。

image

 

3. TLBIMP を使って TLB ファイルから .NET アセンブリを作成

TLB ファイルができたら、同じく Windows SDK に含まれる tlbimp を使うと .NET アセンブリを生成してくれます。

以下のコマンドを実行します。ここはノー エラーで通った。

e:\dropbox\sapfewse> tlbimp sapfewse.tlb  /out:sapfewse.dll
Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.1
Copyright (C) Microsoft Corporation.  All rights reserved.

TlbImp : Type library imported to e:\dropbox\sapfewse\sapfewse.dll

e:\dropbox\sapfewse>

無事、アセンブリである sapfewse.dll をゲットすることができました。

image

 

4. .NET アプリを書く

ここまでが事前準備です。では Visual Studio を起動して .NET アプリを書きます。例によって言語は C# を使いますが、VB でも F# でも動くはずです。

普通に C# の Window Forms Application プロジェクトを作成して下さい。
ソリューションのフォルダーに、.NET アセンブリを作ったときの作業用フォルダーを丸ごとコピーしておくとよいです。

image

Solution Explorer の References を右クリックし、Add Referene… を選択して下さい。
Add Reference ダイアログボックスの Browse タブから、先の手順で作った sapfewse.dll を選んで OK をクリックして下さい。をや、TLB ファイルはそのまま使えるのか。これは知らなかった。

image

名前空間 sapfewse が追加されました。Object Explorer で開いてみると、GuiApplication などのクラスが確認できます。

image

ボタンとテキスト ボックスを配置します。

image

ボタンをクリックしたときのイベント ハンドラーに、以下の 2 行を追加します。ええ、2 行だけです。
GuiApplication のインスタンスを作って、OpenConnection を呼ぶだけです。

sapfewse.GuiApplication SapGuiApp = new sapfewse.GuiApplication();
SapGuiApp.OpenConnection(textBox1.Text);

ソースはこんな感じになります。上の 2 行以外は何もいじっていません。

image

SAP GUI がインストールされている PC 上でプログラムを起動します。
テキスト ボックスに SAP GUI の接続エントリの名称を入力し、Logon をクリックすると接続できます。

この環境では、下記のように "NetWeaver 7.02" という名前のエントリを作ってあるので、"NetWeaver 7.02" と入力します。

image

image

Logon をクリックすると、"A script is opening a connection to system NetWeaver 7.02" という確認のポップアップが表示されます。もちろん OK をクリックします。

image

やけにクラシックな SAP GUI の画面が起動してきました。スキンの設定が変えられるかどうかは試していません。

image

何はともあれ、ログオンはできます。ツールバーとか変ですけど。

image

以上が C# から SAP GUI Scripting を操作するサンプルでした。SAP GUI を起動するサンプルでしたが、既存の SAP GUI セッションにアタッチして値の入力やトランザクションの実行を操作することもできます。むしろそういう使われ方をするほうが多いかもしれません。

オブジェクトのリファレンスは SDN で公開されています。以下のページから SAP GUI Scripting API をダウンロードして下さい。

SAP GUI Scripting
http://www.sdn.sap.com/irj/sdn/index?rid=/webcontent/uuid/007084d7-41f4-2a10-9695-d6bce1673c2f

例えば、サンプルで使った GuiApplication::OpenConnection はこんな感じです。

image

[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