Search code examples
c#windowscomprint-spooler-api

How can I register for `IPrintAsyncNotifyCallback` from C#


I am trying to receive a callback from a v3 printer driver or port monitor, but want to do so from C#. The docs say that we should be able to just pass an object implementing IPrintAsyncNotifyCallback to RegisterForPrintAsyncNotifications, and there is a sample in C++, but I cannot find a TLB to be able to implement this interface in C#.

Further, RegisterForPrintAsyncNotifications doesn't appear to be exported from the dll named in the documentation (Spoolss.dll).

How can I implement IPrintAsyncNotifyCallback without a TLB? How do I locate RegisterForPrintAsyncNotifications?


Solution

  • Firstly, consider writing a v4 print driver instead. While it is not likely that v3 driver support will be dropped at any point soon, v4 drivers are certainly the way to go for new development. They also have a .Net API for callbacks, which avoids having to write the interop yourself. See PrinterExtensionSample.

    Implementing a COM interface without a TLB

    To implement IPrintAsyncNotifyCallback and the IPrintAsyncNotifyChannel and the IPrintAsyncNotifyDataObject interfaces it refereces, we can find their definitions in prnasnot.h, reproduced below.

    // ... snip ...
    DEFINE_GUID(IID_IPrintAsyncNotifyChannel,        0x4a5031b1, 0x1f3f, 0x4db0, 0xa4, 0x62, 0x45, 0x30, 0xed, 0x8b, 0x04, 0x51);
    DEFINE_GUID(IID_IPrintAsyncNotifyCallback,       0x7def34c1, 0x9d92, 0x4c99, 0xb3, 0xb3, 0xdb, 0x94, 0xa9, 0xd4, 0x19, 0x1b);
    DEFINE_GUID(IID_IPrintAsyncNotifyDataObject,     0x77cf513e, 0x5d49, 0x4789, 0x9f, 0x30, 0xd0, 0x82, 0x2b, 0x33, 0x5c, 0x0d);
    // ... snip ...
    DECLARE_INTERFACE_(IPrintAsyncNotifyDataObject, IUnknown)
    {
        // ... snip ...
    };
    // ... snip ...
    DECLARE_INTERFACE_(IPrintAsyncNotifyChannel, IUnknown)
    {
        // ... snip ...
    };
    // ... snip ...
    DECLARE_INTERFACE_(IPrintAsyncNotifyCallback, IUnknown)
    {
        STDMETHOD(QueryInterface)(
            THIS_
            _In_        REFIID riid,
            _Outptr_ void   **ppvObj
            ) PURE;
    
        STDMETHOD_(ULONG, AddRef)(
            THIS
            ) PURE;
    
        STDMETHOD_(ULONG, Release)(
            THIS
            ) PURE;
    
        STDMETHOD(OnEventNotify)(
             THIS_
             _In_ IPrintAsyncNotifyChannel    *pChannel,
             _In_ IPrintAsyncNotifyDataObject *pData
             ) PURE;
    
        STDMETHOD(ChannelClosed)(
             THIS_
             _In_ IPrintAsyncNotifyChannel    *pChannel,
             _In_ IPrintAsyncNotifyDataObject *pData
             ) PURE;
    };
    

    The IIDs at the top and the method ordering and signatures allow us to translate these to the appropriate ComImports.

    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("77cf513e-5d49-4789-9f30-d0822b335c0d")]
    public interface IPrintAsyncNotifyDataObject
    {
        void AcquireData(out IntPtr data, out uint cbData, out IntPtr schema);
        void ReleaseData();
    }
    
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("4a5031b1-1f3f-4db0-a462-4530ed8b0451")]
    public interface IPrintAsyncNotifyChannel
    {
        void SendNotification(IPrintAsyncNotifyDataObject data);
        void CloseChannel(IPrintAsyncNotifyDataObject data);
    }
    
    [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("7def34c1-9d92-4c99-b3b3-db94a9d4191b")]
    public interface IPrintAsyncNotifyCallback
    {
        void OnEventNotify(IPrintAsyncNotifyChannel channel, IPrintAsyncNotifyDataObject data);
        void ChannelClosed(IPrintAsyncNotifyChannel channel, IPrintAsyncNotifyDataObject data);
    }
    
    public enum PrintAsyncNotifyUserFilter : uint
    {
        kPerUser = 0,
        kAllUsers = 1
    }
    
    public enum PrintAsyncNotifyConversationStyle : uint
    {
        kBiDirectional = 0,
        kUniDirectional = 1
    }
    

    Finding RegisterForPrintAsyncNotifications

    Since the C++ sample works as-is, and RegisterForPrintAsyncNotifications is an import – rather than a macro – the linker will look at the WinSpool.lib file named in the documentation to find the appropriate dll. We can do the same using dumpbin.

    c:\Drop>dumpbin -headers "C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10586.0\um\x64\WinSpool.Lib" > out.txt
    // ... snip ...
      Version      : 0
      Machine      : 8664 (x64)
      TimeDateStamp: 56F9F510 Mon Mar 28 22:22:56 2016
      SizeOfData   : 00000030
      DLL name     : WINSPOOL.DRV
      Symbol name  : RegisterForPrintAsyncNotifications
      Type         : code
      Name type    : name
      Hint         : 162
      Name         : RegisterForPrintAsyncNotifications
    // ... snip ...
    

    which shows that RegisterForPrintAsyncNotifications is actually exported from WINSPOOL.DRV.

    [DllImport("WINSPOOL.DRV", PreserveSig = false, ExactSpelling = true)]
    public static extern void RegisterForPrintAsyncNotifications(
        [MarshalAs(UnmanagedType.LPWStr)] string name,
        [MarshalAs(UnmanagedType.LPStruct)] Guid notificationType, PrintAsyncNotifyUserFilter filter,
        PrintAsyncNotifyConversationStyle converstationStyle,
        IPrintAsyncNotifyCallback callback, out PrintAsyncNotificationSafeHandle handle);
    
    [DllImport("WINSPOOL.DRV", PreserveSig = true, ExactSpelling = true)]
    public static extern int UnRegisterForPrintAsyncNotifications(IntPtr handle);
    
    public sealed class PrintAsyncNotificationSafeHandle : SafeHandleZeroOrMinusOneIsInvalid
    {
        public PrintAsyncNotificationSafeHandle()
            : base(true)
        {
        }
    
        protected override bool ReleaseHandle()
        {
            return UnRegisterForPrintAsyncNotifications(handle) == 0 /* S_OK */;
        }
    }