c#

位置:IT落伍者 >> c# >> 浏览文章

在 C# 中处理结构内的数组源代码分析


发布日期:2023年01月29日
 
在 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

typedef struct _IMAGE_OPTIONAL_HEADER {

WORDMagic;

//

DWORD NumberOfRvaAndSizes;

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER *PIMAGE_OPTIONAL_HEADER;

在 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 = ;

public ushort Magic;

//

public uint NumberOfRvaAndSizes;

public IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

}

在 C# 中这样定义结构中的数组是错误的会在编译时获得一个 CS 错误

以下为引用

error CS: 语法错误错误的数组声明符若要声明托管数组秩说明符应位于变量标识符之前

如果改用 C# 中引用类型的类似定义语法

以下内容为程序代码:

public struct IMAGE_OPTIONAL_HEADER

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

public ushort Magic;

//

public uint NumberOfRvaAndSizes;

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

}

则得到一个 CS 错误

以下为引用

error CS: IMAGE_OPTIONAL_HEADERDataDirectory : 结构中不能有实例字段初始值设定项

因为结构内是不能够有引用类型的初始化的这与 class 的初始化工作不同如此一来只能将数组的初始化放到构造函数中而且结构还不能有无参数的缺省构造函数真是麻烦呵呵

以下内容为程序代码:

public struct IMAGE_OPTIONAL_HEADER

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

public ushort Magic;

public uint NumberOfRvaAndSizes;

public IMAGE_DATA_DIRECTORY[] DataDirectory;

public IMAGE_OPTIONAL_HEADER(IntPtr ptr)

{

Magic = ;

NumberOfRvaAndSizes = ;

DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

}

}

这样一来看起来似乎能使了但如果使用 MarshalSizeOf(typeof(IMAGE_OPTIONAL_HEADER)) 看看就会发现其长度根本就跟 C/C++ 中定义的长度不同问题还是在于结构中数组虽然看起来此数组是定义在结构内但实际上在此结构中只有一个指向 IMAGE_DATA_DIRECTORY[] 数组类型的指针而已本应保存在 DataDirectory 未知的数组内容是在托管堆中

于是问题就变成如何将引用类型的数组放在一个值类型的结构中

解决的方法有很多如通过 StructLayout 显式指定结构的长度来限定内容

以下内容为程序代码:

[StructLayout(LayoutKindSequential Size=XXX)]

public struct IMAGE_OPTIONAL_HEADER

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

public ushort Magic;

public uint NumberOfRvaAndSizes;

public IMAGE_DATA_DIRECTORY DataDirectory;

}

注意这儿 StructLayout 中 Size 指定的是整个结构的长度因为 DataDirectory 已经是最后一个字段故而数组的后 个元素被保存在未命名的堆栈空间内使用的时候稍微麻烦一点需要一次性读取整个结构然后通过 unsafe 代码的指针操作来访问 DataDirectory 字段后面的其他数组元素

这种方法的优点是定义简单但使用时需要依赖 unsafe 的指针操作代码且受到数组字段必须是在最后的限制当然也可以通过 LayoutKindExplicit 显式指定每个字段的未知来模拟多个结构内嵌数组但这需要手工计算每个字段偏移比较麻烦

另外一种解决方法是通过 Marshal 的支持显式定义数组元素所占位置

以下内容为程序代码:

[StructLayout(LayoutKindSequential Pack=)]

public struct IMAGE_OPTIONAL_HEADER

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

public ushort Magic;

public uint NumberOfRvaAndSizes;

[MarshalAs(UnmanagedTypeByValArray SizeConst=IMAGE_NUMBEROF_DIRECTORY_ENTRIES)]

public IMAGE_DATA_DIRECTORY[] DataDirectory;

}

这种方法相对来说要优雅一些通过 Marshal 机制支持的属性来定义值数组语义使用起来与普通的数组区别不算太大上述数组定义被编译成 IL 定义

以下内容为程序代码:

field publicmarshal( fixed array []) valuetype IMAGE_DATA_DIRECTORY[] DataDirectory

虽然类型还是 valuetype IMAGE_DATA_DIRECTORY[]但因为 marshal( fixed array []) 的修饰此数组已经从引用语义改为值语义不过这样做还是会受到一些限制如不能多层嵌套使用时性能受到影响等等

除了上述两种在结构定义本身做文章的解决方法还可以从结构的操作上做文章

此类结构除了对结构内数组的访问外主要的操作类型就是从内存块或输入流中读取整个结构因此完全可以使用 CLR 提高的二进制序列化支持通过实现自定义序列化函数来完成数据的载入和保存

以下内容为程序代码:

[Serializable]

public struct IMAGE_OPTIONAL_HEADER : ISerializable

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

public ushort Magic;

public uint NumberOfRvaAndSizes;

public IMAGE_DATA_DIRECTORY[] DataDirectory;

public IMAGE_OPTIONAL_HEADER(IntPtr ptr)

{

Magic = ;

NumberOfRvaAndSizes = ;

DataDirectory = new IMAGE_DATA_DIRECTORY[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

}

[SecurityPermissionAttribute(SecurityActionDemandSerializationFormatter=true)]

public virtual void GetObjectData(SerializationInfo info StreamingContext context)

{

// 完成序列化操作

}

}

这种解决方法可以将结构的载入和存储与结构的内部表现完全分离开来虽然结构内部保存的只是数组引用但用户并不需关心但缺点是必须为每个结构都编写相应的序列化支持代码编写和维护都比较麻烦

与此思路类似的是我比较喜欢的一种解决方法通过一个公共工具基类以 Reflection 的方式统一处理

以下内容为程序代码:

public class IMAGE_OPTIONAL_HEADER : BinaryBlock

{

public const int IMAGE_NUMBEROF_DIRECTORY_ENTRIES = ;

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 = LogManagerGetLogger(typeof(BinaryBlock));

public BinaryBlock()

{

}

static public object LoadFromStream(BinaryReader reader Type objType)

{

if(objTypeEquals(typeof(char)))

{

return readerReadChar();

}

else if(objTypeEquals(typeof(byte)))

{

return readerReadByte();

}

//

else if(objTypeEquals(typeof(double)))

{

return readerReadDouble();

}

else if(objTypeIsArray)

{

// 处理数组的情况

}

else

{

foreach(FieldInfo field in Cla               

上一篇:通过C#中的解构器编写可靠高效的应用程序

下一篇:当C#结构成员是引用,会发生什么