首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 結構體序列化實戰:跨進程記憶體佈局與 Marshal 互操作

     編輯:關於C#

場景:為什麼需要手工序列化結構體?

在與 C/C++ DLL 互操作、共享記憶體、網路封包組裝、硬體通訊等場景,必須確保結構體在記憶體中的位元組佈局與非託管端完全一致。C# 預設的序列化器(BinaryFormatter、JSON、Protobuf)皆不保證記憶體佈局,故需使用 System.Runtime.InteropServices.Marshal 進行精確控制。

結構體定義:屬性控制佈局

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
public struct DataPacket
{
    public int MessageID;                    // 4 bytes
    public double Price;                     // 8 bytes
    public long Quantity;                    // 8 bytes

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 20)]
    public string Symbol;                    // 固定 20 字節 ANSI 字串
    // 若需字元陣列可改用:
    // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
    // public char[] Symbol;
}

屬性解析:

  • LayoutKind.Sequential:按宣告順序依序排列,配合 Pack = 1 取消對齊填充,確保緊湊佈局。
  • CharSet.Ansi:字串以 ANSI 編碼(單字節)封送,避免 Unicode 雙字節導致大小不符。
  • MarshalAs(ByValTStr, SizeConst=20):固定長度字串,直接內嵌於結構體中,不另行配置堆記憶體。

序列化:結構體 → 位元組陣列

public static byte[] Serialize(T structure) where T : struct
{
    int size = Marshal.SizeOf();                 // 取得非託管大小
    IntPtr ptr = Marshal.AllocHGlobal(size);        // 非託管堆配置記憶體
    try
    {
        Marshal.StructureToPtr(structure, ptr, false); // 複製結構體到非託管記憶體
        byte[] bytes = new byte[size];
        Marshal.Copy(ptr, bytes, 0, size);          // 複製到託管位元組陣列
        return bytes;
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);                   // 必須釋放非託管記憶體
    }
}

反序列化:位元組陣列 → 結構體

public static T Deserialize(byte[] data) where T : struct
{
    int size = Marshal.SizeOf();
    if (data.Length < size) throw new ArgumentException("資料長度不足");

    IntPtr ptr = Marshal.AllocHGlobal(size);
    try
    {
        Marshal.Copy(data, 0, ptr, size);           // 託管 → 非託管
        return Marshal.PtrToStructure(ptr);      // 非託管 → 結構體
    }
    finally
    {
        Marshal.FreeHGlobal(ptr);
    }
}

完整範例:共享記憶體讀寫

// 寫入端
var packet = new DataPacket
{
    MessageID = 1,
    Price = 1.23,
    Quantity = 100,
    Symbol = "AAPL"
};
byte[] raw = Serialize(packet);
SharedMemory.Write(raw, raw.Length);  // 假設的共享記憶體類別

// 讀取端
byte[] buffer = new byte[Marshal.SizeOf()];
SharedMemory.Read(ref buffer);
var recv = Deserialize(buffer);
Console.WriteLine($"{recv.MessageID},{recv.Quantity},{recv.Price},{recv.Symbol}");

效能優化建議

  1. 緩衝區重用:高頻場景下,預先配置 byte[] 與 IntPtr,避免頻繁 GC 壓力。
  2. Span<byte> / Memory<byte>:.NET Core 3.0+ 可搭配 MemoryMarshal.Cast 實現零複製轉換。
  3. 避免字串:傾向使用 fixed char[] 或 byte[] 承載固定長度文本,減少封送開銷。

常見坑點

問題原因對策
大小不符Pack/CharSet/SizeConst 設定錯誤用 Marshal.SizeOf 對比 C++ sizeof
中文亂碼CharSet.Ansi 無法容納 Unicode改用 CharSet.Unicode + SizeConst=40(雙字節)
記憶體洩漏忘記 FreeHGlobal統一用 try/finally 或 SafeHandle 封裝
對齊導致位移預設 Pack=8 在 64 位元下插入填充明確指定 Pack = 1 或與非託管端一致

總結

掌握 StructLayout、MarshalAs、Marshal 三大支柱,即可在 C# 中實現與 C/C++ 完全相容的記憶體佈局,滿足高效能互操作、共享記憶體、硬體通訊等嚴苛需求。本文提供的泛型輔助方法可直接落地專案,並作為更複雜協議(如自定義二進位協議、零複製網路框架)的基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved