Contents
Introduction
There are a lot of tutorials on Python and COM over the Internet, but in real practice, you might quickly be confused just going beyond standard IDispatch
things. The same occurred to me when I decided to write unit tests for our set of COM components. The components are rather simple, they implement one custom interface (derived from IUnknown
) and one outgoing IDispatch
interface for events.
First, I tried to use the standard pythoncom module, but it turned out that it didn't support custom COM interfaces. Then, I downloaded the comtypes package and started playing with it. Due to a lack of documentation, it took me about one night to write a simple example. So, here is a step-by-step guide on how to begin using comtypes.
Writing a COM object
We will write a COM component where we'll use the basic techniques for Python-COM interoperability and some thread related tricks.
The component we are creating in this tutorial exposes the interface ITaskLauncher
and supports the outgoing interface _ITaskLauncherEvents
. Here is an excerpt from the IDL file:
interface ITaskLauncher : IUnknown{
[id(1), helpstring("method StartTask")] HRESULT StartTask([in] BSTR name);
};
dispinterface _ITaskLauncherEvents
{
methods:
[id(1), helpstring("method TaskQueued")] HRESULT TaskQueued([in] BSTR name);
[id(2), helpstring("method TaskCompleted")] HRESULT TaskCompleted([in] BSTR name);
};
First, create a Visual Studio project. Select the 'ATL Project' template, give a name to your project, and click OK. On the 'Application Settings' page, set the server type to 'Executable (EXE)' and click Finish.
Switch to class view, select your newly created project, and in the context menu, select Add -> Class... Select 'ATL Simple Object' and click 'Add'. Give a short name 'TaskLauncher' and leave all the other fields, click 'Next'. On the 'Options' page, set the threading model to 'Free', set the interface to 'Custom', and check the 'Automation compatible' checkbox. Also check the 'Connection points' to add events support to your class. Click 'Finish' to create the class.
Important note: When creating a custom interface object, you should check the 'Automation compatible' check box. Otherwise, script languages won't access your interface. However, you can always set this attribute named oleautomation
later by directly modifying the .idl file.
In the class view, locate the ITaskLauncher
interface and choose Add -> Add method... in the context menu. This will bring the 'Add method wizard'. Set the method name to 'StartTask', check the 'in' attribute, choose the BSTR
parameter type, set the parameter name to 'name', and click Add. Click Finish. At this step, the wizard creates a StartTask
method and makes an empty function body that implements this method in our CTaskLauncher
class.
Locate TaskServerLib
in the class view, expand it, and find the _ITaskLauncherEvents
interface. In the context menu, choose Add -> Add method... Leave the return type as HRESULT
, set the method name to 'TaskQueued', and add the 'name' parameter like we did earlier. Click 'Finish'. Repeat this for the 'TaskCompleted
' method with the same parameter.
Now, our source interface that declares the events is ready, but we need Visual Studio to implement the functions that actually fire the events. To do this, locate the CTaskLauncher
class in the class view and select Add -> Add Connection Point... in the context menu. In the dialog box that appears, double-click the _ITaskLauncherEvents
interface and click Finish.
Build the project to ensure that there are no errors at this stage. Now, we are ready to actually implement the component methods.
Open the TaskLauncher.h file and add the following definition at the end:
struct TaskInfo
{
BSTR name;
TaskInfo(BSTR taskName)
{
UINT len = ::SysStringLen(taskName);
name = ::SysAllocStringLen(taskName, len);
}
~TaskInfo()
{
::SysFreeString(name);
}
};
Locate the StartTask
function in TaskLauncher.cpp and add the following implementation:
STDMETHODIMP CTaskLauncher::StartTask(BSTR name)
{
TaskInfo* pTaskInfo = new TaskInfo(name);
BSTR taskName = ::SysAllocStringLen(pTaskInfo->name,
::SysStringLen(pTaskInfo->name));
Fire_TaskQueued(taskName);
delete pTaskInfo;
return S_OK;
}
Now, it seems a bit complicated, but we will need the TaskInfo
structure later. Now, it's time to build our component and start writing the client code.
Writing a COM client in Python
First, we need to know the GUID of our type library. Open the Visual Studio generated TaskServer.idl, and locate the block of code shown on the picture below. Copy the contents of the uuid
attribute.
Open the PythonWin IDE, create a new Python script, replacing comtypes.GUID(...)
with the GUID generated for you by Visual Studio.
import comtypes.client as cc
import comtypes
tlb_id = comtypes.GUID("{3DED0EFB-21ED-4337-B098-1B8316952FFA}")
cc.GetModule((tlb_id, 1, 0))
import comtypes.gen.TaskServerLib as TaskLib
class Sink:
def TaskQueued(self, this, name):
print "TaskQueued event. name = %s" % name
def TaskCompleted(self, this, name):
print "TaskCompleted event. name = %s" % name
task_launcher = cc.CreateObject("TaskServer.TaskLauncher",
None, None, TaskLib.ITaskLauncher)
sink = Sink()
advise = cc.GetEvents(task_launcher, sink)
task_launcher.StartTask("first task")
cc.PumpEvents(5)
advise = None
task_launcher = None
Here, we generate the TaskServerLib
module by calling GetModule
, passing as parameters the type library GUID, the major library version (1), and the minor version (0). Next, we declare the class Sink
that will receive the events from our object. cc.CreateObject
creates a COM-object and obtains the ITaskLauncher
interface from it. At this point, we may call the object's methods, but to receive events, we need some extra setup. Create a Sink
class instance, and call cc.GetEvents
to bind the task_launcher
source interface to the sink. GetEvents
returns the advise connection, and it's a good idea to keep a reference on it. Otherwise, the advise connection could be garbage collected and events will stop to work. Next, we call our method StartTask
and wait for events for 5 seconds in the PumpEvents
loop.
Run this script. Your output should be like this:
# Generating comtypes.gen._3DED0EFB_21ED_4337_B098_1B8316952FFA_0_1_0
# Generating comtypes.gen._00020430_0000_0000_C000_000000000046_0_2_0
# Generating comtypes.gen.stdole
# Generating comtypes.gen.TaskServerLib
TaskQueued event. name = first task
Inter-thread interface marshalling
Now, it's time to modify our COM server to make it more asynchronous. The StartTask
method fires the TaskQueued
event immediately after it is called. Let's add a worker thread that will wait for a couple of seconds and fire the TaskCompleted
event. Visual Studio has generated the Fire_TaskCompleted
proxy function for us, but it's quite useless to be directly called from our worker thread as it doesn't do interface marshalling from the worker thread to the main thread. I suppose there's no elegant solution to overcome this issue in ATL. We could modify the CProxy_ITaskLauncherEvents::Fire_TaskCompleted
function and do all the marshalling by ourselves, but in this case, we won't be able to generate this file if our interface changes. Another approach is to introduce the Fire_TaskCompletedInternal
method to our ITaskLauncher
interface and pass to the worker thread the marshaled interface pointer to the ITaskLauncher
interface. As the Fire_TaskCompletedInternal
method is not supposed to be called directly by COM clients, we'll make it hidden although it will remain in the ITaskLauncher
virtual function table.
So, in the class view, find the ITaskLauncher
interface, Add -> Add Method... in the context menu. Fill in method name as Fire_TaskCompletedInternal
, add the 'name' parameter with the direction [in] and type BSTR
. Click the 'Next' button or the 'IDL Attributes' page. Check 'hidden' checkbox and click 'Finish'.
Modify the TaskInfo
structure and add the LPSTREAM marshalledInterface
member variable.
struct TaskInfo
{
BSTR name;
LPSTREAM marshalledInterface;
TaskInfo(BSTR taskName)
{
UINT len = ::SysStringLen(taskName);
name = ::SysAllocStringLen(taskName, len);
}
~TaskInfo()
{
::SysFreeString(name);
}
};
Locate the CTaskLauncher::StartTask
method and replace it with the following code:
STDMETHODIMP CTaskLauncher::StartTask(BSTR name)
{
TaskInfo* pTaskInfo = new TaskInfo(name);
BSTR taskName = ::SysAllocStringLen(pTaskInfo->name, ::SysStringLen(pTaskInfo->name));
Fire_TaskQueued(taskName);
CoMarshalInterThreadInterfaceInStream(IID_ITaskLauncher, (ITaskLauncher*)this,
&pTaskInfo->marshalledInterface);
if (_beginthreadex(NULL, 0, &threadFunc, (LPVOID)pTaskInfo, 0, NULL) == 0)
{
pTaskInfo->marshalledInterface->Release();
delete pTaskInfo;
};
return S_OK;
}
Insert the threadFunc
function definition immediately before the StartTask
method:
unsigned int __stdcall threadFunc(void* p)
{
CoInitializeEx(NULL, COINIT_MULTITHREADED);
Sleep(2000);
TaskInfo* pTaskInfo = (TaskInfo*)p;
ITaskLauncher* pTaskLauncher;
CoGetInterfaceAndReleaseStream(pTaskInfo->marshalledInterface,
IID_ITaskLauncher, (LPVOID*)&pTaskLauncher);
BSTR taskName = ::SysAllocStringLen(pTaskInfo->name, ::SysStringLen(pTaskInfo->name));
HRESULT hr = pTaskLauncher->Fire_TaskCompletedInternal(taskName);
delete pTaskInfo;
CoUninitialize();
return 0;
}
Finally, locate the CTaskLauncher::Fire_TaskCompleteInternal
method definition and make it look like this:
STDMETHODIMP CTaskLauncher::Fire_TaskCompletedInternal(BSTR name)
{
Fire_TaskCompleted(name);
return S_OK;
}
Rebuild the solution and try to run the Python client again. The output should look like this:
# comtypes.gen._3DED0EFB_21ED_4337_B098_1B8316952FFA_0_1_0 must be regenerated
# Generating comtypes.gen._3DED0EFB_21ED_4337_B098_1B8316952FFA_0_1_0
# Generating comtypes.gen.TaskServerLib
TaskQueued event. name = first task
TaskCompleted event. name = first task
Checklist
- Set the
oleautomation
attribute for the custom interfaces. This is needed for scripting languages like Python, VBA etc., supporting late binding via typelibs.
- Call
CoInitializeEx
once in every thread before calling any COM-related functions or your interface functions. Don't forget to call CoUninitialize
before the thread ends.
- You should marshal interface pointers between threads. See
CoMarshalInterThreadInterfaceInStream
/ CoGetInterfaceAndReleaseStream
for more details. Another technique for doing this is 'Global Interface Table'. See the When to Use the Global Interface Table article for more details.
Download code for this article
References