サーバー スタブ メモリ管理
Server-Stub メモリ管理の概要
MIDL で生成されたスタブは、クライアント プロセスとサーバー プロセスの間のインターフェイスとして機能します。 クライアント スタブは、[in] 属性でマークされたパラメーターに渡されるすべてのデータをマーシャリングし、サーバー スタブに送信します。 サーバー スタブは、このデータを受信すると、呼び出し履歴を再構築し、対応するユーザー実装サーバー関数を実行します。 また、サーバー スタブは、[out] 属性でマークされたパラメーター データをマーシャリングし、クライアント アプリケーションに返します。
MSRPC で使用される 32 ビットマーシャリング されたデータ形式は、ネットワーク データ表現 (NDR) 転送構文の準拠バージョンです。 この形式の詳細については、「グループを開く」Web サイトのを参照してください。 64 ビット プラットフォームの場合、NDR64 と呼ばれる Microsoft 64 ビットの NDR 転送構文の拡張を使用して、パフォーマンスを向上させることができます。
受信データのマーシャリング解除
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 番目のバイト文字 c2 で配置が強制されます。末尾に 1 バイトのパッドがあり、2 バイトのアラインメント } ''' を保持します
- データには、フィールドごとにマーシャリングする必要がある構造が含まれています。 これらのフィールドには、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;
);
上記の呼び出しでは、サーバー スタブが plOutStructure 初期化する必要があります。これは、マーシャリングされたデータに存在せず、サーバー関数の実装で使用できるようにする必要がある暗黙的な [ref] ポインターであるためです。 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] 属性でマークされたデータを "スクラッチ" メモリとして使用しても安全です。 ただし、サーバーは、データ 再割り当てまたは解放を試みず、それらのスペースの制御を 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 送信パラメーターが含まれているため、サーバー スタブは、返されたデータを格納するためにメモリを割り当てます。 割り当てられたメモリは最初は 0 で、pNext は NULL 設定されます。 アプリケーションは、新しいリンク リストのメモリを割り当て、pOut ポイントできます。pNextを> します。pIn とそれに含まれるリンクリストはスクラッチ領域として使用できますが、アプリケーションは pNext ポインターを変更しないでください。
アプリケーションは、pInOut 指すリンク リストの内容を自由に変更できますが、最上位レベルのリンク自体はもちろん、pNext ポインターも変更しないでください。 アプリケーションがリンク リストを短縮することを決定した場合、指定された pNext ポインターが RPC 内部バッファーにリンクされているか、MIDL_user_allocate()で明示的に割り当てられたバッファーにリンクしているのかを認識できません。 この問題を回避するには、次のコードに示すように、ユーザーの割り当てを強制するリンク リスト ポインターの特定の型宣言を追加します。
typedef [force_allocate] PLINKEDLIST;
この属性により、サーバー スタブはリンク リストの各ノードを個別に割り当てることができ、アプリケーションは MIDL_user_free()を呼び出すことによって、リンク リストの短縮された部分を解放できます。 その後、アプリケーションは、新しく短縮されたリンク リストの末尾にある pNext ポインターを NULL 安全に設定できます。