Trouble with Marshal.PtrToStructure() and char arrays in structure DEVMODE
I'm having an issue with using Marshal.PtrToStructure() to extract data out of a pointer to a structure of type DEVMODE. Here is a link to an MSDN entry on the DEVMODE structure.
My C# implementation for this structure is as follows:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DEVMODE
{
public const int CCHDEVICENAME = 32;
public const int CCHFORMNAME = 32;
public unsafe fixed char dmDeviceName [CCHDEVICENAME];
public Int16 dmSpecVersion;
public Int16 dmDriverVersion;
public Int16 dmSize;
public Int16 dmDriverExtra;
public DM_FIELD_TYPE dmFields;
public Int16 dmOrientation;
public Int16 dmPaperSize;
public Int16 dmPaperLength;
public Int16 dmPaperWidth;
public Int16 dmScale;
public Int16 dmCopies;
public Int16 dmDefaultSource;
public Int16 dmPrintQuality;
public POINTL dmPosition;
public Int32 dmDisplayOrientation;
public Int32 dmDisplayFixedOutput;
public short dmColor;
public short dmDuplex;
public short dmYResolution;
public short dmTTOption;
public short dmCollate;
public unsafe fixed char dmFormName [CCHFORMNAME];
public Int16 dmLogPixels;
public Int32 dmBitsPerPel;
public Int32 dmPelsWidth;
public Int32 dmPelsHeight;
public Int32 dmDisplayFlags;
public Int32 dmNup;
public I开发者_如何学Gont32 dmDisplayFrequency;
public Int32 dmICMMethod;
public Int32 dmICMIntent;
public Int32 dmMediaType;
public Int32 dmDitherType;
public Int32 dmReserved1;
public Int32 dmReserved2;
public Int32 dmPanningWidth;
public Int32 dmPanningHeight;
public DEVMODE(byte[] data)
{
unsafe
{
fixed (byte* packet = &data[0])
{
this = *(DEVMODE*)packet;
}
}
}
}
[Flags()]
public enum DM_FIELD_TYPE : int
{
/* field selection bits */
DM_ORIENTATION = 0x00000001,
DM_PAPERSIZE = 0x00000002,
DM_PAPERLENGTH = 0x00000004,
DM_PAPERWIDTH = 0x00000008,
DM_SCALE = 0x00000010,
DM_POSITION = 0x00000020,
DM_NUP = 0x00000040,
DM_DISPLAYORIENTATION = 0x00000080,
DM_COPIES = 0x00000100,
DM_DEFAULTSOURCE = 0x00000200,
DM_PRINTQUALITY = 0x00000400,
DM_COLOR = 0x00000800,
DM_DUPLEX = 0x00001000,
DM_YRESOLUTION = 0x00002000,
DM_TTOPTION = 0x00004000,
DM_COLLATE = 0x00008000,
DM_FORMNAME = 0x00010000,
DM_LOGPIXELS = 0x00020000,
DM_BITSPERPEL = 0x00040000,
DM_PELSWIDTH = 0x00080000,
DM_PELSHEIGHT = 0x00100000,
DM_DISPLAYFLAGS = 0x00200000,
DM_DISPLAYFREQUENCY = 0x00400000,
DM_ICMMETHOD = 0x00800000,
DM_ICMINTENT = 0x01000000,
DM_MEDIATYPE = 0x02000000,
DM_DITHERTYPE = 0x04000000,
DM_PANNINGWIDTH = 0x08000000,
DM_PANNINGHEIGHT = 0x10000000,
DM_DISPLAYFIXEDOUTPUT = 0x20000000
}
public struct POINTL
{
public Int32 x;
public Int32 y;
}
In this structure there are 2 character arrays "dmDeviceName" and "dmFormName". Both are 32 characters long. The problem is, when I attempt to marshal out the structure DEVMODE from a pointer, those character arrays are not being filled properly. For example, dmDeviceName will only have the first character of the actual device name. The rest of the array entries will be just '\0'. The line of code I'm that is doing the marshaling is as follows:
DEVMODE devMode = (DEVMODE)Marshal.PtrToStructure(aData[i].NotifyData.Data.pBuf, typeof(DEVMODE));
"aData[i].NotifyData.Data.pBuf" is a valid pointer to a structure of type DEVMODE. I know this for 2 reasons.
This structure is a subset of information returned from a printer driver call named FindNextPrinterChangeNotification(). I use this to capture print job information, and when I capture a print job and use the above code to marshal out the DEVMODE object, the "dmCopies" field is always exactly correct with the number of copies that were printed in that job. So its a bit strange how the 12th member in a structure is getting marshaled correctly when some of the ones before it appear to not be.
I used Marshal.ReadByte() to forcibly read the first 100 bytes that aData[i].NotifyData.Data.pBuf was pointing to, and sure enough, the first collection of bytes were the full exact name of the printer device. So I know the information is there.
For whatever reason, when I use Marshal.PtrToStructure() it doesn't seem to be able to correctly fill the character arrays. I'm pretty sure most of the other variables are correct, but I have my doubts because of the array issue. Does anyone know whats going on here.
--EDIT -- As requested, here is the code that populates the aData[] array:
private PRINTER_NOTIFY_INFO_DATA[] MarshalOutPrinterNotifyInfoDataArray(IntPtr ppPrinterNotifyInfo)
{
//Dereferencing ppPrinterNotifyInfo and setting NotifyInfoStruct to it.
PRINTER_NOTIFY_INFO NotifyInfoStruct = (PRINTER_NOTIFY_INFO)Marshal.PtrToStructure(ppPrinterNotifyInfo, typeof(PRINTER_NOTIFY_INFO));
//Creating a point to point to the PRINTER_NOTIFY_INFO and then moving it to the end of the structure where the
//aData[] member would begin.
int paData = (int)ppPrinterNotifyInfo + Marshal.SizeOf(typeof(PRINTER_NOTIFY_INFO));
//Creating an array to hold all the elements of our aData array.
PRINTER_NOTIFY_INFO_DATA[] data = new PRINTER_NOTIFY_INFO_DATA[NotifyInfoStruct.Count];
//looping through all the PRINTER_NOTIFY_INFO_DATA elments in the aData member and adding them to our local array.
for (uint i = 0; i < NotifyInfoStruct.Count; i++)
{
//extracting out a single PRINTER_NOTIFY_INFO_DATA item and storing it in our local array
data[i] = (PRINTER_NOTIFY_INFO_DATA)Marshal.PtrToStructure((IntPtr)paData, typeof(PRINTER_NOTIFY_INFO_DATA));
//moving our pointer to the next PRINTER_NOTIFY_INFO_DATA item
paData += Marshal.SizeOf(typeof(PRINTER_NOTIFY_INFO_DATA));
}
return data;
}
private void SomeRoutine()
{
//////////////////
/// some code here
//////////////////
//retrieving information about the most recent change notification for a change notification object associated with the printer
FindNextPrinterChangeNotification(m_ManualResetEvent.SafeWaitHandle.DangerousGetHandle(), out pdwChangeFlags, null, out ppPrinterNotifyInfo);
//Need to extract our PRINTER_NOTIFY_INFO_DATA array out of the PRINTER_NOTIFY_INFO structure
PRINTER_NOTIFY_INFO_DATA[] aData = MarshalOutPrinterNotifyInfoDataArray(ppPrinterNotifyInfo);
//////////////////
/// some code here
//////////////////
}
Use a string:
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCHDEVICENAME)]
public string dmDeviceName;
See: http://pinvoke.net/default.aspx/Structures/DEVMODE.html
EDIT: I didn't notice when posting my quick response since I was focusing on the strings (and there is one at the very beginning of the struct, meaning that the rest of the struct elements can't affect things), but as others have pointed out you have a big problem with the unions in the DEVMODE structure. Elements in a union do not lie sequentially but rather occupy the same space in memory: only one of the union elements may be used at a time. For example:
union {
DWORD dmDisplayFlags;
DWORD dmNup;
};
means that dmDisplayFlags and dmNup are essentially different names for the same block of memory. I.e. dmDisplayFlags and dmNup are both stored at an offset of 116 bytes from the beginning of the structure. Changing dmNup causes the value of dmDisplayFlags to also change, and vice versa. Using C# code like:
public Int32 dmDisplayFlags;
public Int32 dmNup;
means that they get stored sequentially, i.e. at offsets 116 and 120. That upsets the layout of the entire structure. To fix that, you need to use an explicit layout and manually define the field offsets. Look at the link I previously gave at pinvoke.net for an example of how to do that on this particular structure. Notice how dmDisplayFlags and dmNup have the same field offset. Since C# doesn't natively support unions, this is a somewhat-clumsy way to handle unions for those special interop scenarios like this one that require it.
I'd suggest fixing your union issues, and then use strings with ByValTStr as originally suggested (in summary, use what is on pinvoke.net). See if it gets you better results.
Others suggest that it is a Unicode issue, but I don't think that's what it is. The documentation says FindNextPrinterChangeNotification is not available in Unicode. If you examined the structure at byte-level and say it's not Unicode - I definitely believe you.
From the docs:
"ByValTStr: Used for in-line, fixed-length character arrays that appear within a structure. The character type used with ByValTStr is determined by the System.Runtime.InteropServices.CharSet argument of the System.Runtime.InteropServices.StructLayoutAttribute applied to the containing structure. Always use the MarshalAsAttribute.SizeConst field to indicate the size of the array.
.NET Framework ByValTStr types behave like C-style, fixed-size strings inside a structure (for example, char s[5]). The behavior in managed code differs from the Microsoft Visual Basic 6.0 behavior, which is not null terminated (for example, MyString As String * 5)."
Seems pretty clear to me that this should be the generally-accepted "correct" way of doing this.
精彩评论