C#,从二进制文件中读取结构体

7

我希望从二进制文件中读取数据结构。 在C++中,我会这样做:

stream.read((char*)&someStruct, sizeof(someStruct));

在C#中有类似的方法吗?BinaryReader只适用于内置类型。在.NET 4中,有一个MemoryMappedViewAccessor。它提供了像Read<T>这样的方法,看起来是我想要的,但我必须手动跟踪我想读取文件中的哪个位置。
有更好的方法吗?


2
这取决于它们是如何编写的。 - Andrey
http://www.codeproject.com/KB/cs/objserial.aspx - Ed S.
http://code.google.com/p/protobuf-net/ - digEmAll
5个回答

15
public static class StreamExtensions
{
    public static T ReadStruct<T>(this Stream stream) where T : struct
    {
        var sz = Marshal.SizeOf(typeof(T));
        var buffer = new byte[sz];
        stream.Read(buffer, 0, sz);
        var pinnedBuffer = GCHandle.Alloc(buffer, GCHandleType.Pinned);
        var structure = (T) Marshal.PtrToStructure(
            pinnedBuffer.AddrOfPinnedObject(), typeof(T));
        pinnedBuffer.Free();
        return structure;
    }
}

你需要确保你的结构体声明了[StructLayout]和可能的[FieldOffset]注释,以匹配文件中的二进制布局。
编辑:
用法:
SomeStruct s = stream.ReadStruct<SomeStruct>();

@jesperll:这是一个非常糟糕的想法,特别是如果数据结构不是扁平化的。如果结构中有任何指针,则该引用的结构/类将不会被写入输出。更糟糕的是,当重新读取时,它将指向无效的内存空间。 - casperOne
如果您在结构体中有诸如数组之类的东西,那么它们不是值类型,这样会导致问题。 - Jesper Larsen-Ledet
3
但如果你要用它来解析一个包含大量简单类型头部块的文件格式,那么这是完全可行的。 - Jesper Larsen-Ledet
1
+1 这是完全可行的,我正在使用基本相同的代码从ASF文件中读取二进制结构 - @casperOne 我认为问题并没有要求复杂对象的序列化/反序列化机制。 - BrokenGlass
非常酷!Marshal.PtrToStructure() 在枚举类型上抛出异常(可能是因为无法在枚举上使用[StructLayout]?)。对于枚举,您可以使用typeof(T).GetEnumUnderlyingType(),它可以正常工作。 - Carl Walsh

3
这里是Jesper代码的稍微修改版本:

public static T? ReadStructure<T>(this Stream stream) where T : struct
{
    if (stream == null)
        return null;

    int size = Marshal.SizeOf(typeof(T));
    byte[] bytes = new byte[size];
    if (stream.Read(bytes, 0, size) != size) // can't build this structure!
        return null;

    GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
    try
    {
        return (T)Marshal.PtrToStructure(handle.AddrOfPinnedObject(), typeof(T));
    }
    finally
    {
        handle.Free();
    }
}

它成功地处理了EOF情况,因为它返回可空类型。

2
在C#中也可以实现类似的功能,但是你需要在结构体上应用大量的属性来控制它在内存中的布局。默认情况下,JIT编译器控制结构体成员在内存中的布局,这通常意味着它们被重新排列和填充,以考虑速度和内存使用效率最高的布局。
最简单的方法通常是使用BinaryReader读取文件中结构体的各个成员,并将值放入一个类的属性中,即手动将数据反序列化为类实例。
通常情况下,读取文件是此操作的瓶颈,因此读取各个成员的小开销不会明显影响性能。

听起来很合理。性能并不是主要问题,我只是觉得有点不方便。 - B_old
再仔细考虑一下,我不想使用循环来读取某个数组。 - B_old
@B_old:写几行代码逐个读取值要容易得多,而不是为了确保结构体的所有成员在内存中的布局与文件的排列完全一致而正确设置属性。无论你选择哪种解决方案,都无法避免使用循环。 - Guffa

1

为了详细说明Guffa和jesperll的答案,这里提供一个示例,使用基本相同的ReadStruct方法(只是不作为扩展方法)来读取ASF(WMV / WMA)文件的文件头。

MemoryStream ms = new MemoryStream(headerData);
AsfFileHeader asfFileHeader = ReadStruct<AsfFileHeader>(ms);


[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Pack = 1)]
internal struct AsfFileHeader
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] 
    public byte[] object_id;
    public UInt64 object_size;
    public UInt32 header_object_count;
    public byte r1;
    public byte r2;
}

1

在哪些情况下它不可移植?我正在使用C++代码在x86和x64上读取相同的数据,似乎工作正常。 - B_old
如果您在一个平台上编写数据(例如x86),然后在另一个平台(64)上读取,则可能会遇到问题。 - Lavir the Whiolet
这正是我不理解的地方,因为我正在做这件事。你可能有一个链接可以更详细地解释这个问题吗? - B_old
不好意思,我没有详细解释的链接。但是我知道,内存布局在不同平台上甚至在使用不同编译器参数时也会有所不同。没有确切的标准,“内存中的字段必须按照它们在源代码中出现的顺序出现”或“长整型在所有平台上都表示为32位”或“字段在32位数据包中对齐”,也不能有这样的标准。在x86和x64中成功使用C++代码只意味着你很幸运。尝试使用编译器选项或尝试为ARM编译。 - Lavir the Whiolet
它更倾向于编译器实现,而不是x86与x64之间的区别。 - poy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接