Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Printers and SafeHandles

4.84/5 (16 votes)
16 Apr 20076 min read 1   3.5K  
Using SafeHandles to access PrinterInfo and DriverInfo structures.

Sample Image

Introduction

The System.Drawing.Printing.PrinterSettings class is powerful, but limited. Users who are interested in the advanced settings of their printer get stuck. Information that is available by clicking a PrinterDialog is hidden, and the default settings of the printer are unavailable too. The .NET 2.0 framework provides a huge advance in the System.Printing class, but for some actions, we still need basic Windows functions. Thus the idea of using SafeHandles to access and change advanced PrinterSettings.

The SafeHandleZeroOrMinusOneIsInvalid

Many printer functions need a handle to an open printer. When this handle is no longer needed, it must be closed properly. This is an ideal situation to start using a SafeHandle.

The .NET Framework 2.0 SafeHandleZeroOrMinusOneIsInvalid class is a managed wrapper around an IntPtr that knows:

  • when it is invalid, (zero or minus one)
  • when the handle is closed, (this.IsClosed)
  • how to close the handle, (ReleaseHandle)
  • how to handle asynchronous thread aborts safely (it derives from the CriticalFinalizerObject).

The moment we open our printer, we know for certain that the handle will be closed properly, even when the AppDomain is unloaded by the host, or when normal finalizers block or take too long to clean up. Only two things have to be done:

  • Use a constructor, specifying if the handle is to be released reliably.
  • Override the ReleaseHandle method.
C#
public sealed class qSafePrinterHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    internal qSafePrinterHandle() 
        : base(true) 
    {}
    
    [return: MarshalAs(UnmanagedType.U1)]
    protected override bool ReleaseHandle()
    {
        // Only close printer if handle is not invalid
        if (IsInvalid) 
            return true;
        // If we fail, the ReleaseHandleFailed MDA will be activated
        if (!ClosePrinter(this.handle))
            return false;
        // Make the handle invalid
        this.SetHandle(IntPtr.Zero);
        return true; 
    }
}

The IsClosed property refers to the closing of our handle, not of our printer. It will be true only when the SetHandleAsInvalid, Dispose, or Close method is called, and there are no references to the SafeHandle object on other threads. Therefore, immediately after using the ClosePrinter function, we have to set the handle to zero indicating that we do not have an open printer.

Additional Constructor

Providing an additional constructor, our class knows not only how to close our printer, but also how to open it:

C#
...

private qPrinterDefaults _PrinterDefaults = new qPrinterDefaults(true);

internal qSafePrinterHandle(string printername)
    : base(true)
{
    OpenPrinter(printername, out this.handle, ref _PrinterDefaults) ;
}
...

We do not have to be careful with this constructor (a stored value can be useless if someone changed the name of our printer):

  • Our ReleaseHandle only closes the printer when there is a valid handle.
  • Even a null value is accepted as a parameter of this function, the handle returned will be of the local printer server.

Using the same handle for a different printer causes no problems either, if we first call the Release method.

C#
public string PrinterName
{
    get 
    {
        if  (this.IsInvalid) return String.Empty; 
        return new PrinterInfo5().PrinterName;
    }
    set
    {
        // check if we have a new value
        if (value != PrinterName)
        {
            this.ReleaseHandle();
            OpenPrinter(value, out this.handle, ref _PrinterDefaults) ;
        }
    }
}

Caution is only needed when accessing properties. If our handle is valid (not zero, not minus one, handle not closed, and an open printer), we can query for data; if not, we have to return a null value.

C#
public PrinterInfo5 PrinterInfo5
{
    get
    {
        if (this.IsInvalid) 
            return null;
        else
            return PrinterInfo5(this);
    }
}

Reading Unmanaged Memory

Let's first look at some code:

C#
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
internal class PrinterInfo5
{
    private string pPrinterName;           // 0
    private string pPortName;              // 1
    private int pAttributes;               // 2
    private int pDeviceNotSelectedTimeout; // 3
    private int pTransmissionRetryTimeout; // 4

    internal PrinterInfo5(qSafePrinterHandle SafePrinterHandle)
    {
        int num1 = 0;
        // how much memory do we need
        int num2 = 
            GetPrinter(SafePrinterHandle, 5, IntPtr.Zero, 0, out num1);
        if (num1 <= 0)
            throw new Win32Exception("First Call PrinterInfo5 Error");
        // allocate enough memory
        IntPtr data = Marshal.AllocCoTaskMem(num1);
        int num3 = 0;
        // fill the memory with data
        num2 = GetPrinter(SafePrinterHandle, 5, data, num1, out num3);
        if ((num2 != 1) | (num1 != num3))
            throw new Win32Exception("Second Call PrinterInfo5 Error");
        // Method 1
        Marshal.PtrToStructure(data,this);
        // End Method 1
        // free the allocated memory
        Marshal.FreeCoTaskMem(data);
    }
}

We specify a class whose layout is sequential: first, we have the printer name, next comes the port name, then the attributes, the 'device not selected' timeout, and finally, the 'transmission retry' timeout. We are working in Unicode: we specify our charset in the structure layout of our class and in the calling of our GetPrinter function. First, we call the GetPrinter function to know the size of our answer. We allocate memory for our result, and call the function again with an IntPtr to the allocated memory. If the function returns a zero value and the output parameter has the size of our allocated memory, our memory is easily marshaled into a structure. Last but not least, we free the memory we allocated.

Another way of doing the same thing:

C#
 ...
 // Method 2
 int IntPtrSize = Marshal.SizeOf(typeof(IntPtr));
 pPrinterName =
   Marshal.PtrToStringUni(Marshal.ReadIntPtr(data, IntPtrSize * 0));
 pPortName =
   Marshal.PtrToStringUni(Marshal.ReadIntPtr(data, IntPtrSize * 1));
 pAttributes = Marshal.ReadInt32(data, (IntPtrSize * 2));
 pDeviceNotSelectedTimeout = Marshal.ReadInt32(data, (IntPtrSize * 3));
 pTransmissionRetryTimeout = Marshal.ReadInt32(data, (IntPtrSize * 4));
// End Method 2
 ...

When we call the Win32 function, we receive a pointer to something like {intptr, intptr, int32, int32, int32}. The first IntPtr points to a place in unmanaged memory where you can find the printer name, the second IntPtr holds the port name, and next comes three integers. Knowing the size of an IntPtr, it is easy to read on each offset the values we need.

The same done unsafely:

C#
internal unsafe PrinterInfo5(qSafePrinterHandle SafePrinterHandle)
{
    ...
    // Method 3
    num1 = data.ToInt32();
    pPrinterName = new string(*((char**)(num1 + (IntPtrSize * 0))));
    pPortName = new string(*((char**)(num1 + (IntPtrSize * 1))));;
    pAttributes = *(((int*)(num1 + (IntPtrSize * 2))));
    pDeviceNotSelectedTimeout = *(((int*)(num1 + (IntPtrSize * 3))));
    pTransmissionRetryTimeout = *(((int*)(num1 + (IntPtrSize * 4))));
    // End Method 3      
    ...

Another Safehandle

Our pPrinterName is the first IntPtr to read. But in the PrinterInfo1 structure, the name of the printer comes on the third place, and in the PrinterInfo2, it is on the second IntPtr. Why not create a class that:

  • stores what level of information we are querying,
  • allocates memory for our answer,
  • fills the memory with an answer,
  • frees the allocated memory when no longer needed,
  • can read strings and integers at different offsets.

Another implementation of a SafeHandleZeroOrMinusOneIsInvalid:

C#
public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
    private int IntPtrSize = Marshal.SizeOf(typeof(IntPtr));
    internal int Level;
    protected SafeHandle PrinterSafeHandle;
    protected int BytesNeeded;
    protected int Size;
       
    protected abstract void RefreshInfo();
      
    protected qSafeInfo(SafeHandle pSafeHandle, int infoLevel) 
        : base (true)
    {
        PrinterSafeHandle = pSafeHandle;
        Level = infoLevel;
        RefreshInfo();
    }
            
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    [return: MarshalAs(UnmanagedType.U1)]
    protected override bool ReleaseHandle()
    {
        if (!IsInvalid)
        {
            if (Size > 0)
            {
               //Free the allocated memory
                Marshal.FreeHGlobal(base.handle);
                base.SetHandle(IntPtr.Zero);
            }
        }
        return true;
    }

    protected void AllocMem()
    {
        if (Size >= BytesNeeded)
            // do nothing if we allocated to much memory
            return;
        if (Size != 0)
           // Release the memory if we allocated not enough memory
            Marshal.FreeHGlobal(base.handle);
        if (BytesNeeded != 0)
            // allocate sufficient memory
            base.handle = Marshal.AllocHGlobal(BytesNeeded);
        // remember how much memory we allocated
        Size = BytesNeeded;
    }

    protected int GetIntField(int offset)
    {
        if (this.IsInvalid) return 0;
        return Marshal.ReadInt32(this.handle, IntPtrSize * offset); 
    }        
        
    protected string GetStringField(int offset)
    {
        if (this.IsInvalid) return null;
        IntPtr ptr = Marshal.ReadIntPtr(this.handle, IntPtrSize * offset);
        string s = Marshal.PtrToStringUni(ptr);
        if (s == null) s = string.Empty;
        return s;
    }        
}

Here, we use the handle to point to a place in memory. The moment we close our handle, memory will be automatically freed. When the AllocMem function is called and not enough memory is allocated, we simply allocate more memory (the possibility that other printers use the same driver is quite big, people tend to buy from one provider, and the PrinterInfo structures are not too big).

Now, we are ready to read information from our printer. But not only printer information, we can use the same SafeHandle to read ports, drivers, forms, print monitors,...

C#
public abstract class qSafePrinterInfo : qSafeInfo
{
    protected qSafePrinterInfo(SafeHandle PrinterHandle, int level)
        :base(PrinterHandle, level)
    {}

    [PrintingPermission(SecurityAction.Demand, Level = 
               PrintingPermissionLevel.DefaultPrinting)]
    protected override void RefreshInfo()
    {
        if (!GetPrinter(PrinterHandle, Level, handle, Size, 
             out BytesNeeded) && (BytesNeeded > 0))
        {
            // allocate memory
            AllocMem();
            if (!GetPrinter(PrinterHandle, Level, handle, Size, 
                        out BytesNeeded) && (BytesNeeded > 0))
                throw new Win32Exception(GetType().FullName + 
                          Level.ToString() + " Error");
        }
    }

public abstract class qSafeDriverInfo : qSafeInfo
{
    protected qSafeDriverInfo(SafeHandle PrinterHandle, int level) 
        : base(PrinterHandle, level)
    { }

    [PrintingPermission(SecurityAction.Demand, Level = 
                PrintingPermissionLevel.DefaultPrinting)]
    protected override void RefreshInfo()
    {
        if (!GetPrinterDriver(PrinterHandle, IntPtr.Zero, Level, handle, 
             Size, out BytesNeeded) && (BytesNeeded > 0))
        {
            AllocMem();
            if (!GetPrinterDriver(PrinterHandle, IntPtr.Zero, Level, handle, 
                        Size, out BytesNeeded) && (BytesNeeded > 0))
                throw new Win32Exception(GetType().FullName + 
                          Level.ToString() + " Error");
        }
    }
}

Writing to Unmanaged Memory

If you can read, you can write. This universal truth is especially true in a situation where we know our handle is valid and not closed.

C#
...
public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
...
    protected abstract bool Update();

    protected bool SetIntField(int offset, int val)
    {
        if (this.IsInvalid) throw new InvalidOperationException();
        Marshal.WriteInt32(this.handle, IntPtrSize * offset, val);
        return Update();
    }
    
    protected bool SetStringField(int offset, string val)
    {
        if (this.IsInvalid) throw new InvalidOperationException();
        IntPtr ptr1 = Marshal.StringToHGlobalUni(val);
        Marshal.WriteIntPtr(this.handle, IntPtrSize * offset, ptr1);
        bool b = Update();
        Marshal.FreeHGlobal(ptr1);
        return b;
    }
}

public abstract class qSafePrinterInfo : qSafeInfo
{
...
     [PrintingPermission(SecurityAction.Demand, Level = 
                   PrintingPermissionLevel.AllPrinting)]
     protected override bool Update()
     {
        if (this.IsInvalid)
            throw new InvalidOperationException("Invalid Memory handle");
        if (PrinterSafeHandle.IsInvalid)
            throw new InvalidOperationException("Printer SafeHandle Invalid");
        bool b = SetPrinter(PrinterHandle, Level, handle, 0);
        if (!b) throw new Win32Exception("Printer NOT Updated.");
        this.RefreshInfo();
        return b;
    }
}

Now, we can read and write to almost every field of our printer info. Our new PrinterInfo5 looks like this:

C#
public sealed class PrinterInfo5 : qSafePrinterInfo
{
    internal PrinterInfo5(SafeHandle PrinterHandle)
        : base(PrinterHandle,5)
    { }

    public string PrinterName
    {
        get { return GetStringField(0);}
        set { SetStringField(0, value);}
    }
    public string PortName
    {
        get { return GetStringField(1);}
        set { SetStringField(1, value);}
    }
    ...
    public int DeviceNotSelectedTimeout
    {
        get { return GetIntField(3); }
        set { SetIntField(3, value);}
    }
    public int TransmissionRetryTimeout
    {
        get { return GetIntField(4); }
        set { SetIntField(4, value);}
    }  
}

Enumeration Functions

A lot of printer functions return an array of data (EnumPrinters, EnumForms, EnumPrinterDrivers,...). If the size of the structure is known, it is easy to calculate at which offset starts the next record.

C#
public class qSafeDriverInfos : qSafeInfo
{
    private string _servername;
    private int recordcount;

    internal qSafeDriverInfos(string ServerName, int level)
        : base(IntPtr.Zero, level)
    {
        _servername = ServerName;
        RefreshInfo();
    }

    [PrintingPermission(SecurityAction.Demand, Level = 
                PrintingPermissionLevel.DefaultPrinting)]
    protected override void RefreshInfo()
    {
        if (!EnumPrinterDrivers(_servername, "All", Level, handle, 
             Size, out BytesNeeded, out recordcount) && (BytesNeeded > 0))
        {
            AllocMem();
            if (!EnumPrinterDrivers(_servername, "All", Level, handle, 
                                    Size, out BytesNeeded, out recordcount) )
                throw new Win32Exception("DriverInfo" + 
                          Level.ToString() + " Error");
        }
    }

    internal qSafeDriverInfo[] Drivers
    {
        get
        {
            qSafeDriverInfo[]  _collection = 
                     new qSafeDriverInfo[this.recordcount];
            IntPtr ptr = base.handle;
            for (int i = 1; i <= this.recordcount; i++)
            {
                switch (this.Level)
                {
                    case 1:
                        _collection[i-1] = new DriverInfo1(ptr);
                        //size = 1
                        ptr = new IntPtr(base.handle.ToInt32() + 
                                  (i * this.IntPtrSize * 1));
                        break;
                    case 2:
                        _collection[i-1] = new DriverInfo2(ptr);
                        //size = 6
                        ptr = new IntPtr(base.handle.ToInt32() + 
                                  (i * this.IntPtrSize * 6));
                        break;
                    ...
                    default:
                        return null;
                }
            }
            return _collection;
        }
    }
}

We only have to provide an additional constructor to our DriverInfo and qSafeInfo classes. Because the memory is already allocated by the call of the Enum function, we can use the constructor with a false argument: the memory will be freed the moment we close the qSafeDriverInfos class.

C#
public sealed class DriverInfo1 : qSafeDriverInfo
{
    ...
    public DriverInfo1(IntPtr MemoryHandle)
        : base(MemoryHandle, 1)
    { }
    ...
}

public abstract class qSafeInfo : SafeHandleZeroOrMinusOneIsInvalid
{
    ...
    protected qSafeInfo(IntPtr pMemoryHandle, int infoLevel)
        : base(false)
    {
        base.handle = pMemoryHandle;
        Level = infoLevel;
    }
    ...
}

Reading and Writing to the DevMode

Sample Image

The DevMode is a structure inside the PrinterInfo2 class that holds the default printer settings. We can use the same ideas to read and update values:

C#
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 1)]
public class DevMode
{
    ...

    internal DevMode(IntPtr DevModeHandle)
    {
        Marshal.PtrToStructure(DevModeHandle,this);
    }
  
    public short Copies
    {
        get { return dmCopies; }
        set
        {
            if (value != dmCopies) 
            {
                dmCopies = value;
                UpdateDevMode(0x100);
            }
        }
    }
    private bool UpdateDevMode(int field)
    {
        if (PrintComponent.SafePrinterHandle.IsInvalid) return false;
        dmFields = field;
        IntPtr ptr1 = 
          PrintComponent.SafePrinterHandle.PrinterInfo2.GetIntPtrField(7);
        Marshal.StructureToPtr(this, ptr1, true);
        return 
         PrintComponent.SafePrinterHandle.PrinterInfo2.SetIntPtrField(7, ptr1);
    }
}

Of course, additional settings have to be checked; the value for our copies cannot be negative and has to be less than the maximum copies the printer can handle. We have to check if our printer supports setting this field. This is partly done in the code that I provide. In this article, I only wanted to show how easy life can be using SafeHandles.

Points of Interest

  • It is possible to save PrinterInfo, DevMode, PortInfo, PrintJobInfo, ... (Marshal.Copy(this.handle , new byte[Size] , 0 ,Size))
  • and of course, restore (Size = b.Length; Marshal.Copy(b, 0, this.handle, Size);Update();).

Part 2 explains how to monitor a printer using a SafeWaitHandle.

History

  • 2 July 2006: Initial version.
  • 5 October 2006: Link added to part 2.
  • 6 December 2006: Updated version, and minor changes to the DevMode class.
  • 15 March 2007: Links updated.
  • 13 April 2007:
    • Example of enumeration function added.
    • Support for network printers added.
    • Collection of DriverInfo by PrintServer.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here