Search code examples
c#interoppinvoke

Alignment error with C# StructLayout.Explicit in DEVMODE structure


I'm trying to use the EnumDisplaySettings which uses the DEVMODE structure as a result structure. The DEVMODE structure uses a few unions internally which makes it a little more complicated to use in C#. The unions are used for either numerating displays or printers. FieldOffsets in an StructLayout.Explicit should do the trick to use unions.

Below is the structure which was copied from pinvoke.net. Obviously some others also had problems with this structure and resolved the unions by simply making them StructLayout.Sequential and created two structs, one for displays and one for printers.

The exception that is thrown is on field offset 70, which tells, that the field is either not aligned or overlapped by another field. And this is what I don't understand, of course fields can overlap with Explicit layouting used and also the field before at field offset 68 is short, which cannot overlap into fieldoffset 70. This is how the structure works as defined by Microsoft. When moving the fieldoffset from 70 to 72 it works.

So I'm really not into fixing my problem in general, but I'm interested in the background of what's happening here.

[StructLayout(LayoutKind.Explicit, CharSet = CharSet.Ansi)]
public struct DEVMODE3
{
    public const int CCHDEVICENAME = 32;
    public const int CCHFORMNAME = 32;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
    [System.Runtime.InteropServices.FieldOffset(0)]
    public string dmDeviceName;
    [System.Runtime.InteropServices.FieldOffset(32)]
    public Int16 dmSpecVersion;
    [System.Runtime.InteropServices.FieldOffset(34)]
    public Int16 dmDriverVersion;
    [System.Runtime.InteropServices.FieldOffset(36)]
    public Int16 dmSize;
    [System.Runtime.InteropServices.FieldOffset(38)]
    public Int16 dmDriverExtra;
    [System.Runtime.InteropServices.FieldOffset(40)]
    public uint dmFields;

    [System.Runtime.InteropServices.FieldOffset(44)]
    Int16 dmOrientation;
    [System.Runtime.InteropServices.FieldOffset(46)]
    Int16 dmPaperSize;
    [System.Runtime.InteropServices.FieldOffset(48)]
    Int16 dmPaperLength;
    [System.Runtime.InteropServices.FieldOffset(50)]
    Int16 dmPaperWidth;
    [System.Runtime.InteropServices.FieldOffset(52)]
    Int16 dmScale;
    [System.Runtime.InteropServices.FieldOffset(54)]
    Int16 dmCopies;
    [System.Runtime.InteropServices.FieldOffset(56)]
    Int16 dmDefaultSource;
    [System.Runtime.InteropServices.FieldOffset(58)]
    Int16 dmPrintQuality;

    [System.Runtime.InteropServices.FieldOffset(44)]
    public POINTL dmPosition;
    [System.Runtime.InteropServices.FieldOffset(52)]
    public Int32 dmDisplayOrientation;
    [System.Runtime.InteropServices.FieldOffset(56)]
    public Int32 dmDisplayFixedOutput;

    [System.Runtime.InteropServices.FieldOffset(60)]
    public short dmColor; // See note below!
    [System.Runtime.InteropServices.FieldOffset(62)]
    public short dmDuplex; // See note below!
    [System.Runtime.InteropServices.FieldOffset(64)]
    public short dmYResolution;
    [System.Runtime.InteropServices.FieldOffset(66)]
    public short dmTTOption;
    [System.Runtime.InteropServices.FieldOffset(68)]
    public short dmCollate; // See note below!
    [System.Runtime.InteropServices.FieldOffset(70)]
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHFORMNAME)]
    public string dmFormName;
    [System.Runtime.InteropServices.FieldOffset(102)]
    public Int16 dmLogPixels;
    [System.Runtime.InteropServices.FieldOffset(104)]
    public Int32 dmBitsPerPel;
    [System.Runtime.InteropServices.FieldOffset(108)]
    public Int32 dmPelsWidth;
    [System.Runtime.InteropServices.FieldOffset(112)]
    public Int32 dmPelsHeight;
    [System.Runtime.InteropServices.FieldOffset(116)]
    public Int32 dmDisplayFlags;
    [System.Runtime.InteropServices.FieldOffset(116)]
    public Int32 dmNup;
    [System.Runtime.InteropServices.FieldOffset(120)]
    public Int32 dmDisplayFrequency;
}

Solution

  • 70 is the correct offset. It is 102 for CharSet = CharSet.Auto, the one you should always favor.

    The problem is that the code uses [FieldOffset] unnecessarily. That doesn't just set the offset of the field in the marshaled struct, it also hammers down the offset of the field in the managed struct.

    And that's a big problem, 70 is not valid since that mis-aligns the string in memory. The .NET memory model demands that in 32-bit mode, reference type references need to be aligned to an address that is a multiple of 4. The garbage collector really hates misaligned fields, object references need to be able to be atomically updated and misaligned references can't have that guarantee. They may straddle the L1 cache line which requires two memory bus cycles to set the value. Causes tearing, seeing only part of an update, a problem that is impossible to debug.

    Delete all the [FieldOffset] attributes or copy/paste from the Reference Source. Yet another advantage is that you can feel good about it still working correctly if your program runs in 64-bit mode.