65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Python 操作自定义 COM 接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (11投票s)

2007 年 12 月 31 日

CPOL

6分钟阅读

viewsIcon

88199

downloadIcon

810

开始使用 comtypes 包的分步教程。

目录

引言

互联网上有很多关于 Python 和 COM 的教程,但在实际操作中,一旦你开始接触标准 IDispatch 以外的内容,很容易就会感到困惑。当我想为我们的 COM 组件集编写单元测试时,我也遇到了同样的问题。这些组件相当简单,它们实现了一个自定义接口(派生自 IUnknown)和一个用于事件的传出 IDispatch 接口。

起初,我尝试使用标准的 pythoncom 模块,但发现它不支持自定义 COM 接口。然后,我下载了 comtypes 包并开始尝试使用它。由于文档匮乏,我花了一个晚上才写出了一个简单的示例。因此,这里是如何开始使用 comtypes 的分步指南。

编写 COM 对象

我们将编写一个 COM 组件,其中我们将使用 Python-COM 互操作性的基本技术和一些与线程相关的技巧。

我们在本教程中创建的组件公开了 ITaskLauncher 接口,并支持传出的 _ITaskLauncherEvents 接口。这是 IDL 文件中的一段摘录。

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);
};

首先,创建一个 Visual Studio 项目。选择“ATL 项目”模板,为您的项目命名,然后单击“确定”。在“应用程序设置”页面上,将服务器类型设置为“可执行文件 (EXE)”,然后单击“完成”。

Atl Project Wizard

切换到类视图,选择新创建的项目,然后在上下文菜单中选择“添加”->“类...”。选择“ATL 简单对象”并单击“添加”。给它一个短名称“TaskLauncher”,然后保留所有其他字段,单击“下一步”。在“选项”页面上,将线程模型设置为“自由”,将接口设置为“自定义”,并选中“自动化兼容”复选框。另外,选中“连接点”以向您的类添加事件支持。单击“完成”以创建类。

Atl Simple Object Wizard

重要提示:创建自定义接口对象时,应选中“自动化兼容”复选框。否则,脚本语言将无法访问您的接口。但是,您可以稍后通过直接修改 .idl 文件,将此属性命名为 oleautomation 来设置。

在类视图中,找到 ITaskLauncher 接口,然后在上下文菜单中选择“添加”->“添加方法...”。这将打开“添加方法向导”。将方法名称设置为“StartTask”,选中“in”属性,选择 BSTR 参数类型,将参数名称设置为“name”,然后单击“添加”。单击“完成”。在此步骤中,向导将创建一个 StartTask 方法,并在我们的 CTaskLauncher 类中实现此方法的一个空函数体。

在类视图中,找到 TaskServerLib,展开它,然后找到 _ITaskLauncherEvents 接口。在上下文菜单中,选择“添加”->“添加方法...”。将返回类型保留为 HRESULT,将方法名称设置为“TaskQueued”,并像之前一样添加“name”参数。单击“完成”。对“TaskCompleted”方法重复此操作,使用相同的参数。

现在,我们的声明事件的源接口已准备就绪,但我们需要 Visual Studio 来实现实际触发事件的函数。为此,在类视图中找到 CTaskLauncher 类,然后在上下文菜单中选择“添加”->“添加连接点...”。在出现的对话框中,双击 _ITaskLauncherEvents 接口,然后单击“完成”。

Implement Connection Point Wizard

生成项目,以确保在此阶段没有错误。现在,我们已准备好实际实现组件方法。

打开 TaskLauncher.h 文件,并在末尾添加以下定义。

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

TaskLauncher.cpp 中找到 StartTask 函数,并添加以下实现。

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;
}

现在,这看起来有点复杂,但我们稍后将需要 TaskInfo 结构。现在,是时候构建我们的组件并开始编写客户端代码了。

用 Python 编写 COM 客户端

首先,我们需要知道我们类型库的 GUID。打开 Visual Studio 生成的 TaskServer.idl,找到下图所示的代码块。复制 uuid 属性的内容。

Locating library GUID

打开 PythonWin IDE,创建一个新的 Python 脚本,将 comtypes.GUID(...) 替换为您由 Visual Studio 生成的 GUID。

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

在这里,我们通过调用 GetModule 来生成 TaskServerLib 模块,并将类型库 GUID、主库版本(1)和次版本(0)作为参数传递。接下来,我们声明 Sink 类,它将接收来自我们对象的事件。cc.CreateObject 创建一个 COM 对象并从中获取 ITaskLauncher 接口。此时,我们可以调用对象的方法,但要接收事件,我们需要一些额外的设置。创建一个 Sink 类实例,并调用 cc.GetEventstask_launcher 源接口绑定到 sink。GetEvents 返回一个 advise 连接,最好保留对其的引用。否则,advise 连接可能会被垃圾回收,事件将停止工作。接下来,我们在 PumpEvents 循环中调用我们的方法 StartTask 并等待事件 5 秒钟。

运行此脚本。您的输出应如下所示:

# 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

线程间接口封送

现在,是时候修改我们的 COM 服务器使其更具异步性了。StartTask 方法在调用后立即触发 TaskQueued 事件。让我们添加一个工作线程,它将等待几秒钟并触发 TaskCompleted 事件。Visual Studio 为我们生成了 Fire_TaskCompleted 代理函数,但直接从工作线程调用它相当无用,因为它不会从工作线程到主线程进行接口封送。我猜在 ATL 中没有优雅的解决方案可以克服这个问题。我们可以修改 CProxy_ITaskLauncherEvents::Fire_TaskCompleted 函数并自己进行所有封送,但这样,如果我们的接口发生更改,我们就无法生成此文件。另一种方法是在我们的 ITaskLauncher 接口中引入 Fire_TaskCompletedInternal 方法,并将封送后的 ITaskLauncher 接口指针传递给工作线程。由于 Fire_TaskCompletedInternal 方法不应被 COM 客户端直接调用,因此我们将使其隐藏,尽管它仍将保留在 ITaskLauncher 虚函数表中。

因此,在类视图中,找到 ITaskLauncher 接口,在上下文菜单中选择“添加”->“添加方法...”。将方法名称填写为 Fire_TaskCompletedInternal,添加具有 [in] 方向和 BSTR 类型的“name”参数。单击“下一步”按钮或“IDL 属性”页面。选中“hidden”复选框,然后单击“完成”。

修改 TaskInfo 结构,并添加 LPSTREAM marshalledInterface 成员变量。

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

找到 CTaskLauncher::StartTask 方法,并将其替换为以下代码。

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;
}

threadFunc 函数定义插入到 StartTask 方法的正前方。

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;
}

最后,找到 CTaskLauncher::Fire_TaskCompleteInternal 方法定义,并使其如下所示。

STDMETHODIMP CTaskLauncher::Fire_TaskCompletedInternal(BSTR name)
{
    Fire_TaskCompleted(name);

    return S_OK;
}

重新生成解决方案,然后再次尝试运行 Python 客户端。输出应如下所示:

# 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

清单

  • 为自定义接口设置 oleautomation 属性。这对于 Python、VBA 等脚本语言通过 typelib 进行后期绑定是必需的。
  • 在调用任何 COM 相关函数或接口函数之前,在每个线程中调用一次 CoInitializeEx。不要忘记在线程结束前调用 CoUninitialize
  • 您应该在线程之间封送接口指针。有关更多详细信息,请参阅 CoMarshalInterThreadInterfaceInStream/ CoGetInterfaceAndReleaseStream。另一种技术是“全局接口表”。有关更多详细信息,请参阅 何时使用全局接口表 文章。

下载本文代码

参考文献

© . All rights reserved.