COM/DCOMのマーシャリングに伴う実行時エラー

作成:2005年12月6日

改訂:2006年5月17日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   プログラミング

COM/DCOMは本来はバイナリ規格ですが、インターフェースの仕様は、IDL (Interface Definition Language) を使って人間が読める形で定義するのが一般的です。

IDLのインターフェース定義で使えるデータ型は、C言語/C++言語のデータ型ととても良く似ています。実際、Visual C++ (VC++) を使ってソフトウェアを作る場合は、COM/DCOMの関数呼び出しは、ふつうの関数呼び出しとの違いは、ほとんど意識することがありません。

しかし、COM/DCOMのデータ型と、C言語/C++言語のデータ型とは、厳密には異なっています。注意しないと、コンパイルには成功しても、実行時に予期しないエラーが発生することになります。

ここでは、データ型のマーシャリングに伴う、COM/DCOMの実行時エラーを紹介します。

目次

  1. 列挙型の取りうる値の範囲に伴う問題
  2. 出力用引数の値の書き換えに伴う問題
  3. 構造体のサイズに伴う問題
  4. 構造体のアライメントに伴う問題
    1. アライメントの指定
    2. COM/DCOMにおけるアライメントの意味

列挙型の取りうる値の範囲に伴う問題

COM/DCOMでも、列挙型(enum型)を使うことができます。しかし、COM/DCOMの関数呼び出しで列挙型のデータを受け渡すと、関数の返り値(HRESULT型)に、下記のエラーコードが返されることがあります。

0x800706f5 問い合わせの値は範囲外です。
The enumeration value is out of range.

このエラーコードは、下記の値の合成となっています。

内容 結果
エラービット エラー 1
ファシリティ Win32 7
Win32エラーコード RPC_X_ENUM_VALUE_OUT_OF_RANGE 0x000006F5

これは、COM/DCOMと、C言語/C++言語とでは、列挙型の取りうる値の範囲が異なっていることが原因です。

C言語/C++言語では、特に指定をしなければ、列挙型は4バイトのsigned int型となります。

しかし、COM/DCOMでは、2バイトのunsigned short型となっています。そのため、最大でも65535までしか値を取ることができません。更に、ネットワークを通じた転送では、最大で32767までになります。

このエラーに遭遇するのは、例えば次のようなケースです。

負の値を持つ列挙型を定義した場合

C言語/C++言語の列挙型は、負の値を持つこともできます。

typedef enum {
        Bar = -1,
        Baz
} FooEnum;
              

この列挙型をCOM/DCOMのインターフェースでも使用すると、実行時にエラーが発生します。

このような列挙型をIDLの定義で使用しても、MIDL(Microsoft IDLコンパイラ)はコンパイル時にはエラーを出力しないので、注意が必要です。

また、この列挙型を使うインターフェースでも、0以上の値だけを受け渡す場合は、エラーとはなりません。上記の例では、Bazを渡す場合は正常に動作しますが、Barを渡した時点で初めてエラーになります。

初期化しなかった場合

値を受け取るためだけの次のような関数を定義したとしましょう。

HRESULT IFoo::Get( [out] FooEnum* e );
              

この場合は、次のように呼び出しても問題ありません。

FooEnum e;
HRESULT hResult = pFoo->Get(&e);
              

しかし、値を受け取ったり、送ったりする、次のような双方向の関数を定義したとしましょう。

HRESULT IFoo::GetSet( [in,out] FooEnum* e, [in] bool bGet );
              

この場合は、たとえ値を受け取る場合であっても、次のように呼び出すとエラーが発生します。

FooEnum e;
HRESULT hResult = pFoo->GetSet(&e, true);       // Get
              

これは、変数eが初期化されていないため、(たいていは)0〜32767の範囲を超える値が送られてしまうためです。

また、列挙型の配列を受け渡す場合にも、注意が必要です。

例えば、n個の列挙型の値を返す、次のような関数を定義したとしましょう。

typedef struct {
        int n;
        FooEnum e[10];
} FooStruct;

HRESULT IFoo::Get( /*[out]*/ FooStruct* s ) {
        s->n = 5;

        for (int i = 0 ; i < s->n ; i++) {
                s->e[i] = Bar;
        }

        return S_OK;
}
              

この場合、構造体FooStructのうち、e[5]〜e[9]には(たいていは)0〜32767の範囲を超える値が入ったままとなり、エラーが発生します。

いずれの場合も、列挙型を正しく初期化した上で呼び出せば、エラーは発生しません。

出力用引数の値の書き換えに伴う問題

COM/DCOMでも、C言語/C++言語と同じように、引数にポインタを渡すことで、関数から値を受け取ることができます。

typedef struct {
        int x;
        int y;
} FooStruct;

HRESULT IFoo::Get( [out] FooStruct* data );
        

しかし、COM/DCOMでは、値を受け取るために渡した変数の中身が、いったん0にクリアされます。そのため、C言語/C++言語と同じようなコードを書くと、思わぬ落とし穴にはまることがあります。

予めデフォルト値をセットしておけない

関数を呼び出す前に、変数にデフォルト値をセットしておくケースがあります。

例えば、次のC++言語の例では、m_bFlagというフラグ変数が真の場合のみ、値がセットされます。そうでなければ、関数を呼び出す前にセットしておいたデフォルトの値が表示されます。

void Foo::Get ( FooStruct* data ) {
                :

        if (this->m_bFlag) {
                data->x = this->m_x;
                data->y = this->m_y;
        }

                :
}

int main ( int argc, char** argv ) {
        Foo foo;

                :

        FooStruct data;
        data.x = 1;     // デフォルト
        data.y = 1;     // デフォルト

        foo.Get(&data);

        printf("%d %d\n", data.x, data.y);

                :
}
              

しかし、このコードをそのままCOM/DCOMに移植すると、正しく動作しません。

COM/DCOMでは、Get関数を呼び出した時点で、変数dataの値が0にクリアされます。そのため、m_bFlagというフラグ変数が偽の場合は、予めセットしておいたデフォルトの値ではなく、0が表示されます。

たとえ、リモートオブジェクトに接続できず、COM/DCOMの呼び出しに失敗した場合であっても、変数の値が0にクリアされるので、注意が必要です。

C言語/C++言語と異なり、COM/DCOMでは、関数が呼び出せない場合を考えておく必要があります。しかし、関数が呼び出せない場合に備えて、予め変数にデフォルト値をセットしておくことはできません。必ず、次のようにしなければなりません。

HRESULT h = foo.Get(&data);
if (h != S_OK) {
        data.x = 1;     // デフォルト
        data.y = 1;     // デフォルト
}
              
呼び出す前に値がクリアされる

別のスレッドで変数の値を監視して、値が変わったら何か処理を行う、というケースがあります。この時、監視している変数のポインタを引数に渡して、関数の中で値を書き変えている場合は、注意が必要です。

C言語/C++言語では、関数の中で値を書き換えても、正しく動作します。

C言語/C++言語の関数で値を書き換える場合のシーケンス

しかし、値を書き換える関数をCOM/DCOMに移植すると、正しく動作しません。COM/DCOMでは、関数を呼び出した時点で、いったん値が0にクリアされます。つまり、値が2回変化したことになってしまうのです。

COM/DCOMの関数で値を書き換える場合のシーケンス

COM/DCOMに移植すると、変数の値は、関数呼び出しが完了するまで書き換わらなくなります。そのため、タイミングに関する問題も発生するかもしれません。

なお、値を受け取るために、int型のような基本データ型の変数のポインタを渡している場合は、このような値の書き換えに伴う問題は発生しません。int型であれば、関数を呼び出しても、値が0にクリアされることはなく、C言語/C++言語と同じように動作します。

HRESULT IFoo::Get( [out] int* data );
        

つまり、COM/DCOMの場合は、int型のような基本データ型と、構造体とでは、ポインタを渡した場合の動作が異なっていますので、注意が必要です。

構造体のサイズに伴う問題

COM/DCOMのインターフェースで、64KBを超える巨大な構造体を使おうとすると、MIDL(Microsoft IDLコンパイラ)は、コンパイル時にエラーを出力します。

しかし、64KBより小さい構造体を使って、コンパイルが通った場合でも、COM/DCOMの関数を呼び出してみると、エラーが起きることがあります。

例えば、次のような構造体を定義します。

typedef struct {
        int data[2000];
} FooStruct;
        

そして、次のような関数を定義したとしましょう。

HRESULT IFoo::Set( [in] FooStruct foo );
        

この構造体のサイズは8KBなので、この関数はコンパイルに成功します。ところが、呼び出してみると、下記のエラーコードが返され、呼び出しに失敗します。

0x8007000e この操作を完了するのに十分な記憶領域がありません。
Not enough storage is available to complete this operation.

このHRESULT型の値に対応するWin32のエラーコードは、次の通りです。

0x0000000e ERROR_OUTOFMEMORY

この時、COM/DCOMサーバでは、かなり重大な問題が発生しているようです。一度このエラーが発生した後で、同じ関数をもう一度呼び出してみると、2回目からは、下記のエラーコードが返されるように変化しました。

0x800703e6 メモリの場所に無効なアクセスがありました。
Invalid access to memory location.
0x800706ba RPCサーバーを利用できません。
The RPC server is unavailable.

これらのHRESULT型の値に対応するWin32のエラーコードは、次の通りです。

0x000003e6 ERROR_NOACCESS
0x000006ba RPC_S_SERVER_UNAVAILABLE

どちらのエラーコードが返されるかは、状況によります。COM/DCOMサーバの作り方によっては、サーバでアプリケーションエラーが発生してしまうこともあるようです。

この原因は、構造体のサイズが大きすぎることです。64KBより小さければコンパイルには成功しますが、必ずしも実行できるとは限らないことになります。

FooStruct構造体の定義を次のように変更して、構造体のサイズを4KBと小さくすると、この関数を呼び出しても成功するようになります。

typedef struct {
        int data[1000];
} FooStruct;
        

実験したところでは、エラーが発生するかどうかの境目は、6KB〜8KBの間でした。

なお、関数の引数に構造体そのものを渡すのではなく、構造体のポインタを渡すようにすれば、8KBを超える大きな構造体であっても、エラーは発生しません。上記の例で言えば、関数を次のように定義すると、呼び出しに成功するようになります。

HRESULT IFoo::Set( [in] FooStruct* foo );
        

COM/DCOM関数で構造体を受け渡す時は、必ずポインタを引数に渡すようにしましょう。

構造体のアライメントに伴う問題

アライメントの指定

COM/DCOMを使う時は、構造体のアライメントにも注意しなくてはなりません。

構造体のアライメントは、Visual C++のデフォルトでは8バイト境界になっています。この設定を変更しても、コンパイルは通ります。しかし、COM/DCOMの関数を呼び出すと、アプリケーションエラーが発生してしまいます。

Visual C++の設定画面

ですが、COM/DCOMを使う時でも、構造体のアライメントを変更できない訳ではありません。

COM/DCOMの関数で構造体を使う時は、たいてい、構造体を定義したヘッダファイルを、C++言語のソースファイルと、インターフェースを定義したIDLファイルと、両方で#includeすることになるでしょう。

ヘッダファイルをインクルードする模式図

ここで、構造体を定義したヘッダファイルの中で、#pragmaで指定すれば、COM/DCOMを使う時でも、構造体のアライメントを変更できます。

pragmaでアライメントを指定した時の模式図

一方、Visual C++の設定でアライメントを変更しても、MIDL(Microsoft IDLコンパイラ)には影響しません。すると、IDLファイルと、C++言語のソースファイルとで、アライメントが食い違ってしまいます。その結果、アプリケーションエラーが発生します。

設定画面でアライメントを指定した時の模式図

COM/DCOMにおけるアライメントの意味

ところで、COM/DCOMを使う時に、構造体のアライメントを指定するのは、どういう意味があるのでしょうか。

C++言語のソースファイルに対してアライメントを指定すると、構造体のメモリ消費量を少なくすることができます。しかし、IDLファイルに対してアライメントを指定しても、クライアントとサーバの間で転送されるデータ量を減らせる訳ではないようです。

COM/DCOMのマーシャリングは、下記のようになっています。

データ形式の変換とデータ転送の模式図

COM/DCOMはバイナリ規格です。クライアントとサーバの間のデータ転送形式は、NDR (Network Data Representation) として決められています。

C++言語の構造体の形式と、NDR形式との変換は、プロキシとスタブが行います。プロキシとスタブは、IDLファイルから、MIDL(Microsoft IDL コンパイラ)によって、プロキシ・スタブDLLとして生成されます。

IDLファイルに対するアライメントの指定は、クライアントやサーバから、どのようなアライメントで構造体が渡されてくるかを、プロキシとスタブに教えてあげる、という意味になるようです。

Visual C++の設定でアライメントを変更してしまうと、プロキシが想定しているデータ形式と、実際にクライアントから渡されるデータ形式が異なってしまい、そこでアプリケーションエラーが発生してしまうようです。

アライメントの不整合

アプリケーションエラーは、chkesp.cというファイルで発生しました。このファイルが、C++言語の構造体とNDR形式との変換に関わっているようです。

データの転送量は減らせませんが、アライメントを小さくすることで、より大きな構造体を受け渡すことは可能になります。

例えば、次のような構造体を定義します。

typedef struct {
        char a;
        double b;
} BarStruct;
          

この構造体のサイズは、1バイト境界であれば9バイト、8バイト境界であれば16バイトとなります。

このBarStruct構造体を使って、次のような構造体を定義します。

typedef struct {
        BarStruct data[4500];
} FooStruct;
          

デフォルトの8バイト境界では、FooStructのサイズは72KBとなるため、コンパイルできません。しかし、アライメントを1バイト境界にすると、サイズが40KBとなり、COM/DCOMの関数で受け渡せるようになります。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.