September 23, 2007
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:
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.
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.
“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.
Thank you for this class.
Could you kindly implement event handling also?
(I mean IConnectionPointContainer)
Also it would be great to not to lose changes in ByRef parameters (for those that passed as non-const into CDispatchWrapper::method).
Thanks Ryan, saved me a couple of days!
@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.