Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / ATL
Article

Working with custom COM interfaces from Python

Rate me:
Please Sign up or sign in to vote.
4.60/5 (12 votes)
2 Jan 2008CPOL6 min read 84.4K   762   41   3
Step-by-step tutorial to begin using the comtypes package.

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.

Atl Project Wizard

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.

Atl Simple Object Wizard

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.

Implement Connection Point Wizard

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:

C++
struct TaskInfo
{
    BSTR name;
    TaskInfo(BSTR taskName)
    {
        //copy taskName to name
        UINT len = ::SysStringLen(taskName);
        name = ::SysAllocStringLen(taskName, len);
    }
    ~TaskInfo()
    {
        ::SysFreeString(name);
    }
};

Locate the StartTask function in TaskLauncher.cpp and add the following implementation:

C++
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.

Locating library GUID

Open the PythonWin IDE, create a new Python script, replacing comtypes.GUID(...) with the GUID generated for you by Visual Studio.

Python
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.

C++
struct TaskInfo
{
    BSTR name;
    LPSTREAM marshalledInterface;
    TaskInfo(BSTR taskName)
    {
        //copy taskName to name
        UINT len = ::SysStringLen(taskName);
        name = ::SysAllocStringLen(taskName, len);
    }
    ~TaskInfo()
    {
        ::SysFreeString(name);
    }
};

Locate the CTaskLauncher::StartTask method and replace it with the following code:

C++
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)
    {
        //clean up if we couldn't start the thread
        pTaskInfo->marshalledInterface->Release();
        delete pTaskInfo;
    };

    return S_OK;
}

Insert the threadFunc function definition immediately before the StartTask method:

C++
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:

C++
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Russian Federation Russian Federation
Independent software developer. Expertise in full software development life cycle, web development (ASP.Net, C#), desktop apps (MFC/C++, WTL/C++, Windows.Forms/C#), DCOM components, .NET interop, TAPI and TSAPI CTI-integration, software quality assurance/testing.
Willing to relocate.

Comments and Discussions

 
GeneralMy vote of 5 Pin
bChen@sh27-Aug-12 22:59
bChen@sh27-Aug-12 22:59 
GeneralError in my python3.2 environment Pin
simonwoo11-Apr-11 23:43
simonwoo11-Apr-11 23:43 
GeneralCalling QueryInterface Pin
Sharjith10-Dec-08 4:46
professionalSharjith10-Dec-08 4:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.