Quiz: What’s wrong with the following code?


The following C# code has the goal of enabling managed code to call CreateDC, but it’s incorrect.  Calling all Interop aficionados… Can you see what’s wrong?


using System;


using System.Runtime.InteropServices;



internal class DeviceContext


{


  [DllImport(“gdi32.dll”, CharSet=CharSet.Auto)]


  internal static extern IntPtr CreateDC(


    string lpszDriver, string lpszDevice,


    string lpszOutput, ref DEVMODE lpInitData);


}


 


[StructLayout(LayoutKind.Sequential)]


internal class DEVMODE


{


  [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]


  public char [] dmDeviceName;


  public short dmSpecVersion;


  public short dmDriverVersion;


  public short dmSize;


  public short dmDriverExtra;


  public int dmFields;


  public DEVMODE_UNION u;


  public short dmColor;


  public short dmDuplex;


  public short dmYResolution;


  public short dmTTOption;


  public short dmCollate;


  [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]


  public byte [] dmFormName;


  public short dmLogPixels;


  public int dmBitsPerPel;


  public int dmPelsWidth;


  public int dmPelsHeight;


  public int dmDisplayFlagsOrdmNup;


  public int dmDisplayFrequency;


  public int dmICMMethod;


  public int dmICMIntent;


  public int dmMediaType;


  public int dmDitherType;


  public int dmReserved1;


  public int dmReserved2;


  public int dmPanningWidth;


  public int dmPanningHeight;


}


 


[StructLayout(LayoutKind.Explicit)]


internal struct DEVMODE_UNION


{


  [FieldOffset(0)]


  public short dmOrientation;


  [FieldOffset(2)]


  public short dmPaperSize;


  [FieldOffset(4)]


  public short dmPaperLength;


  [FieldOffset(6)]


  public short dmPaperWidth;


  [FieldOffset(8)]


  public short dmScale;


  [FieldOffset(10)]


  public short dmCopies;


  [FieldOffset(12)]


  public short dmDefaultSource;


  [FieldOffset(14)]


  public short dmPrintQuality;


 


  [FieldOffset(0)]


  public int dmPosition_x;


  [FieldOffset(4)]


  public int dmPosition_y;


  [FieldOffset(8)]


  public int dmDisplayOrientation;


  [FieldOffset(12)]


  public int dmDisplayFixedOutput;


}

Comments (36)

  1. Jim says:

    I’m by no means an Interop expert, but this quiz brought up a couple questions of my own…

    First:
    Why do you not specify the call as:
    internal static extern IntPtr CreateDC(
    string lpszDriver, [In] string lpszDevice,
    string lpszOutput, [In] ref DEVMODE lpInitData);

    Are the [In], [Out], [In, Out] references just used for documentation purposes, or do they do something? Perhaps you explain this in your book, and I just haven’t gotten that far yet.

    Anyway, I’m pointing my finger at how the DEVMODE structure is sent to CreateDC call.

  2. Adam Nathan says:

    These attributes can definitely have meaning. In this case, the by-value string parameters marshal as "in-only" anyway, so explicitly marking them with [In] has no effect. But you can use them to restrict marshaling in some cases. Marking a by-ref parameter with [In] can be a nice optimization to override the default [In, Out] behavior if you don’t care about the contents of the parameter after the call.

    But even when you use these attributes to specify a non-default behavior, they still might not have any effect! [In] and [Out] only matter when the marshaler has some interesting work to do (copying a parameter from a managed representation to a separate unmanaged representation, or vice versa). For blittable types, where the managed and unmanaged memory representations are identical, the marshaler may directly hand out a pointer to the original memory location to unmanaged code. In such cases, it doesn’t matter what attributes you use; if unmanaged code modifies that memory, you’ll see the change after the call!

    But the lack of attributes is not the source of any grief in this example. Keep looking – it sounds like you’re on the right track!

  3. The fact that DEVMODE is a class and you pass it as a ref parameter will cause a DEVMODE** to be passed to the API – one level of indirection too much.

    For anyone who didn’t catch that, I’d recommend watching Sonja’s latest MSDN TV episode at http://msdn.microsoft.com/msdntv/.

  4. Adam Nathan says:

    Yes, and a nice tie-in to MSDN TV! That’s one error in the code, but there’s still another one!

    To elaborate on this first mistake, we have two options to fix it and make it marshal as a DEVMODE*:
    1) Change the class to a struct
    2) Remove the "ref" to pass the class reference by-value
    Option 2 is better in this case because the documentation for CreateDC says, "The lpInitData parameter must be NULL if the device driver is to use the default initialization (if any) specified by the user." With option 2 you can pass null. With option 1, you can’t.

    Can anyone find the other mistake?

  5. I count two more. :-)

    1) DEVMODE should have CharSet.Auto.
    2) dmFormName should be a char[], not byte[]. That is according to Wingdi.h, the Platform SDK docs seem to be incorrect.

  6. Adam Nathan says:

    Yes! As-is, the dmDeviceName field would always get marshaled as an array of ANSI characters. By marking the structure with CharSet.Auto, it will get marshaled as Unicode characters on Unicode platforms (matching the corresponding CharSet.Auto marking on the CreateDC API).

    And yes, the MSDN documentation misled me. WINGDI.H defines the dmFormName field as a BYTE array in _devicemodeA (a.k.a. DEVMODEA), but as a WCHAR array in _devicemodeW (a.k.a. DEVMODEW). The former must be passed to CreateDCA, and latter must be passed to CreateDCW. So that field should indeed be a char array also to get the ANSI/Unicode treatment.

    The final corrections:

    [DllImport("gdi32.dll", CharSet=CharSet.Auto)]
    internal static extern IntPtr CreateDC(
    string lpszDriver, string lpszDevice,
    string lpszOutput, DEVMODE lpInitData);

    with the following DEVMODE definition:

    [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
    internal class DEVMODE
    {
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]
    public char [] dmDeviceName;
    public short dmSpecVersion;
    public short dmDriverVersion;
    public short dmSize;
    public short dmDriverExtra;
    public int dmFields;
    public DEVMODE_UNION u;
    public short dmColor;
    public short dmDuplex;
    public short dmYResolution;
    public short dmTTOption;
    public short dmCollate;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst=32)]
    public char [] dmFormName;
    public short dmLogPixels;
    public int dmBitsPerPel;
    public int dmPelsWidth;
    public int dmPelsHeight;
    public int dmDisplayFlagsOrdmNup;
    public int dmDisplayFrequency;
    public int dmICMMethod;
    public int dmICMIntent;
    public int dmMediaType;
    public int dmDitherType;
    public int dmReserved1;
    public int dmReserved2;
    public int dmPanningWidth;
    public int dmPanningHeight;
    }

  7. Yiru Tang says:

    It seems that the union struct is not matching what is defined in MSDN …

    [StructLayout(LayoutKind.Explicit)]
    internal struct DEVMODE_UNION
    {

    [FieldOffset(0)]
    public short dmOrientation;
    [FieldOffset(2)]
    public short dmPaperSize;
    [FieldOffset(4)]
    public short dmPaperLength;
    [FieldOffset(6)]
    public short dmPaperWidth;
    [FieldOffset(8)]
    public short dmScale;
    [FieldOffset(10)]
    public short dmCopies;
    [FieldOffset(12)]
    public short dmDefaultSource;
    [FieldOffset(14)]
    public short dmPrintQuality;

    [FieldOffset(0)]
    public int dmPosition_x;
    [FieldOffset(4)]
    public int dmPosition_y;
    [FieldOffset(8)]
    public int dmDisplayOrientation;
    [FieldOffset(12)]
    public int dmDisplayFixedOutput;

    }

    union {
    struct {
    short dmOrientation;
    short dmPaperSize;
    short dmPaperLength;
    short dmPaperWidth;
    short dmScale;
    short dmCopies;
    short dmDefaultSource;
    short dmPrintQuality;
    };
    POINTL dmPosition;
    DWORD dmDisplayOrientation;
    DWORD dmDisplayFixedOutput;
    };

    Should
    [FieldOffset(8)]
    public int dmDisplayOrientation;
    [FieldOffset(12)]
    public int dmDisplayFixedOutput;
    start from offset 0?

  8. Adam Nathan says:

    Yes, another great catch! Next time I’m not going to admit how many mistakes are in the code up front! :)

  9. Anonymous says:

    Since we are on Interop "puzzles", Adam, would you be kind enough to look at these two baffling issues http://dotnetweblogs.com/sgentile/posts/6626.aspx in Interop that a friend of mine is reporting. More description is avaliable on his site as well under http://urbanasylum.dynu.com/JustTheFacts/archives/000108.html and http://urbanasylum.dynu.com/JustTheFacts/archives/000109.html.

    We would be most appreciative.

  10. Sam Gentile says:

    Whoops-) The last post was me…
    Since we are on Interop "puzzles", Adam, would you be kind enough to look at these two baffling issues http://dotnetweblogs.com/sgentile/posts/6626.aspx in Interop that a friend of mine is reporting. More description is avaliable on his site as well under http://urbanasylum.dynu.com/JustTheFacts/archives/000108.html and http://urbanasylum.dynu.com/JustTheFacts/archives/000109.html.

    We would be most appreciative.

  11. Adam Nathan says:

    I posted a response to the first issue:

    "I understand your desire for this to work as you describe, although when managed arrays are marshaled to SAFEARRAYs, strings always marshal to BSTRs. The rules are similar to marking non-array types with values from the UnmanagedType enumeration – you can’t marshal a single string as a VARIANT, so you can’t marshal an array of strings as a SAFEARRAY of VARIANTs.

    With an array of objects, you have a few choices for the SafeArraySubType: VT_VARIANT, VT_UNKNOWN, and VT_DISPATCH. This coincides with the fact that you can mark a single object with UnmanagedType.Struct, UnmanagedType.IUnknown, or UnmanagedType.IDispatch. VT_VARIANT is the default behavior, so in your workaround above, you could eliminate the MarshalAsAttribute altogether."

    As for the second issue, I don’t have much to say other than I don’t know why the MIDL warning isn’t documented, and I personally think it should be an error, too. I’m glad he was able to get past the issue!

  12. Sam Gentile says:

    Thanks Adam! as usual, you are most kind.

  13. TomG says:

    This is pretty timely, I have been trying to figure out how to call EnumDisplaySettings but haven’t been having much luck. I am trying to query the dmDisplayOrientation field but am noticing that it seems to execute but no values are populated. Any ideas? I think I am screwing up the DEVMODE_UNION field, what should the corrected layout be there?

  14. TomG, if you look in the Platform SDK docs for EnumDisplaySettings, you’ll see that it only populates a few of the members in the DEVMODE. dmDisplayOrientation is not one of them.

  15. TomG says:

    At the moment I am not seeing any fields populated which is what’s confusing me. If I do a watch on the DEVMODE struct, the only visible thing that happens is the dmSize field changes from 220 to 0 after returning from EnumDisplaySettings. (I believe the dmDisplayOrientation field is populated when called on a Tablet PC which is what I am ultimately trying to do.)

    On a side note, Yiru Tang mentioned that he thought the DEVMODE_UNION struct was declared incorrectly but I don’t believe it is (even though Adam confirmed it?). The docs on MSDN incorrectly show the field as [1] when it should really be [2] (from wingdi.h).

    [1]
    union {
    struct {
    short dmOrientation;
    short dmPaperSize;
    short dmPaperLength;
    short dmPaperWidth;
    short dmScale;
    short dmCopies;
    short dmDefaultSource;
    short dmPrintQuality;
    };
    POINTL dmPosition;
    DWORD dmDisplayOrientation;
    DWORD dmDisplayFixedOutput;
    };

    [2]
    union {
    struct {
    short dmOrientation;
    short dmPaperSize;
    short dmPaperLength;
    short dmPaperWidth;
    short dmScale;
    short dmCopies;
    short dmDefaultSource;
    short dmPrintQuality;
    };
    struct {
    POINTL dmPosition;
    DWORD dmDisplayOrientation;
    DWORD dmDisplayFixedOutput;
    };
    };

  16. TomG, The following code works for me with the DEVMODE definition posted above and the suggested changes.

    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    static extern bool EnumDisplaySettings(string lpszDeviceName, uint iModeNum, [In,Out] DEVMODE lpDevMode);

    static void Main() {
    DEVMODE dm = new DEVMODE();
    dm.dmSize = (short)Marshal.SizeOf(typeof(DEVMODE));
    uint mode = 0;
    while ( EnumDisplaySettings(null, mode++, dm) )
    Console.WriteLine("{0}x{1}, {2} bpp at {3} Hz", dm.dmPelsWidth, dm.dmPelsHeight,
    dm.dmBitsPerPel, dm.dmDisplayFrequency );
    }

  17. Adam Nathan says:

    Yes, the [In, Out] is crucial on the DEVMODE parameter, and I neglected to put that on the signature earlier. Unlike the managed world where a by-value reference type parameter has [In, Out] semantics (the callee can change the type’s fields), passing a by-value reference type parameter to unmanaged code has [In] semantics by default. This can bite you when passing arrays or classes-as-structures to unmanaged code. This situation is probably worth a separate blog entry!

  18. Larry Heller says:

    Hi Adam,

    I am trying to call a COM method from managed code; the method has a parameter which is declared in IDL as:

    [in] SAFEARRAY(IDispatch*) *Objects

    In VB6 this was the way the parameter was created thusly:

    Dim obs() As Object
    ReDim obs(1 To mitems.Count)
    
    Dim i As Long
    For i = 1 To mitems.Count
        Set obs(i) = mitems(i)
    Next i
    

    I am having trouble creating an array to pass to this parameter in VB.Net.
    I have imported the typelib with /sysarray because the method expects a 1-based SAFEARRAY, and I create the parameter as:

        Dim obs As System.Array
        Dim rgLengths() As Integer = {mitems.Count}
        Dim rgBounds() As Integer = {1}
        obs = System.Array.CreateInstance(GetType(Object), rgLengths, rgBounds)
    
        Dim i As Integer
        For i = 1 To mitems.Count
            obs.SetValue(mitems(i), i)
        Next i
    

    But when I try to make the call, I get an exception:

    SafeArrayTypeMismatchException: Specified array was not of the expected type.

    What am I doing wrong?

  19. Adam Nathan says:

    The COM object must be expecting a VT_DISPATCH SafeArray, but when you call it from managed code you’re actually passing a VT_VARIANT SafeArray (in which the type of each VARIANT element happens to be a VT_DISPATCH). To make the array marshal as a VT_DISPATCH SafeArray instead, you’ll need to mark the managed signature’s array parameter with [MarshalAs(UnmanagedType.SafeArray, SafeArraySubType=VT_DISPATCH)]. If you used the type library importer to create an Interop assembly for your type library, you could ILDASM it to a text file, change the signature, then ILASM it. If you happen to have my Interop book, chapter 7 shows you the steps and IL Assembler syntax for doing this.

  20. Adam Nathan says:

    I spoke too soon. The importer is already marking the array with SafeArraySubType=VT_DISPATCH based on the info in the type library. Larry sent me his code and the issue was that the type library needs to be imported with TlbImp’s /sysarray option. (VS.NET uses this option whenver you add a COM reference within the IDE.) That’s because without /sysarray the SAFEARRAY gets imported as a by-reference System.Object array (which must have zero lower bounds). The InvalidCastException comes from the VB.NET code implicitly casting his System.Array to the imported by-ref object array and failing due to the bounds mismatch. In C#, the failure is much more explicit because you’d be forced to do the cast yourself.

  21. TomG says:

    Yup: [In, Out] was the culprit, thanks Mattias. I guess that means there’s a bug in your book for the P/Invoke EnumDisplaySettings definition Adam?

    One more thing if I set the iModeNum param to -1 (ENUM_CURRENT_SETTINGS), it won’t compile because -1 is not a uint. How does one correctly handle things like this in C#?

  22. Richard A. Hein says:

    I worked with EnumDisplaySettings and ChangeDisplaySettings PInvoke calls way back. The code is quite different, but worked for Beta 1 and 2 of .NET 1.0. Interesting to see the changes.

    The original code (looking at it now is almost painful … I don’t know why I put the GPL license there :*) … I have learned a lot!). It can be found here (PInvoke stuff is at the bottom): http://groups.google.ca/groups?hl=en&lr=&ie=UTF-8&oe=UTF-8&selm=OK3BNzv4AHA.1428%40tkmsftngp03&rnum=8

  23. Adam Nathan says:

    TomG, if you’re referring to my definition of EnumDisplaySettings in Appendix E, that doesn’t need [In, Out] because it’s being passed by reference, which exhibits [In, Out] behavior by default. This assumes that EnumDisplaySettings is defined as a struct (not a class), as with all the signatures in the appendix. Of course, this means that you would be unable to pass null for this parameter. If there’s somewhere else I’m defining it, please let me know!

    As for passing -1, the simplest thing to do is define the parameter as an int instead of a uint, as you’d have to do in VB.NET. Or you could leave it as a uint and pass 0xFFFFFFFF. Or you could cast -1 to a uint using C#’s unchecked syntax.

  24. Jim M. says:

    Having as similar problem to the above. Have a COM object I work with. I use the IDE to create the reference. Several COM methods return SAFEARRAYs which are mapped to System.Array. Whenever I call the methods that return values other then strings, I get the SafeArrayTypeMismatchException exception. Any guidance? thanks.

    • Jim
  25. Jeff says:

    I used this very simple Devmode — It seems to work for me.

    // Dev Mode String Sizes
    private const int CCDEVICENAME = 32;
    private const int CCFORMNAME = 32;

    public struct DEVMODE
    {
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=CCDEVICENAME)]
    public string dmDeviceName;
    public short dmSpecVersion;
    public short dmDriverVersion;
    public short dmSize;
    public short dmDriverExtra;
    public int dmFields;
    public short dmOrientation;
    public short dmPaperSize;
    public short dmPaperLength;
    public short dmPaperWidth;
    public short dmScale;
    public short dmCopies;
    public short sdmDefaultSource;
    public short dmPrintQuality;
    public short dmColor;
    public short dmDuplex;
    public short dmYResolution;
    public short dmTTOption;
    public short dmCollate;

     [MarshalAs(UnmanagedType.ByValTStr, SizeConst=CCFORMNAME)]
     public string dmFormName;
     public short dmUnusedPadding;
     public short dmBitsPerPel;
     public int dmPelsWidth;
     public int dmPelsHeight;
     public int dmDisplayFlags;
     public int dmDisplayFrequency;
    

    }

  26. Ken says:

    Help,

    I seem to have fallen into whitewater.!!I am trying to use the AddPrinter API — does that mean I will have to also build a DevMode stuct and populate it? –( Hello Mattias Sjögren)
    P.S. I am not lazy just slow to adjust from VB to C#. I am still trying to add those printer. (They are network printers).

    Any help is appreciated,
    Ken

  27. Thomas Carpe says:

    There seem to be a lot of people here with experience using DLLImport. I am having trouble with a Declare Function statement in VB.net that passes a ByRef sArray() As Integer as one of its parameters. When the reference comes back, it is empty except for the first index of the array. Is there something i can do to make this marshall correctly?

    Thanks in advance

  28. Srinath B.T. says:

    Hi Adam, this is Srinath here. I am basically a C++ programmer, with little knowledge of .NET.

    When i try to access a COM method which returns SAFEARRAY(VT_VARIANT), i am getting

    "Specified array was not of the expected type." exception. Can you please help me to get out of this problem. If possible, can you send me the code snippet to overcome this excetpion.

    Srinath B.T.

  29. Debt Loan says:

    very informative site with lots of useful info

  30. vigrx says:

    Would have risen above Edmund use over with  54 concerta dream. One soon  54 concerta as fourteen often in man’s  54 concerta eye  54 concerta night of Miss Grey finish that copy for  54 concerta Mr. And how long does

    <a href=" http://concertaonline.acb.pl/ ">54 concerta</a> 54 concerta

    <a href=" http://concertaonline.acb.pl/ ">concerta online</a> concerta online

    <a href=" http://concertaritalinvs.acb.pl/ ">concerta effects side</a> concerta effects side

  31. vigrx says:

    Would have risen above Edmund use over with  54 concerta dream. One soon  54 concerta as fourteen often in man’s  54 concerta eye  54 concerta night of Miss Grey finish that copy for  54 concerta Mr. And how long does

    <a href=" http://concertaonline.acb.pl/ ">54 concerta</a> 54 concerta

    <a href=" http://concertaonline.acb.pl/ ">concerta online</a> concerta online

    <a href=" http://concertaritalinvs.acb.pl/ ">concerta effects side</a> concerta effects side