Using late binding from C++

If you're working on Windows, COM is a great way to let Python and C++ collaborate. When you're consuming a COM server written in C++ from Python, the win32com module makes this a snap. Consuming a Python COM server from C++, however, can be a pain, because you have to use late binding.

Note: I've seen tantalizing rumors about defining the server's interface via a tlb file, but I've never seen a working example. If you have one, please let me know!

I've written a C++ class that wraps an IDispatch pointer, and makes handling late binding from C++ relatively painless. (Few things are genuinely painless in C++, unfortunately…)

Here's the code. It contains my CDispatchWrapper class, unit tests, and a sample COM server written in Python for testing.

Here is some sample usage:

CDispatchWrapper server(L"SimpleServer.Test") ;

CComVariant name(L"Ryan") ;
CComVariant greeting = server.method(L"Greet", name) ;

ATLASSERT( wstring(greeting.bstrVal) == L"Hello, Ryan") ;

And here's the code in all its glory:

// *****************************************************
//  DispatchWrapper   version:  1.0   date: 09/23/2007
//  ———————————————————–
//  Wraps an IDispatch pointer for use in late binding
//  ———————————————————–
//  Copyright (C) 2007 Ryan Ginstrom
// *****************************************************
//  MIT License
// *****************************************************
#pragma once

#include "atlcomcli.h" // CComPtr etc.
#include "comdef.h" // _com_error
#include <vector>
#include <map>
#include <string>

class CDispatchWrapper
{
   typedef std::vector< CComVariant > var_vec ;
   typedef std::map< std::wstring, DISPID > map_type ;

   // cache DISPIDs for performance
   map_type m_dispids ;

   // We wrap this
   CComPtr< IDispatch > m_app ;

public:

   // constructors
   CDispatchWrapper( LPCWSTR app_name )
   {
      CLSID clsid;
      CLSIDFromProgID(app_name, &clsid);
      this->create(clsid) ;
   }
   CDispatchWrapper( CLSID clsid )
   {
      this->create(clsid) ;
   }
   CDispatchWrapper( const CComPtr< IDispatch > app ) :
   m_app( app )
   {}
   CDispatchWrapper( const VARIANT &app )
   {
      ATLASSERT( app.vt == VT_DISPATCH ) ;
      m_app = (app.pdispVal) ;
   }

   CDispatchWrapper( const IDispatchPtr app ) :
   m_app( app )
   {}

   // assignemt operators
   CDispatchWrapper& operator=( const CComPtr< IDispatch > app )
   {
      m_app = app ;
      return *this ;
   }
   CDispatchWrapper& operator=( const IDispatchPtr app )
   {
      m_app = app ;
      return *this ;
   }
   CDispatchWrapper& operator=( const VARIANT &app )
   {
      m_app = app.pdispVal ;
      return *this ;
   }

   // properties
   void prop_put( LPOLESTR NAME, const VARIANT &PROP )
   {
      Wrap( DISPATCH_PROPERTYPUT, NULL, NAME, PROP ) ;
   }

   CComVariant prop_get( LPOLESTR NAME )
   {
      CComVariant out_arg ;
      Wrap(DISPATCH_PROPERTYGET|DISPATCH_METHOD, &out_arg, NAME ) ;
      return out_arg ;
   }
   CComVariant prop_get( LPOLESTR NAME, const VARIANT &in_arg )
   {
      CComVariant out_arg ;
      Wrap( DISPATCH_PROPERTYGET|DISPATCH_METHOD, &out_arg, NAME,  in_arg ) ;
      return out_arg ;
   }

   // methods
   CComVariant method( LPOLESTR NAME )
   {
      CComVariant out_arg ;
      Wrap( DISPATCH_METHOD, &out_arg, NAME ) ;
      return out_arg ;
   }
   CComVariant method( LPOLESTR NAME, const VARIANT &in_arg )
   {
      CComVariant out_arg ;
      Wrap( DISPATCH_METHOD, &out_arg, NAME,  in_arg ) ;
      return out_arg ;
   }
   CComVariant method( LPOLESTR NAME,
                        const VARIANT &in_arg1,
                        const VARIANT &in_arg2 )
   {
      CComVariant out_arg ;
      Wrap( DISPATCH_METHOD, &out_arg, NAME,  in_arg1, in_arg2 ) ;
      return out_arg ;
   }

   DISPID get_dispid(LPOLESTR name)
   {
   // Cache this for performance
      map_type::iterator pos = m_dispids.find(name) ;
      if ( pos != m_dispids.end())
      {
         return pos->second ;
      }

      DISPID dispID ;

      // Get DISPID for name passed…
      HRESULT hr = m_app->GetIDsOfNames(IID_NULL,
                                       &name,
                                       1,
                                       LOCALE_USER_DEFAULT,
                                       &dispID) ;
      if ( FAILED(hr))
      {
         throw _com_error(hr) ;
      }

      m_dispids[name] = dispID ;
      return dispID ;
   }
protected:
   HRESULT Wrap( WORD autoType, VARIANT *result, LPOLESTR name )
   {
      var_vec vars ;
      return this->Wrap( autoType, result, name,  vars ) ;
   }
   HRESULT Wrap( WORD autoType,
                     VARIANT *result,
                     LPOLESTR name,
                     const VARIANT &in_arg )
   {
      var_vec vars ;
      vars.resize(1) ;
      vars[0] = in_arg ;
      return this->Wrap( autoType, result, name,  vars ) ;
   }
   HRESULT Wrap( WORD autoType,
                              VARIANT *result,
                              LPOLESTR name,
                              const VARIANT &in_arg1,
                              const VARIANT &in_arg2 )
   {
      var_vec vars ;
      vars.resize(2) ;
      vars[0] = in_arg2 ;
      vars[1] = in_arg1 ;
      return this->Wrap( autoType, result, name,  vars ) ;
   }

   // implementation details

   HRESULT Wrap(WORD autoType,
                              VARIANT *result,
                              LPOLESTR name,
                              var_vec &vars )
   {

      // Get DISPID for name passed…
      DISPID dispID = this->get_dispid(name) ;

      // Variables used…
      DISPPARAMS dp = { NULL, NULL, 0, 0 } ;
      // Build DISPPARAMS
      dp.cArgs = vars.size() ;
      if ( ! vars.empty())
      {
         dp.rgvarg = &vars[0] ;
      }

      DISPID dispidNamed = DISPID_PROPERTYPUT ;
      // Handle special-case for property-puts!
      if(autoType & DISPATCH_PROPERTYPUT)
      {
         dp.cNamedArgs = 1;
         dp.rgdispidNamedArgs = &dispidNamed;
      }

      // Make the call
      HRESULT hr = S_OK ;
      EXCEPINFO except_info = {0} ;
      UINT err_num = 0 ;

      hr = m_app->Invoke(dispID,
         IID_NULL,
         LOCALE_SYSTEM_DEFAULT,
         autoType,
         &dp, result,
         &except_info,
         &err_num ) ;

      if ( FAILED( hr ) )
      {
         throw _com_error(hr) ;
      }

      return hr ;
   }

   void create( CLSID clsid )
   {
      HRESULT hr = ::CoCreateInstance(clsid,
                  NULL, CLSCTX_LOCAL_SERVER|CLSCTX_INPROC_SERVER,
                  IID_IDispatch,
                  CDispatchWrapper::out_ptr( &m_app ) ) ;

      if ( FAILED(hr))
      {
         throw _com_error(hr) ;
      }
   }

   // Safe cast of a pointer to type void**
   template< typename I >
   static void ** out_ptr( I **ppi )
   {
      ATLASSERT( ppi != NULL ) ;
      return reinterpret_cast< void ** >(ppi) ;
   }

};

Note: For those of you using the venerable ATL 3.0, you should replace the import of "atlcomcli.h" with "atlbase.h" in order to get CComVariant/CComPtr. Thanks to Gal Aviel for pointing this out.

Comments

  1. October 6th, 2007| 4:50 pm

    Have you looked at #import? It allows you to go from a DLL or EXE with COM objects in it and builds classes directly for you to use. Here is a statement that I use for ADO:

    #import rename( “EOF”, “adoEOF” )

    With that you can open an ADO connection like this:

    ADODB::_ConnectionPtr cnx;
    cnx.CreateInstance( __uuidof( ADODB::Connection ) );
    cnx->CommandTimeout = _wtoi( Setting::value( L”Database”, L”CountCommandTimeout” ).c_str() );
    cnx->Open( m_interface->m_cnx->ConnectionString, L”", L”", ADODB::adConnectUnspecified );

    (Ignore the FOST.3 specific Setting stuff, it just grabs the DSNs.)

    Personally I find _variant_t and _bstr_t better than the CCom variants - there’s something about that MFC pseudo-namespace that feels icky.

  2. Ryan Ginstrom
    October 6th, 2007| 5:13 pm

    “Have you looked at #import? ”

    Yes, and that’s what I’d normally use if I had a tlb resource (either stand-alone or embedded in the executable). But COM servers written in Python (and VB?) have to be late-bound. There’s probably some voodoo you can do to generate a tlb file from an idl file and associate that with the Python COM server, but I haven’t figured out how.

    I personally like CComBSTR better than _bstr_t because it has more functionality (LoadString, CopyTo, etc.) Same goes for CComVariant/_variant_t. And there are a lot of useful classes in the “MFC-style name” classes, like CComQIPtr.

    The underscore variants (no pun intended) have the advantage of an exception class, and of course these are the types used by #import.

    I wrote the class to accept VARIANT arguments though, so you can use either type in your own code.

  3. ~MyXa~
    October 24th, 2007| 11:14 pm

    Thank you for this class.

    Could you kindly implement event handling also?
    (I mean IConnectionPointContainer)

  4. ~MyXa~
    October 25th, 2007| 11:37 pm

    Also it would be great to not to lose changes in ByRef parameters (for those that passed as non-const into CDispatchWrapper::method).

  5. Ben
    March 9th, 2008| 11:35 pm

    Thanks Ryan, saved me a couple of days!

  6. March 11th, 2008| 11:34 am

    @Ben

    Glad you found it useful.

    @~MyXa~

    You’re right, event sinking would be useful to have. I’ll have to do some research on that. Right now I’ve just been passing my Python COM servers “listener” COM objects.

Leave a reply