Search code examples
c#windowsprint-spooler-api

EnumJobs returning a different JOB_INFO_1 size than Marshal.SizeOF


I'm calling the Win32 function EnumJobs (http://msdn.microsoft.com/en-us/library/windows/desktop/dd162625(v=vs.85).aspx) from managed code (C#).

    [DllImport("Winspool.drv", SetLastError = true, EntryPoint = "EnumJobsA")]
    public static extern bool EnumJobs(
       IntPtr hPrinter,                    // handle to printer object
       UInt32 FirstJob,                // index of first job
       UInt32 NoJobs,                // number of jobs to enumerate
       UInt32 Level,                    // information level
       IntPtr pJob,                        // job information buffer
       UInt32 cbBuf,                    // size of job information buffer
       out UInt32 pcbNeeded,    // bytes received or required
       out UInt32 pcReturned    // number of jobs received
    );

EnumJobs(_printerHandle, 0, 99, 1, IntPtr.Zero, 0, out nBytesNeeded, out pcReturned);

I'm specifying Level 1 to receive a JOB_INFO_1 but the problem I'm having is the above function is returning nBytesNeeded as 240 per struct while the Marshal.SizeOf(typeof(JOB_INFO_1)) is 64 bytes causing a memory exception when I run Marshal.PtrToStructure. Counting the bytes manually for the struct gives 64 so I'm at a bit of a loss as to why I'm receiving the 240 byte structures from function, any insight would be appreciated.

[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet=CharSet.Unicode)]
public struct JOB_INFO_1
{
    public UInt32 JobId;
    public string pPrinterName;
    public string pMachineName;
    public string pUserName;
    public string pDocument;
    public string pDatatype;
    public string pStatus;
    public UInt32 Status;
    public UInt32 Priority;
    public UInt32 Position;
    public UInt32 TotalPages;
    public UInt32 PagesPrinted;
    public SYSTEMTIME Submitted;
}

Solution

  • The size of 64 is indeed correct for JOB_INFO_1 but if you look closely at the documentation, it talks about an array of structs :

    pJob [out]
    A pointer to a buffer that receives an array of JOB_INFO_1, JOB_INFO_2, or JOB_INFO_3 structures.
    

    Additionally it is written :

    The buffer must be large enough to receive the array of structures and any strings or other data to which the structure members point.

    So there are bytes here for extra data beside the structs themselves.

    Solution:

    Populate the structs, increment pointer for next struct and ignore the remaining bytes.

    Complete example:

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Windows;
    
    namespace WpfApplication3
    {
        public partial class MainWindow
        {
            public MainWindow()
            {
                InitializeComponent();
                Loaded += MainWindow_Loaded;
            }
    
            private void MainWindow_Loaded(object sender, RoutedEventArgs e)
            {
                // Get handle to a printer
                var hPrinter = new IntPtr();
                bool open = NativeMethods.OpenPrinterW("Microsoft XPS Document Writer", ref hPrinter, IntPtr.Zero);
                Debug.Assert(open);
    
                /* Query for 99 jobs */
                const uint firstJob = 0u;
                const uint noJobs = 99u;
                const uint level = 1u;
    
                // Get byte size required for the function
                uint needed;
                uint returned;
                bool b1 = NativeMethods.EnumJobsW(
                    hPrinter, firstJob, noJobs, level, IntPtr.Zero, 0, out needed, out returned);
                Debug.Assert(!b1);
                uint lastError = NativeMethods.GetLastError();
                Debug.Assert(lastError == NativeConstants.ERROR_INSUFFICIENT_BUFFER);
    
                // Populate the structs
                IntPtr pJob = Marshal.AllocHGlobal((int) needed);
                uint bytesCopied;
                uint structsCopied;
                bool b2 = NativeMethods.EnumJobsW(
                    hPrinter, firstJob, noJobs, level, pJob, needed, out bytesCopied, out structsCopied);
                Debug.Assert(b2);
    
                var jobInfos = new JOB_INFO_1W[structsCopied];
                int sizeOf = Marshal.SizeOf(typeof (JOB_INFO_1W));
                IntPtr pStruct = pJob;
                for (int i = 0; i < structsCopied; i++)
                {
                    var jobInfo_1W = (JOB_INFO_1W) Marshal.PtrToStructure(pStruct, typeof (JOB_INFO_1W));
                    jobInfos[i] = jobInfo_1W;
                    pStruct += sizeOf;
                }
                Marshal.FreeHGlobal(pJob);
    
                // do something with your structs
            }
        }
    
        public class NativeConstants
        {
            public const int ERROR_INSUFFICIENT_BUFFER = 122;
        }
    
        public partial class NativeMethods
        {
            [DllImport("kernel32.dll", EntryPoint = "GetLastError")]
            public static extern uint GetLastError();
        }
    
        public partial class NativeMethods
        {
            [DllImport("Winspool.drv", EntryPoint = "OpenPrinterW")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool OpenPrinterW([In] [MarshalAs(UnmanagedType.LPWStr)] string pPrinterName,
                ref IntPtr phPrinter, [In] IntPtr pDefault);
    
            [DllImport("Winspool.drv", EntryPoint = "EnumJobsW")]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool EnumJobsW([In] IntPtr hPrinter, uint FirstJob, uint NoJobs, uint Level, IntPtr pJob,
                uint cbBuf, [Out] out uint pcbNeeded, [Out] out uint pcReturned);
        }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct JOB_INFO_1W
        {
            public uint JobId;
            [MarshalAs(UnmanagedType.LPWStr)] public string pPrinterName;
            [MarshalAs(UnmanagedType.LPWStr)] public string pMachineName;
            [MarshalAs(UnmanagedType.LPWStr)] public string pUserName;
            [MarshalAs(UnmanagedType.LPWStr)] public string pDocument;
            [MarshalAs(UnmanagedType.LPWStr)] public string pDatatype;
            [MarshalAs(UnmanagedType.LPWStr)] public string pStatus;
            public uint Status;
            public uint Priority;
            public uint Position;
            public uint TotalPages;
            public uint PagesPrinted;
            public SYSTEMTIME Submitted;
        }
    
        [StructLayout(LayoutKind.Sequential)]
        public struct SYSTEMTIME
        {
            public ushort wYear;
            public ushort wMonth;
            public ushort wDayOfWeek;
            public ushort wDay;
            public ushort wHour;
            public ushort wMinute;
            public ushort wSecond;
            public ushort wMilliseconds;
        }
    }
    

    Result:

    Job 1:

    enter image description here

    Job 2 :

    enter image description here

    EDIT

    It seems that you will have to get a little more robust checking than I did, because EnumJobs seems to return true when there are no jobs in queue. In the case of my example the assertion will fail but that won't mean that the code is wrong; just make sure that you have some jobs in the queue for the purpose of testing the function.