Skip to content

MS_Marshaling

nishi_74322014 edited this page Sep 1, 2026 · 1 revision

マーシャリング

概要

マイクロソフト技術書では、

  • マネージ(.NET)・アンマネージ(C/C++)コード間の相互運用
  • COM を使用したイン・プロセス呼び出し
  • COM を使用した別・プロセス呼び出し
  • DCOM を使用したリモート・サーバ呼び出し

における「データ変換」を指していることが多い。

詳細

  • マーシャリングの「Marshal」は、

    • 一般的には
      「鉄道 操車場」
      (分岐器、ハンプなどを経て目的の仕分線に送る設備。ヤードとも呼ばれる)
      を意味するが、

    • IT 用語としては、
      COM などで実装されていた「マーシャリング」を指し、
      アプリケーション ドメイン、プロセス、開発技術などの
      各境界を越えて、オブジェクトを転送する技術の総称

    である。

  • 開発技術の境界を越えるための技術としては、以下のものがある。

補足(語源の説明は正確だが、もう一段の由来がある): 「鉄道操車場」という
説明は辞書的には正しいが、計算機用語としての系譜も補っておく。

【marshal の原義】★
   ・古フランク語 marhskalk(馬の世話人)
     → 「整列させる / 順序立てて配置する」
     → 軍隊の「元帥(Marshal)」も同語源
   ・鉄道用語の marshalling yard(操車場)は
     「貨車を並べ替える場所」

【計算機用語としての意味】★
   ・【メモリ上のオブジェクトを、
     境界を越えて運べる形に「並べ直す」】
     → シリアライズ(直列化)と近いが、
       マーシャリングは【参照の扱いまで含む】点が違う

   シリアライズ … 値をバイト列にする
   マーシャリング … 値も参照も含めて
                   「向こう側で意味を持つ形」に変換する ★
                   (= Proxy を立てる、ハンドルを複製する等)

相互運用マーシャリング

マネージコード・アンマネージドコード間のデータ変換のこと。

C/C++ と .NET では当然メモリ上のデータの表現が異なる。

例えば、.NET では、

  • ポインタを直接扱うことができないし、
  • オブジェクトは GC(ガベージコレクタ)などで管理されている。

なので、マネージコード・アンマネージドコード間のデータ変換が必要になる。

補足(「ポインタを直接扱うことができない」は少し古い): 原文の説明は
「安全なコードでは」という前提での話であり、正確には扱える

【.NET でポインタを扱う手段】★
   ・【unsafe + ポインタ】(C# の unsafe コンテキスト)
       unsafe { byte* p = ...; }
     → プロジェクトで AllowUnsafeBlocks が必要
   ・【IntPtr / nint】… ハンドルやアドレスの受け渡し
   ・【Span<T> / Memory<T>】★(.NET Core 2.1〜)
     → 【unsafe なしで】連続メモリを安全に扱える
     → スタック・配列・アンマネージ メモリを
       同じ抽象で扱える
   ・【fixed】… GC の移動を止めてアドレスを固定する ★
     → 「ピン留め」。長時間行うとヒープが断片化する
   ・【GCHandle】… マネージ オブジェクトを
     アンマネージ側に渡す間、回収されないようにする

【本質的な難しさ】★
   ・GC が【オブジェクトを動かす】ことが問題の根源
     → アンマネージ側にアドレスを渡した後で
       GC が動かすと、そのアドレスは無効になる
     → だから【ピン留め】が要る
   ・所有権の所在
     → 誰が確保し、誰が解放するのか
     → 取り違えるとリーク or ヒープ破壊

プリミティブ型のマーシャリング

マネージ(.NET)からアンマネージ(C/C++)の DLL や COM を呼び出す場合、
相互運用マーシャラーによって、
マネージコード・アンマネージドコード間のデータが変換される。

補足(実際に躓く型): 「自動的に変換される」ものと
明示的な指定が要るものがある。

【そのまま通る(Blittable 型)】★
   byte, sbyte, short, ushort, int, uint,
   long, ulong, float, double, IntPtr
     → メモリ表現が同一なので【コピーすら不要】
     → 配列も Blittable ならピン留めして直接渡せる

【変換が必要(Non-Blittable 型)】★
   bool    … .NET は 1 byte、Win32 の BOOL は 4 byte ★
             → [MarshalAs(UnmanagedType.Bool)]
   char    … .NET は UTF-16、C は環境依存
   string  … 表現が全く違う(下記)
   構造体  … レイアウトが違いうる

【文字列の指定】★
   [DllImport("user32", CharSet = CharSet.Unicode)]
     CharSet.Ansi     … char* (変換コストあり)
     CharSet.Unicode  … wchar_t* 【Windows API は本来こちら】★
     CharSet.Auto     … OS に合わせる(現在は実質 Unicode)

   ・関数名の A / W 接尾辞は
     CharSet に応じて【自動で付けられる】
       MessageBox → MessageBoxW(Unicode 指定時)
   ・戻り値が文字列の場合は
     【誰が解放するか】が問題になる
     → StringBuilder で受ける / IntPtr で受けて
       Marshal.PtrToStringUni + 適切な解放

構造体のマーシャリング

Marshal クラスを使用して構造体のマーシャリングが可能。

構造体配列、構造体配列配列、構造体配列の階層構造など。

構造体配列のマーシャリングを行う場合は、
要素数などは別途引数などを用意して把握できるようにしておく必要がある。

補足(構造体マーシャリングの必須知識): 「要素数を別途渡す」という
指摘は本質的である。C の世界には配列長の概念がないためである。

【StructLayout は必ず指定する】★
   [StructLayout(LayoutKind.Sequential)]
   struct POINT { public int x; public int y; }

     LayoutKind.Sequential … 宣言順に並べる【既定にすべき】★
     LayoutKind.Explicit   … FieldOffset で位置を明示
                             (共用体 union の再現に使う)
     LayoutKind.Auto       … CLR が自由に並べ替える
                             → 【相互運用に使ってはいけない】

【Pack(アライメント)】★
   [StructLayout(LayoutKind.Sequential, Pack = 1)]
     → C 側が #pragma pack(1) なら合わせる必要がある
     → 【合っていないとフィールドがずれる】
       (エラーにならず、値だけ狂うので発見が遅れる)★

【配列を含む構造体】
   [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)]
   public int[] values;     // 固定長配列(構造体に埋め込む)

   [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
   public string path;      // 固定長文字列バッファ

【サイズの確認】★
   Marshal.SizeOf<T>()  で C 側の sizeof と一致するか
   【必ず検証する】
     → 一致しなければレイアウト指定が誤っている
【構造体配列の受け渡し(原文の主題)】★
   // 確保 → コピー → 解放 を自分で行う
   int size = Marshal.SizeOf<POINT>();
   IntPtr buf = Marshal.AllocHGlobal(size * count);
   try {
       for (int i = 0; i < count; i++)
           Marshal.StructureToPtr(items[i],
               buf + i * size, false);
       NativeCall(buf, count);      // ← 【count を別途渡す】★
   } finally {
       Marshal.FreeHGlobal(buf);    // ← 【必ず解放】★
   }

   ※ AllocHGlobal / FreeHGlobal は対で使う
   ※ AllocCoTaskMem / FreeCoTaskMem は COM 用
     → 【確保元と解放元を取り違えるとヒープが壊れる】★
【今なら「ソース生成」が使える】★
   .NET 7 以降の【LibraryImport】属性
     [LibraryImport("user32.dll", StringMarshalling =
         StringMarshalling.Utf16)]
     internal static partial int MessageBoxW(...);

     → コンパイル時にマーシャリング コードが生成される
     → 【実行時のリフレクションが不要】=速い
     → AOT(Native AOT)と相性がよい ★
     → DllImport は今も動くが、新規は LibraryImport 推奨

オブジェクトのマーシャリング

補足(MarshalByRefObject は事実上役目を終えた): このクラスは
.NET Remoting とアプリケーション ドメインを前提にしていた。

【値渡し(MBV)と参照渡し(MBR)】★
   ・[Serializable] なクラス
       → 【コピーが相手側に作られる】(Marshal By Value)
   ・MarshalByRefObject の派生
       → 相手側には【Proxy が作られる】(Marshal By Reference)
       → メソッド呼び出しが元のドメインへ転送される ★

【現況】
   ・【.NET Remoting は .NET Core に移植されなかった】★
   ・【AppDomain の複数生成も不可能】
     (→ [.NET4におけるサンドボックス化API](MS_DotNet4Sandbox) 参照)
   ・MarshalByRefObject 型自体は残っているが、
     【実質的に意味を持たない】

【代替】
   ・プロセス間 → 【gRPC / 名前付きパイプ / HTTP】★
   ・.NET 内の分離 → AssemblyLoadContext
   ・「向こう側のオブジェクトを操作する」感覚が要るなら
     → gRPC のストリーミング or SignalR

COMを使用したイン・プロセス呼び出し

生成済みの STACOM コンポーネントのポインタを使用して
マルチスレッド・クライアントから呼び出した場合、
Windows メッセージキューによって呼び出しが直列化される。

これもマーシャリングの一種である。

移行メモ(誤字): 移行元の「Windwos メッセージキュー」を
Windows メッセージキュー」に修正した。
なお、この仕組みの詳細は ウィンドウ メッセージ を参照。

COMを使用した別・プロセス呼び出し

ローカル・プロセスの引数・戻り値のデータをリモート・プロセスにコピーする。

DCOMを使用したリモート・サーバ呼び出し

ローカル・プロセスの引数・戻り値のデータをリモート・サーバのプロセスにコピーする。

補足(DCOM の現在): 記述は正しいが、運用上の注意が大きく変わった。

【DCOM 強化(Windows の既定変更)】★
   CVE-2021-26414 への対応として、
   Microsoft は DCOM の【認証レベルを引き上げ】た。
     2021年6月  修正提供(既定は無効)
     2022年6月  既定で有効(無効化可能)
     2023年3月  【強制的に有効。無効化不可】★

   → 古い DCOM クライアント/サーバは
     【RPC_E_ACCESS_DENIED 等で動かなくなった】
   → 双方を更新するか、DCOM の利用をやめる必要がある

【そもそも DCOM は現代の環境と相性が悪い】
   ・【動的ポート】を使う(135 + 高位ポート)
     → ファイアウォール / NAT を越えられない
   ・認証が Windows 統合前提
   ・障害時の切り分けが極めて難しい

   → 新規で「マシンを越えてオブジェクトを呼ぶ」なら
     【HTTP / gRPC】を選ぶ ★

カスタムマーシャリング

COM のカスタムマーシャリングを説明にするには、
ADODB.Recordset のマーシャリングなどが良いサンプルになる。

  • ADODB.Recordset のポインタを渡すとプロセス間を超えて、
    ADODB.Recordset のオブジェクト階層がコピーされる。

  • プロセス A、プロセス B で ADODB.Recordset のポインタが共有されている場合、
    プロセス A で ADODB.Recordset に AddNew メソッドを呼び出し行を追加すると、
    プロセス間を超えてプロセス B の ADODB.Recordset に行が追加される。

また、カスタムマーシャリングを相互運用(マネージ型を COM に公開する)で
使用することも可能。

補足(ADODB.Recordset を例に選んだのは的確): この例が
カスタム マーシャリングの本質をよく示している理由を補っておく。

【既定のマーシャリングでは足りない理由】★
   ・Recordset は【行・列の階層構造】を持つ
   ・既定の Proxy/Stub で渡すと、
     1 セル読むたびに【プロセス間呼び出しが発生】する
     → 1000 行 × 20 列 = 20,000 往復
     → 実用にならない ★

【カスタム マーシャリングがすること】
   ・IMarshal インターフェースを実装し、
     「自分をどう運ぶか」を【自分で決める】
   ・Recordset は
     → 【中身を丸ごとシリアライズして一度に送る】
     → 向こう側で復元する(=実質 Marshal By Value)★
     → だから「ポインタを渡したのにコピーされる」
       という原文の観察になる

【原文の 2 番目の記述について】
   「プロセス A で AddNew すると
     プロセス B にも行が追加される」
     → これは【切断されたレコードセットではなく、
       接続されたまま Proxy を経由している場合】の挙動
     → ADODB.Recordset は
       「切断(Disconnected)」と「接続」で
       マーシャリングの振る舞いが変わる ★
     → 両方の記述が併記されているのは
       この 2 モードを反映している
【現代における同じ問題】★
   「粒度の細かい呼び出しを境界越しにするな」
   という教訓は【今もそのまま通用する】
     ・N+1 クエリ問題(ORM)
     ・REST API の chatty な設計
     → 【まとめて 1 回で運ぶ】(DTO / バッチ取得)
     → GraphQL / gRPC のストリーミングも
       この課題への回答である

参考

移行メモ(自己リンク): 移行元では参考の 1 件目が
[[マーシャリング]]【marshalling】の意味
自ページへのリンクを含む形になっていたため、リンクを外した。


Tags: 移行, Windows, プログラミング, .NET開発

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally