stringvb.netvisual-studio-2022marshalling

Issue assigning string struct members by Marshal.PtrToStructure from a long string


I need to efficiently parse a fixed size, delimited message in VisualBasic WinForms application (.NET framework 4.6.2, VS20222). For a test, I built a small console app(.NET 8). The structure:

<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
Public Structure MyStruct
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=30)> Public Field1 As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=1)> Public F1 As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=10)> Public Field2 As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=1)> Public F2 As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=18)> Public Field3 As String
        <MarshalAs(UnmanagedType.ByValTStr, SizeConst:=1)> Public F3 As String
    End Structure

The test code:

Dim defaultValue = "123456789012345678901234567890;1234567890;123456789012345678;"
Dim myInstance As New MyStruct()
Dim ptr As IntPtr = Marshal.AllocHGlobal(Marshal.SizeOf(GetType(MyStruct)))
' Copy the long string data directly into the memory block
Marshal.Copy(System.Text.Encoding.ASCII.GetBytes(defaultValue), 0, ptr, Marshal.SizeOf(GetType(MyStruct)))
myInstance = Marshal.PtrToStructure(Of MyStruct)(ptr)
Marshal.FreeHGlobal(ptr)

In this test app, i get it perfectly working - semicolons are assigned to the Fx fields, valuable data - in Fieldx, e.g. Field2 contains "1234567890". However, if I simply copy declaration and code to my .Net 4.6.2 Application, i get all Fields missing the last character (Field2 has "123456789") and the separator fields are empty strings. Here are the two debugger outputs of two identical pieces of code:

enter image description here

Can somebody help me understand what is going wrong and how to solve that? Thanks in advance!

EDIT I made a reverse test, hoping that can give a hint. In the "broken" app, I assigned by hand all fields and then used StructureToPointer - to see how they are actually laid out in memory. Apparently, the fields are perfectly aligned, just last symbols are somehow overwritten with 0x00 (i assume during copying, since the struct still displays correct values), see the screenshot with memory mapping of ptr (on top) showing these zeros and debugger view of the struct itself showing the correct values. Stranger and stranger...

enter image description here


Solution

  • Marshaling is governed by the specified UnmanagedType.ByValTStr argument. From the documentation:

    Used for in-line, fixed-length character arrays that appear within a structure. ByValTStr types behave like C-style, fixed-size strings inside a structure (for example, char s[5]). The character type used with ByValTStr is determined by the CharSet argument of the StructLayoutAttribute attribute applied to the containing structure. Always use the SizeConst field to indicate the size of the array.

    The key phrase is "ByValTStr types behave like C-style, fixed-size strings". C-style strings have a terminating null character (see: What are null-terminated strings?).

    The string fields you defining are not null terminated. The fact that the code works in .Net 8 is due to bug in the .Net's implementation while marshaling from unmanaged memory to a managed String. When you attempted the "reverse test" (managed to unmanaged) in .Net 8, the marshaler replaced the last character with a null character; this is 0x00 you seen written to memory.

    Congratulations you unearthed a bug.

    The .Net Framework implementation is correct in reading only first (SizeConst - 1) characters from memory; hence the trunctated fields under .NetFramework 4.6.

    That said, your field data is an array of characters. So why not marshal it as such?

    <StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Ansi)>
    Public Class MyDataLayout
    
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=30)> Private _Field1 As Char()
        Private _F1 As Char
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=10)> Private _Field2 As Char()
        Private _F2 As Char
        <MarshalAs(UnmanagedType.ByValArray, SizeConst:=18)> Private _Field3 As Char()
        Private _F3 As Char
    
        Public ReadOnly Property Field1 As String
            Get
                Return New String(_Field1)
            End Get
        End Property
    
        Public ReadOnly Property Field2 As String
            Get
                Return New String(_Field2)
            End Get
        End Property
    
        Public ReadOnly Property Field3 As String
            Get
                Return New String(_Field3)
            End Get
        End Property
    
    End Class