1. 程式人生 > >在 C# 中處理結構內的陣列

在 C# 中處理結構內的陣列


    在 C/C++ 程式碼中,大量摻雜著包括普通型別和陣列的結構,如定義 PE 檔案頭結構的 IMAGE_OPTIONAL_HEADER 結構定義如下:

以下內容為程式程式碼:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

typedef struct _IMAGE_OPTIONAL_HEADER {

    WORD    Magic;

    //...

    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

    在 C/C++ 中這樣在結構中使用陣列是完全正確的,因為這些陣列將作為整個結構的一部分,在對結構操作時直接訪問結構所在記憶體塊。但在 C# 這類語言中,則無法直接如此使用,因為陣列是作為一種特殊的引用型別存在的,如定義:
以下內容為程式程式碼:

public struct IMAGE_DATA_DIRECTORY
{
  public uint VirtualAddress;
  public uint Size;
}

public struct IMAGE_OPTIONAL_HEADER
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  //...

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}

    在 C# 中這樣定義結構中的陣列是錯誤的,會在編譯時獲得一個 CS0650 錯誤:
以下為引用:

error CS0650: 語法錯誤,錯誤的陣列宣告符。若要宣告託管陣列,秩說明符應位於變數識別符號之前


    如果改用 C# 中引用型別的類似定義語法,如
以下內容為程式程式碼:

public struct IMAGE_OPTIONAL_HEADER
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  //...

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY[] DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}

    則得到一個 CS0573 錯誤:
以下為引用:

error CS0573: “IMAGE_OPTIONAL_HEADER.DataDirectory” : 結構中不能有例項欄位初始值設定項


    因為結構內是不能夠有引用型別的初始化的,這與 class 的初始化工作不同。如此一來只能將陣列的初始化放到建構函式中,而且結構還不能有無引數的預設建構函式,真是麻煩,呵呵
以下內容為程式程式碼:

public struct IMAGE_OPTIONAL_HEADER
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY[] DataDirectory;

  public IMAGE_OPTIONAL_HEADER(IntPtr ptr)
  {
    Magic = 0;
    NumberOfRvaAndSizes = 0;

    DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  }
}

    這樣一來看起來似乎能使了,但如果使用 Marshal.SizeOf(typeof(IMAGE_OPTIONAL_HEADER)) 看看就會發現,其長度根本就跟 C/C++ 中定義的長度不同。問題還是在於結構中陣列,雖然看起來此陣列是定義在結構內,但實際上在此結構中只有一個指向 IMAGE_DATA_DIRECTORY[] 陣列型別的指標而已,本應儲存在 DataDirectory 未知的陣列內容,是在託管堆中。
    於是問題就變成如何將引用型別的陣列,放在一個值型別的結構中。

    解決的方法有很多,如通過 StructLayout 顯式指定結構的長度來限定內容:
以下內容為程式程式碼:

[StructLayout(LayoutKind.Sequential, Size=XXX)]
public struct IMAGE_OPTIONAL_HEADER
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY DataDirectory;
}

    注意這兒 StructLayout 中 Size 指定的是整個結構的長度,因為 DataDirectory 已經是最後一個欄位,故而陣列的後 15 個元素被儲存在未命名的堆疊空間內。使用的時候稍微麻煩一點,需要一次性讀取整個結構,然後通過 unsafe 程式碼的指標操作來訪問 DataDirectory 欄位後面的其他陣列元素。
    這種方法的優點是定義簡單,但使用時需要依賴 unsafe 的指標操作程式碼,且受到陣列欄位必須是在最後的限制。當然也可以通過 LayoutKind.Explicit 顯式指定每個欄位的未知來模擬多個結構內嵌陣列,但這需要手工計算每個欄位偏移,比較麻煩。

    另外一種解決方法是通過 Marshal 的支援,顯式定義陣列元素所佔位置,如
以下內容為程式程式碼:

[StructLayout(LayoutKind.Sequential, Pack=1)]
public struct IMAGE_OPTIONAL_HEADER
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  public uint NumberOfRvaAndSizes;

  [MarshalAs(UnmanagedType.ByValArray, SizeConst=IMAGE_NUMBEROF_DIRECTORY_ENTRIES)]
  public IMAGE_DATA_DIRECTORY[] DataDirectory;
}

    這種方法相對來說要優雅一些,通過 Marshal 機制支援的屬性來定義值陣列語義,使用起來與普通的陣列區別不算太大。上述陣列定義被編譯成 IL 定義:
以下內容為程式程式碼:

.field public  marshal( fixed array [16]) valuetype IMAGE_DATA_DIRECTORY[] DataDirectory

    雖然型別還是 valuetype IMAGE_DATA_DIRECTORY[],但因為 marshal( fixed array [16]) 的修飾,此陣列已經從引用語義改為值語義。不過這樣做還是會受到一些限制,如不能多層巢狀、使用時效能受到影響等等。

    除了上述兩種在結構定義本身做文章的解決方法,還可以從結構的操作上做文章。

    此類結構除了對結構內陣列的訪問外,主要的操作型別就是從記憶體塊或輸入流中讀取整個結構,因此完全可以使用 CLR 提高的二進位制序列化支援,通過實現自定義序列化函式來完成資料的載入和儲存,如:
以下內容為程式程式碼:


[Serializable]
public struct IMAGE_OPTIONAL_HEADER : ISerializable
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY[] DataDirectory;

  public IMAGE_OPTIONAL_HEADER(IntPtr ptr)
  {
    Magic = 0;
    NumberOfRvaAndSizes = 0;

    DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
  }

  [SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter=true)]
  public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    // 完成序列化操作
  }
}

    這種解決方法可以將結構的載入和儲存,與結構的內部表現完全分離開來。雖然結構內部儲存的只是陣列引用,但使用者並不需關心。但缺點是必須為每個結構都編寫相應的序列化支援程式碼,編寫和維護都比較麻煩。

    與此思路類似的是我比較喜歡的一種解決方法,通過一個公共工具基類以 Reflection 的方式統一處理,如:
以下內容為程式程式碼:

public class IMAGE_OPTIONAL_HEADER : BinaryBlock
{
  public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16;

  public ushort Magic;

  public uint NumberOfRvaAndSizes;

  public IMAGE_DATA_DIRECTORY[] DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}

    注意原本的 struct 在這兒已經改為 class,因為通過這種方式已經沒有必要非得固守值型別的記憶體模型。BinaryBlock 是一個公共的工具基類,負責通過 Reflection 提供型別的載入和儲存功能,如
以下內容為程式程式碼:

public class BinaryBlock
{
  private static readonly ILog _log = LogManager.GetLogger(typeof(BinaryBlock));

public BinaryBlock()
{
}

  static public object LoadFromStream(BinaryReader reader, Type objType)
  {
    if(objType.Equals(typeof(char)))
    {
      return reader.ReadChar();
    }
    else if(objType.Equals(typeof(byte)))
    {
      return reader.ReadByte();
    }
    //...
    else if(objType.Equals(typeof(double)))
    {
      return reader.ReadDouble();
    }
    else if(objType.IsArray)
    {
      // 處理陣列的情況
    }
    else
    {
      foreach(FieldInfo field in ClassType.GetFields())
      {
        field.SetValue(obj, LoadFromStream(...));
      }
    }

    return true;
  }

  public bool LoadFromStream(Stream stream)
  {
    return LoadFromStream(new BinaryReader(stream), this);
  }
}

    LoadFromStream 是一個巢狀方法,負責根據指定欄位型別從流中載入相應的值。使用時只需要對整個型別呼叫此方法,則會自動以 Reflection 機制,遍歷類的所有欄位進行處理,如果有巢狀定義的情況也可以直接處理。使用此方法,型別本身的定義基本上就無需擔心載入和儲存機制,只要從 BinaryBlock 型別繼承即可。有興趣的朋友還可以對此類進一步擴充套件,支援二進位制序列化機制。

    此外 C# 2.0 中為了解決此類問題提供了一個新的 fixed array 機制,支援在結構中直接定義內嵌值語義的陣列,如
以下內容為程式程式碼:

struct data
{
    int header;
    fixed int values[10];
}

    此結構在編譯時由編譯器將陣列欄位翻譯成一個外部值型別結構,以實現合適的空間佈局,如
以下內容為程式程式碼:

.class private sequential ansi sealed beforefieldinit data
       extends [mscorlib]System.ValueType
{
  .class sequential ansi sealed nested public beforefieldinit '<values>e__FixedBuffer0'
         extends [mscorlib]System.ValueType
  {
    .pack 0
    .size 40
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 [img]/images/wink.gif[/img]
    .field public int32 FixedElementField
  } // end of class '<values>e__FixedBuffer0'

  .field public int32 header
  .field public valuetype data/'<values>e__FixedBuffer0' values
  .custom instance void [mscorlib]System.Runtime.CompilerServices.FixedBufferAttribute::.ctor(class [mscorlib]System.Type, int32) = ( ...)
} // end of class data

    可以看到 values 欄位被編譯成一個值型別,而值型別本身使用的是類似於上述第一種解決方法的思路,強行限制結構長度。而在使用時,也完全是類似於第一種解決方法的 unsafe 操作,如對此陣列的訪問被編譯成 unsafe 的指標操作:
以下內容為程式程式碼:

// 編譯前
for(int i=0; i<10; i++)
  d.values[i] = i;

// 編譯後
for(int i=0; i<10; i++)
  &data1.values.FixedElementField[(((IntPtr) i) * 4)] = i;

    不幸的是這種方式必須通過 unsafe 方式編譯,因為其內部都是通過 unsafe 方式實現的。而且也只能處理一級的巢狀定義,如果將 IMAGE_OPTIONAL_HEADER 的定義轉換過來會得到一個 CS1663 錯誤:
以下內容為程式程式碼:

error CS1663: Fixed sized buffer type must be one of the following: bool, byte, short, int, long, char, sbyte, ushort, uint, ulong, float or double