Marshal.Copy 不从字节数组复制到从 IntPtr 中提供的地址开始的结构

Marshal.Copy not copying from bytes array into structure starting at the address provided in an IntPtr

(从某种意义上说,这是 Extracting structs in the middle of a file into my structures other than accessing the file byte for byte and compose their value 的后续问题。)

我有一个在其流中包含结构记录的文件:

[Start]...[StructureRecord]...[End]

包含的结构适合此布局,其中存在一个变量:

Public Structure HeaderStruct
    Public MajorVersion As Short
    Public MinorVersion As Short
    Public Count As Integer         
End Structure

private grHeaderStruct As HeaderStruct

正是在这个变量中,我想要驻留在文件中的结构的副本。

所需的命名空间:

'File, FileMode, FileAccess, FileShare:
Imports System.IO

'GCHandle, GCHandleType:
Imports System.Runtime.InteropServices

'SizeOf, Copy:
Imports System.Runtime.InteropServices.Marshal

我的 FileStream 名为 oFS

Using oFS = File.Open(sFileName, FileMode.Open, FileAccess.Read)
    ...

假设 oFS 现在位于 [StructureRecord] 的开头。

因此有 8 个字节 (HeaderStruct.Length) 需要从文件中读取,并将它们复制到该结构的记录实例中。为此,我包装了逻辑以从文件中读取所需的字节数并将它们传输到我的结构记录到一个通用方法 ExtractStructure 中。目标在调用例程之前实例化。

    grHeaderStruct = New HeaderStruct
    ExtractStructure(Of HeaderStruct)(oFS, grHeaderStruct)
    ...
End Using

(在建议在专用方法之外仅读取这 8 个字节的技术之前,您可能应该知道,整个文件由相互依赖的结构组成。Count 字段表示,接下来是 3 个子结构,但它们本身包含 Count 字段等。我认为获取它们的例程并不是一个太糟糕的主意。)

然而,这是导致我现在头疼的例程:

'Expects to find a structure of type T at the actual position of the 
'specified filestream oFS. Reads this structure into a byte array and copies
'it to the structure variable specified in oStruct.
Public Shared Sub ExtractStructure(Of T As Structure) _
    (oFS As FileStream, ByRef oStruct As T)

    Dim oGCHandle As GCHandle
    Dim oStructAddr As IntPtr
    Dim iStructLen As Integer
    Dim abStreamData As Byte()

    'Obtain a handle to the structure, pinning it so that the garbage
    'collector does not move the object. This allows the address of the 
    'pinned structure to be taken. Requires the use of Free when done.
    oGCHandle = GCHandle.Alloc(oStruct, GCHandleType.Pinned)

    Try
        'Retrieve the address of the pinned structure, and its size in bytes.
        oStructAddr = oGCHandle.AddrOfPinnedObject
        iStructLen = SizeOf(oStruct)

        'From the file's current position, obtain the number of bytes 
        'required to fill the structure.
        ReDim abStreamData(0 To iStructLen - 1)
        oFS.Read(abStreamData, 0, iStructLen)

        'Now both the source data is available (abStreamData) as well as an 
        'address to which to copy it (oStructAddr). Marshal.Copy will do the
        'copying.
        Marshal.Copy(abStreamData, 0, oStructAddr, iStructLen)
    Finally
        'Release the obtained GCHandle.
        oGCHandle.Free()
    End Try
End Sub

说明

oFS.Read(abStreamData, 0, iStructLen)

确实按照立即数 window:

读取具有正确值的正确字节数
?abstreamdata
{Length=8}
    (0): 1
    (1): 0
    (2): 2
    (3): 0
    (4): 3
    (5): 0
    (6): 0
    (7): 0

我不能在这里使用 Marshal.StructureToPtr,因为字节数组不是结构。但是,Marshal class 也有一个 Copy 方法。

只是我在这里显然漏掉了一点,因为

Marshal.Copy(abStreamData, 0, oStructAddr, iStructLen)

执行预期的复制(但它也不会抛出异常):

?ostruct
    Count: 0
    MajorVersion: 0
    MinorVersion: 0

我怎么没看懂,为什么这个副本打不开?

MS 文档没有为 Marshal.Copy 提供太多信息:https://msdn.microsoft.com/en-us/library/ms146625(v=vs.110).aspx:

source - Type: System.Byte() The one-dimensional array to copy from.

startIndex - Type: System.Int32 The zero-based index in the source array where copying should start.

destination - Type: System.IntPtr The memory pointer to copy to.

length - Type: System.Int32 The number of array elements to copy.

看起来好像我已经设置好了所有东西,所以Marshal.Copy有没有可能没有复制到结构,而是复制到其他地方?

oStructAddr 确实看起来像一个地址 (&H02EB7B64),但它在哪里?

编辑

接收结果的参数实例化和例程调用之间

    grHeaderStruct = New HeaderStruct
    ExtractStructure(Of HeaderStruct)(oFS, grHeaderStruct)

我用一些测试数据填充了实例化的记录,看看它是否真的正确通过了:

#Region "Test"
    With grMultifileDesc
        .MajorVersion = 7
        .MinorVersion = 8
        .Count = 9
    End With
#End Region

在程序中,我在立即数window中检查了Marshal.Copy前后记录的值。两次我都获得:

?ostruct
{MyProject.MyClass.HeaderStruct}
    Count: 9
    MajorVersion: 7
    MinorVersion: 8

(记录字段的重新排列在调用者和被调用者中是相同的,这本身就是一个问题,因为数据是从文件中读取的,并且会被错误地复制到结构中。不过,这不是问题的主题。)

结论:已获取数据,但Marshal.Copy已在被调用方中制作。所以这不仅仅是一个 "returns wrong data" 问题。

编辑 2

事实证明,Marshal.Copy 并不复制数据,如文档中所述,而是指向数据的指针。

Marshal.ReadByte(oStructAddr) return 字节数组的第一个字节,Marshal.ReadByte(oStructAddr + 1) 第二个字节,等等

但是我如何 return Out 参数中的这个数据?

我不能确定为什么复制到固定地址的更新字节没有反映在原始结构中,但我怀疑互操作编组器进行了复制。参见:Copying and Pinning.

我确实知道,如果您固定一个字节数组,使用 Marshal.Copy 所做的更改会传播回托管数组。话虽如此,这里有一个示例,您应该可以使用它来满足您的需要。

Dim lenBuffer As Int32 = Marshal.SizeOf(Of HeaderStruct)
Dim buffer As Byte() = New Byte(0 To lenBuffer - 1) {}
Dim gchBuffer As GCHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned)
Dim hs As New HeaderStruct With {.MajorVersion = 4, .MinorVersion = 2, .Count = 5}

' copy to pinned byte array 
Marshal.StructureToPtr(hs, gchBuffer.AddrOfPinnedObject, True)
' buffer = {4, 0, 2, 0, 5, 0, 0, 0} ' array can be written to file

' change the data - simulates array read from file
buffer(0) = 1 ' MajorVersion = 1
buffer(2) = 3 ' MinorVersion = 3
buffer(4) = 2 '.Count = 2

' read from pinned byte array
Dim hs2 As HeaderStruct = Marshal.PtrToStructure(Of HeaderStruct)(gchBuffer.AddrOfPinnedObject)
gchBuffer.Free()

编辑:根据评论,OP 似乎认为上面显示的技术不能作为 reading/writing 到流的通用方法来实现。所以这是一个 copy/paste 示例。

Sub Example()
    Using ms As New IO.MemoryStream
        Dim hs As New HeaderStruct With {.MajorVersion = 4, .MinorVersion = 2, .Count = 5}
        Debug.Print($"Saved structure:  {hs}")
        WriteStruct(ms, hs)  'write structure to stream

        ms.Position = 0 ' reposition stream to start 
        Dim hs2 As New HeaderStruct ' target for reading from stream
        ReadStruct(ms, hs2)
        Debug.Print($"Retrieved structure: {hs2}")
    End Using
End Sub

Sub WriteStruct(Of T As {Structure})(strm As IO.Stream, ByRef struct As T)
    Dim lenBuffer As Int32 = Marshal.SizeOf(Of T)
    Dim buffer As Byte() = New Byte(0 To lenBuffer - 1) {}
    Dim gchBuffer As GCHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned)
    Marshal.StructureToPtr(struct, gchBuffer.AddrOfPinnedObject, True)
    strm.Write(buffer, 0, lenBuffer)
    gchBuffer.Free()
End Sub

Sub ReadStruct(Of T As {Structure})(strm As IO.Stream, ByRef struct As T)
    Dim lenBuffer As Int32 = Marshal.SizeOf(Of T)
    Dim buffer As Byte() = New Byte(0 To lenBuffer - 1) {}
    strm.Read(buffer, 0, buffer.Length)
    Dim gchBuffer As GCHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned)
    struct = Marshal.PtrToStructure(Of T)(gchBuffer.AddrOfPinnedObject)
    gchBuffer.Free()
End Sub

Public Structure HeaderStruct
    Public MajorVersion As Short
    Public MinorVersion As Short
    Public Count As Integer
    Public Overrides Function ToString() As String
        Return $"MajorVersion = {MajorVersion}, MinorVersion = {MinorVersion}, Count = {Count}"
    End Function
End Structure

Example 的输出:

Saved structure: MajorVersion = 4, MinorVersion = 2, Count = 5

Retrieved structure: MajorVersion = 4, MinorVersion = 2, Count = 5

ExtractStructure方法需要这样重写:

'Expects to find a structure of type T at the actual position of the 
'specified filestream oFS. Reads this structure into a byte array and copies
'it to the structure variable specified in oStruct.
Public Shared Sub ExtractStructure(Of T As Structure) _
    (oFS As FileStream, ByRef oStruct As T)

    Dim iStructLen As Integer
    Dim abStreamData As Byte()
    Dim hStreamData As GCHandle
    Dim iStreamData As IntPtr

    'From the file's current position, read the number of bytes required to
    'fill the structure, into the byte array abStreamData.
    iStructLen = Marshal.SizeOf(oStruct)
    ReDim abStreamData(0 To iStructLen - 1)
    oFS.Read(abStreamData, 0, iStructLen)

    'Obtain a handle to the byte array, pinning it so that the garbage
    'collector does not move the object. This allows the address of the 
    'pinned structure to be taken. Requires the use of Free when done.
    hStreamData = GCHandle.Alloc(abStreamData, GCHandleType.Pinned)
    Try
        'Obtain the byte array's address.
        iStreamData = hStreamData.AddrOfPinnedObject()

        'Copy the byte array into the record.
        oStruct = Marshal.PtrToStructure(Of T)(iStreamData)
    Finally
        hStreamData.Free()
    End Try
End Sub

有效。