次の方法で共有


サーバー スタブ メモリ管理

Server-Stub メモリ管理の概要

MIDL によって生成されたスタブは、クライアント プロセスとサーバー プロセスの間のインターフェイスとして機能します。 クライアント スタブは 、[in] 属性でマークされたパラメーターに渡されたすべてのデータをマーシャリングし、サーバー スタブに送信します。 このデータを受信すると、サーバー スタブによって呼び出し履歴が再構築され、対応するユーザー実装サーバー関数が実行されます。 また、サーバー スタブは 、[out] 属性でマークされたパラメーター データをマーシャリングし、クライアント アプリケーションに返します。

MSRPC で使用される 32 ビットマーシャリングされたデータ形式は、ネットワーク データ表現 (NDR) 転送構文の準拠バージョンです。 この形式の詳細については、「 Open Group Web サイト」を参照してください。 64 ビット プラットフォームでは、NDR64 と呼ばれる NDR 転送構文に対する Microsoft 64 ビット拡張機能を使用して、パフォーマンスを向上させることができます。

受信データのマーシャリング解除

MSRPC では、クライアント スタブは、[ in] としてタグ付けされたすべてのパラメーター データを 1 つの連続バッファーにマーシャリングして、サーバー スタブに送信します。 同様に、サーバー スタブは、クライアント スタブに戻るために、連続バッファー内の [out] 属性でマークされたすべてのデータをマーシャリングします。 RPC の下のネットワーク プロトコル レイヤーは転送のためにバッファーを断片化してパケット化できますが、断片化は RPC スタブに対して透過的です。

サーバー呼び出しフレームを作成するためのメモリ割り当ては、負荷の高い操作になる可能性があります。 サーバー スタブは、可能な限り不要なメモリ使用量を最小限に抑えようとし、サーバー ルーチンが [in] 属性または [in, out] 属性でマークされたデータを解放または再割り当てしないことを前提としています。 サーバー スタブは、不要な重複を回避するために、可能な限りバッファー内のデータの再利用を試みます。 一般的な規則は、マーシャリングされたデータ形式がメモリ形式と同じ場合、RPC は、同じ形式のデータに追加のメモリを割り当てるのではなく、マーシャリングされたデータへのポインターを使用します。

たとえば、次の RPC 呼び出しは、マーシャリングされた形式がメモリ内形式と同じ構造体で定義されます。

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

この場合、RPC は plInStructure によって参照されるデータに追加のメモリを割り当てません。むしろ、マーシャリングされたデータへのポインターをサーバー側の関数実装に渡すだけです。 RPC サーバー スタブは、スタブが "-robust" フラグ (MIDL コンパイラの n 個の最新バージョンの既定の設定) を使用してコンパイルされている場合、マーシャリング解除プロセス中にバッファーを検証します。 RPC は、サーバー側関数の実装に渡されるデータが有効であることを保証します。

plOutStructure にはメモリが割り当てられます。サーバーにデータが渡されないので注意してください。

受信データのメモリ割り当て

サーバー スタブが [in] 属性または [in, out] 属性でマークされたパラメーター データにメモリを割り当てる場合があります。 これは、マーシャリングされたデータ形式がメモリ形式と異なる場合、またはマーシャリングされたデータを構成する構造体が十分に複雑であり、RPC サーバー スタブによってアトミックに読み取られる必要がある場合に発生します。 サーバー スタブで受信したデータにメモリを割り当てる必要がある一般的なケースをいくつか次に示します。

  • データは、変化する配列または準拠する変化する配列です。 これらは、[length_is()] 属性または [first_is()] 属性が設定されている配列 (または配列へのポインター) です。 NDR では、これらの配列の最初の要素のみがマーシャリングされ、送信されます。 たとえば、次のコード スニペットでは、パラメーター pv に渡されるデータにメモリが割り当てられます。

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • データは、サイズが大きい文字列または非準拠の文字列です。 通常、これらの文字列は [size_is()] 属性でタグ付けされた文字データへのポインターです。 次の例では、 SizeString サーバー側関数に渡される文字列にはメモリが割り当てられますが、 NormalString 関数に渡される文字列は再利用されます。

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • データは単純型であり、メモリ サイズは 、enum16__int3264 などのマーシャリングされたサイズとは異なります。

  • データは、メモリアラインメントが自然なアラインメントよりも小さい、上記のデータ型のいずれかを含む、または末尾のバイトパディングを持つ構造体によって定義されます。 たとえば、次の複雑なデータ構造では、2 バイトの配置が強制され、末尾にパディングがあります。

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l;2 番目のバイト char c2 で配置が強制されます。2 バイトのアラインメント } ''' を保持する末尾の 1 バイト パッドがあります

  • データには、フィールドごとにマーシャリングする必要がある構造が含まれています。 これらのフィールドには、DCOM インターフェイスで定義されているインターフェイス ポインターが含まれます。無視されたポインター。[range] 属性で設定された整数値。[wire_marshal]、[user_marshal][transmit_as]、および [represent_as] 属性で定義された配列の要素。と埋め込まれた複雑なデータ構造。
  • データには、共用体、共用体を含む構造体、または共用体の配列が含まれます。 共用体の特定の分岐のみがワイヤ上にマーシャリングされます。
  • データには、少なくとも 1 つの非固定ディメンションを持つ多次元準拠配列を持つ構造体が含まれています。
  • データには、複雑な構造体の配列が含まれています。
  • データには、 enum16 や __int3264 などの単純なデータ型の配列 が含まれています
  • データには、ref ポインターとインターフェイス ポインターの配列が含まれています。
  • データには、ポインターに [force_allocate] 属性が適用されています。
  • データには、ポインターに [allocate(all_nodes)] 属性が適用されています。
  • データには、ポインターに [byte_count] 属性が適用されています。

64 ビット データと NDR64 転送構文

前述のように、64 ビット データは NDR64 と呼ばれる特定の 64 ビット転送構文を使用してマーシャリングされます。 この転送構文は、ポインターが 32 ビット NDR でマーシャリングされ、64 ビット プラットフォーム上のサーバー スタブに送信されるときに発生する特定の問題に対処するために開発されました。 この場合、マーシャリングされた 32 ビット データ ポインターは 64 ビットのデータ ポインターと一致せず、メモリ割り当ては常に発生します。 64 ビット プラットフォームでより一貫性のある動作を作成するために、Microsoft は NDR64 という新しい転送構文を開発しました。

この問題を示す例を次に示します。

typedef struct PtrStruct
{
  long l;
  long *pl;
}

この構造体は、マーシャリングされると、32 ビット システム上のサーバー スタブによって再利用されます。 ただし、サーバー スタブが 64 ビット システムに存在する場合、NDR マーシャリングされたデータの長さは 4 バイトですが、必要なメモリ サイズは 8 になります。 その結果、メモリの割り当てが強制され、バッファーの再利用はほとんど発生しません。 NDR64 は、ポインターのマーシャリングされたサイズを 64 ビットにすることで、この問題に対処します。

32 ビット NDR とは対照的に、 enum16__int3264 などの単純なデータ 型では、NDR64 の下で構造体や配列が複雑になりません。 同様に、末尾のパッド値は構造体を複雑にしません。 インターフェイス ポインターは、最上位レベルで一意のポインターとして扱われます。その結果、インターフェイス ポインターを含む構造体と配列は複雑とは見なされず、使用するために特定のメモリ割り当てを必要としません。

送信データの初期化

すべての受信データが未登録になった後、サーバー スタブは [out] 属性でマークされた送信専用ポインターを初期化する必要があります。

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

上記の呼び出しでは、サーバー スタブは、マーシャリングされたデータに存在せず、サーバー関数の実装で使用できるようにする必要がある暗黙的な [ref] ポインターであるため、plOutStructure を初期化する必要があります。 RPC サーバー スタブは、[ out] 属性を使用して最上位レベルの参照専用ポインターを初期化し、ゼロにします。 その下にある [out] 参照ポインターも再帰的に初期化されます。 再帰は、[ unique] 属性または [ptr] 属性が設定されているポインターで停止します。

サーバー関数の実装では、最上位のポインター値を直接変更できないため、再割り当てできません。 たとえば、上記の ProcessRpcStructure の実装では、次のコードは無効です。

void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
    plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
    Process(plOutStructure);
}

plOutStructure はスタック値であり、その変更は RPC に反映されません。 サーバー関数の実装では、 plOutStructure を解放しようとして割り当てを回避できます。これにより、メモリが破損する可能性があります。 その後、サーバー スタブは、メモリ内の最上位ポインター (ポインターからポインターの場合) と、スタック上のサイズが予想よりも小さい最上位の単純な構造体に領域を割り当てます。

クライアントは、特定の状況で、サーバー側のメモリ割り当てサイズを指定できます。 次の例では、クライアントは受信サイズ パラメーターに送信データの サイズ を指定します。

void VariableSizeData
(
    [in] long size,
    [out, size_is(size)] char *pv
);

サイズを含む受信データのマーシャリングを解除した後、サーバー スタブは"sizeof(char)*size" のサイズの pv にバッファーを割り当てます。 領域が割り当てられると、サーバー スタブによってバッファーがゼロになります。 この特定のケースでは、バッファーのサイズが実行時に決定されるため、スタブは MIDL_user_allocate()を使用してメモリを割り当てることに注意してください。

DCOM インターフェイスの場合、クライアントとサーバーが同じ COM アパートメントを共有している場合、または ICallFrame が実装されている場合は、MIDL で生成されたスタブがまったく関与しない可能性があることに注意してください。 この場合、サーバーは割り当て動作に依存できず、クライアント サイズのメモリを個別に検証する必要があります。

サーバー側関数の実装と送信データ マーシャリング

受信データのアンマーシャリングと、送信データを格納するために割り当てられたメモリの初期化の直後に、RPC サーバー スタブは、クライアントによって呼び出される関数のサーバー側実装を実行します。 現時点では、サーバーは [in, out] 属性で特別にマークされたデータを変更でき、送信専用データ ( [out] でタグ付けされたデータ) に割り当てられたメモリを設定できます。

マーシャリングされたパラメーター データの操作に関する一般的な規則は簡単です。サーバーは新しいメモリのみを割り当てたり、サーバー スタブによって特別に割り当てられたメモリを変更したりできます。 データの既存のメモリを再割り当てまたは解放すると、関数呼び出しの結果とパフォーマンスに悪影響を及ぼす可能性があり、デバッグが非常に困難になる可能性があります。

論理的には、RPC サーバーはクライアントとは異なるアドレス空間に存在し、通常はメモリを共有しないと見なすことができます。 その結果、サーバー関数の実装では、クライアント のメモリ アドレスに影響を与えることなく 、"スクラッチ" メモリとして [in] 属性でマークされたデータを使用しても問題ありません。 つまり、サーバーは [in] データの再割り当てまたは解放を試みず、それらのスペースの制御を RPC サーバー スタブ自体に残すべきではありません。

一般に、サーバー関数の実装では、[ in, out] 属性でマークされたデータを再割り当てまたは解放する必要はありません。 固定サイズ データの場合、関数実装ロジックはデータを直接変更できます。 同様に、可変サイズのデータの場合、関数の実装では 、[size_is()] 属性に指定されたフィールド値も変更できません。 データのサイズを変更するために使用するフィールド値を変更すると、クライアントに返されるバッファーのサイズが小さくなり、異常な長さに対処できない可能性があります。

[in, out] 属性でマークされたデータによって使用されるメモリをサーバー ルーチンが再割り当てする必要がある状況が発生した場合、スタブによって提供されるポインターが、MIDL_user_allocate() またはマーシャリングされたワイヤ バッファーで割り当てられたメモリに対するかどうかは、サーバー側の関数の実装で認識されない可能性があります。 この問題を回避するために、MS RPC では、データに [force_allocate] 属性が設定されている場合に、メモリ リークや破損が発生しないようにすることができます。 [force_allocate] が設定されている場合、サーバー スタブは常にポインターにメモリを割り当てますが、注意が必要なのは、使用するたびにパフォーマンスが低下することです。

呼び出しがサーバー側関数の実装から返されると、サーバー スタブは [out] 属性でマークされたデータをマーシャリングし、クライアントに送信します。 サーバー側関数の実装で例外がスローされた場合、スタブはデータをマーシャリングしないことに注意してください。

割り当てられたメモリの解放

RPC サーバー スタブは、例外が発生したかどうかにかかわらず、呼び出しがサーバー側関数から返された後にスタック メモリを解放します。 サーバー スタブは、スタブによって割り当てられたすべてのメモリと、 MIDL_user_allocate()で割り当てられたすべてのメモリを解放します。 サーバー側関数の実装では、例外をスローするか、エラー コードを返すことによって、RPC に常に一貫性のある状態を与える必要があります。 複雑なデータ構造の作成中に関数が失敗した場合は、すべてのポインターが有効なデータを指しているか 、NULL に設定されていることを確認する必要があります。

このパスの間、サーバー スタブは[ in] データを含むマーシャリングされたバッファーの一部ではないすべてのメモリを解放します。 この動作の 1 つの例外は、[ allocate(dont_free)] 属性が設定されたデータです。サーバー スタブは、これらのポインターに関連付けられているメモリを解放しません。

スタブと関数実装によって割り当てられたメモリがサーバー スタブによって解放された後、スタブは、特定のデータに 対して [notify_flag] 属性が指定されている場合に、特定の通知関数を呼び出します。

RPC を介したリンク リストのマーシャリング -- 例

typedef struct _LINKEDLIST
{
    long lSize;
    [size_is(lSize)] char *pData;
    struct _LINKEDLIST *pNext;
} LINKEDLIST, *PLINKEDLIST;

void Test
(
    [in] LINKEDLIST *pIn,
    [in, out] PLINKEDLIST *pInOut,
    [out] LINKEDLIST *pOut
);

上記の例では、 LINKEDLIST のメモリ形式はマーシャリングされたワイヤ形式と同じです。 その結果、サーバー スタブは 、pIn の下のデータ ポインターのチェーン全体にメモリを割り当てません。 代わりに、RPC はリンク リスト全体のワイヤ バッファーを再利用します。 同様に、スタブは pInOut にメモリを割り当てず、代わりにクライアントによってマーシャリングされたワイヤ バッファーを再利用します。

関数シグネチャには送信パラメーター pOut が含まれているため、サーバー スタブは、返されたデータを格納するためにメモリを割り当てます。 割り当てられたメモリは、最初はゼロアウトされ、 pNext はNULL に設定されます。 アプリケーションは、新しいリンク リストのメモリを割り当て、pOut-pNext> をポイントできます。pIn とそのリストに含まれるリンクリストはスクラッチ領域として使用できますが、アプリケーションは pNext ポインターを変更しないでください。

アプリケーションは 、pInOut が指すリンク リストの内容を自由に変更できますが、最上位レベルのリンク自体はもちろん、 pNext ポインターを変更することはできません。 アプリケーションがリンク リストを短くすることにした場合、指定された pNext ポインターが RPC 内部バッファーに t をリンクしているか、 MIDL_user_allocate()で特別に割り当てられたバッファーにリンクしているかはわかりません。 この問題を回避するには、次のコードに示すように、ユーザーの割り当てを強制するリンク リスト ポインターの特定の型宣言を追加します。

typedef [force_allocate] PLINKEDLIST;

この属性により、サーバー スタブはリンク リストの各ノードを個別に割り当てることができ、アプリケーションは MIDL_user_free() を呼び出すことでリンク リストの短縮部分を解放できます。 その後、アプリケーションは、新しく短縮されたリンク リストの末尾にある pNext ポインターを NULL に安全に設定できます。