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

Posted

技术标签:

【中文标题】Marshal.Copy 不从字节数组复制到从 IntPtr 中提供的地址开始的结构中【英文标题】:Marshal.Copy not copying from bytes array into structure starting at the address provided in an IntPtr 【发布时间】:2018-04-20 05:16:21 【问题描述】:

(这在某种意义上是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)

确实根据即时窗口读取具有正确值的正确字节数:

?abstreamdata
Length=8
    (0): 1
    (1): 0
    (2): 2
    (3): 0
    (4): 3
    (5): 0
    (6): 0
    (7): 0

我不能在这里使用Marshal.StructureToPtr,因为字节数组不是结构。然而,Marshal 类也有一个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 - 类型:System.Byte() 要从中复制的一维数组。

startIndex - 类型:System.Int32 应开始复制的源数组中从零开始的索引。

目标 - 类型:System.IntPtr 要复制到的内存指针。

长度 - 类型:System.Int32 要复制的数组元素的数量。

看起来我已经设置了正确的一切,那么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

在程序中,我在即时窗口中检查Marshal.Copy 之前和之后的记录值。两次我都获得:

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

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

结论:获得了数据,但没有Marshal.Copy 已经在被调用者中。所以这不仅仅是一个“返回错误数据”的问题。

编辑 2

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

Marshal.ReadByte(oStructAddr) 确实返回字节数组的第一个字节,Marshal.ReadByte(oStructAddr + 1) 它的第二个字节,等等。

但是如何在 Out 参数中返回这些数据?

【问题讨论】:

写得很好的问题。当您将HeaderStruct 设为Class 而不是Structure 时,您能否检查它是否有效?结构可能驻留在堆栈上,我不确定堆栈上是否有可能有 GCHandle。 (只是临时检查,绝对不是永久解决方案)。 @NicoSchertler,谢谢。这在尝试使用GCHandle.Alloc 获取oStruct 的句柄时已经失败(异常'对象不包含原始数据')。它在将GCHandleType 更改为Normal(不再固定它!)时执行,但是我将无法使用oGCHandle.AddrOfPinnedObject。使用oStructAddr = CType(oGCHandle, IntPtr) 代替工作,但随后SizeOf 抱怨,该对象不能被封送为非托管结构。 @NicoSchertler :固定结构应该可以工作,我自己已经做了几次,没有问题。 -- Herb:基于错误我设法找到了这个:***.com/a/15549992 - 显然,当你的结构中有一个非原始变量(即指向一个类的变量)时,它会被抛出。 @VisualVincent,结构如OP所示:它包含两个short和一个整数,实际填充为1、2、3(在文件中表示为0x01 00 02 00 03 00 00 00)。根本没有对对象的引用(除了一切)。我用一些进一步的发现更新了 OP。显然,Copy 不是按照文档复制数据,而是一个指针。 (我可以使用 ReadByte 从该位置检索副本,但不知道如何填充 out 参数。) "Copy 不是按照文档复制数据,而是指针"是什么意思? Marshal.Copy() 将指定范围的字节复制到指定的目标内存地址。但是,您需要使用 GCHandleType.Pinned 创建 GCHandle 并使用 AddrOfPinnedObject 才能使其正常工作。 【参考方案1】:

我不能确定为什么复制到固定地址的更新字节没有反映在原始结构中,但我怀疑互操作编组器会复制。见: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 似乎认为上面显示的技术不能实现为读取/写入流的通用方法。所以这里是一个复制/粘贴的例子。

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 的输出:

保存的结构:MajorVersion = 4,MinorVersion = 2,Count = 5

检索到的结构:MajorVersion = 4,MinorVersion = 2,Count = 5

【讨论】:

如(现已更新)问题中所述,涉及许多不同的结构。您的方法将导致创建一个类来单独处理每种结构类型,因此我可以完全省略声明结构并从一开始就创建类。 @herb,此答案演示了回答您问题的技术。示例使用硬编码类型的事实确实排除了将其编写为泛型的可能性。有关通用示例,请参阅更新。【参考方案2】:

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

工作。

【讨论】:

以上是关于Marshal.Copy 不从字节数组复制到从 IntPtr 中提供的地址开始的结构中的主要内容,如果未能解决你的问题,请参考以下文章

将非托管数据复制到托管数组中

Unity c#intptr到没有大小信息的字节数组[重复]

c#中Marshal.Copy()方法的使用

如何将字节数组中的图像发送到从android到具有Flask的python的url [重复]

C# AcessExceptionViolation 与 Marshal.Copy

使用 user32 SendMessage 调用 Marshal.Copy 时的 AccessViolation