Search code examples
c#arraysstringbytemarshalling

Marshalling String (But Not Byte Array) Inside Struct Causes AccessViolationException


I have the following structure that I'm trying to marshal in C#:

  • Length (4 bytes)
  • Version (4 bytes)
  • MachineID (16 bytes)

The problem I'm having is getting the MachineID as a string in C#. In the structure, I specify [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Size = 24)] at the top and [MarshalAs(UnmanagedType.LPStr, SizeConst = 16)] for the MachineID. But when I do this, I get the System.AccessViolationException is thrown.

If I change the MachineID to be byte[] and specify [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] it works fine.

Below is code that works with a byte[] but not a string. The MachineID in ASCII form is ABCDEFGHIJKLMNO.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

public class Test
{
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Size = 24)]
    struct Valid
    {
        public uint Length;
        public uint Version;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
        public byte[] MachineId;
    }

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi, Size = 24)]
    struct Invalid
    {
        public uint Length;
        public uint Version;
        [MarshalAs(UnmanagedType.LPStr, SizeConst = 16)] 
        public string MachineId;
    }

    public static void Main()
    {
        var bytes = new byte[]
        {
            0x58, 0, 0, 0,
            0, 0, 0, 0,
            0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0
        };

        var pinnedBuffer = GCHandle.Alloc(bytes, GCHandleType.Pinned);
        var ptr = pinnedBuffer.AddrOfPinnedObject();

        // Works!
        Debug.Assert(bytes.Length == Marshal.SizeOf<Valid>());

        var trackerData1 = Marshal.PtrToStructure<Valid>(ptr);

        Console.WriteLine("Length: {0}", trackerData1.Length);
        Console.WriteLine("Version: {0}", trackerData1.Version);
        Console.WriteLine("MachineId: {0}", Encoding.ASCII.GetString(trackerData1.MachineId));

        // Doesnt work!
        Debug.Assert(bytes.Length == Marshal.SizeOf<Invalid>());

        // Throws System.AccessViolationException
        var trackerData2 = Marshal.PtrToStructure<Invalid>(ptr);

        Console.WriteLine("Length: {0}", trackerData2.Length);
        Console.WriteLine("Version: {0}", trackerData2.Version);
        Console.WriteLine("MachineId: {0}", trackerData2.MachineId);

        pinnedBuffer.Free();

        Console.In.ReadLine();
    }
}

If you run this code in IDEOne, you'll see that it throws the System.AccessViolationException when it tries to marshal the string and it's not just my computer.

What I'm curious is why does it work for a byte array but not a string?


Solution

  • You need to marshal it as ByValTStr for this to work:

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 16)]
    public string MachineId;
    

    Here's the example.