如何反省 win32com 包装器?

How to introspect win32com wrapper?

我有一个记录光谱数据并由第 3 方应用程序控制的设备。出于自动化目的,我想使用应用程序的 COM 接口来检索 Python 中的数据。由于 Python 中没有使用 API 的正确文档,我从不同的网络资源中收集了以下代码,成功获得了第一帧:

comtypes.client.GetModule(('{1A762221-D8BA-11CF-AFC2-508201C10000}', 3, 11))
import comtypes.gen.WINX32Lib as WinSpecLib
win32com.client.pythoncom.CoInitialize()
doc = win32com.client.Dispatch("WinX32.DocFile")

buffer = ctypes.c_float()
frame = 1
spectrum = doc.GetFrame(frame, buffer)

但是,对GetFrame的调用与厂商提供的Visual Basic中的定义不一致:

Sub GetFrame(frame As Integer, buffer As Variant)

GetFrame copies the data from a document into a Visual Basic array. If buffer is an empty Variant, GetFrame creates an array of the proper size and data type and sets buffer to point to it before copying the data.

这意味着在 Visual Basic 中变量 buffer 填充了数据而函数 GetFrame 没有 return 值,而在 Python 中 buffer 保持不变,但函数 GetFrame 执行 return 实际数据。

如果我没有观察到我的程序随机崩溃并抛出 MemoryError 并因此在代码的这个位置指示内存泄漏,我不会关心这些细微之处。所以我怀疑每次调用 GetFrame 都会为缓冲区分配一些内存但从未释放,因为 win32com 以某种方式搞砸了 API 包装。

这个推理引出了我的实际问题:我如何反省包装器并理解它的作用?到目前为止,我找不到任何提示 win32com 生成的代码存储在任何文件中,但也许我只是没有找到正确的位置。

在 IPython 中,我也尝试使用 doc.GetFrame?? 获取信息,但它没有 return 任何实现:

Signature: doc.GetFrame(frame=<PyOleMissing object at 0x06F20BC8>, FrameVariant=<PyOleMissing object at 0x06F20BC8>)
Docstring: <no docstring>
File:      c:\programming\python\src\<comobject winx32.docfile>
Type:      method

我还可以尝试获取有关 API 包装器的更多信息吗?

多方尝试,我终于找到了解决问题的方法。第一个重要的认识是发现调用 EnsureDispatch 而不是 Dispatch 可以让我访问由 win32com.

生成的包装器
>>> import win32com.client
>>> doc = win32com.client.gencache.EnsureDispatch ("WinX32.DocFile")
>>> print(doc.GetFrame.__module__)
'win32com.gen_py.1A762221-D8BA-11CF-AFC2-508201C10000x0x3x12.IDocFile4'

在我的例子中,相应的文件位于以下文件夹中:

C:\WinPython\WinPython-32bit-3.5.2.2\python-3.5.2\Lib\site-packages\win32com\gen_pyA762221-D8BA-11CF-AFC2-508201C10000x0x3x12

GetFrame 的实现如下所示。

def GetFrame(self, frame=defaultNamedNotOptArg, FrameVariant=defaultNamedNotOptArg):
    'Get Frame Data'
    return self._ApplyTypes_(10, 1, (24, 0), ((2, 1), (16396, 3)), 'GetFrame', None, frame, FrameVariant)

所以魔法就在方法 _ApplyTypes_ 中。此方法本身在 win32com\client\__init__.

中定义
def _ApplyTypes_(self, dispid, wFlags, retType, argTypes, user, resultCLSID, *args):
    return self._get_good_object_(
        self._oleobj_.InvokeTypes(dispid, 0, wFlags, retType, argTypes, *args),
        user, resultCLSID)

我们可以看到基本都传给了InvokeTypes。根据this message on the Python-win32 mailing list, InvokeTypes is very similar to Invoke, which in turn is a re-implementation of IDispatch::Invoke. The source code of the C++ implementation integrated in Python can be found here.

通过这个 C++ 实现还解释了我原来的问题中困扰我的问题:Invoke 的 Python 版本明确地将 byref 参数转换为 return 值。因此,至少,应该不会有我一开始怀疑的内存泄漏。

现在我们可以了解参数类型的哪些内容?必要的信息存储在元组 ((2, 1), (16396, 3)) 中。我们有两个参数,第一个是仅输入参数(由 1 表示),而第二个是输入和输出参数(由 3 = 1 | 2 表示)。根据预期的 this blog entry, the respective first numbers tell us the kind of Variant 数据类型。

我们可以在this列表中查找,数字的实际含义。第一个参数是带符号的 int16,这是有道理的,因为它指定了帧号。第二个数字含义如下。

16396 = 0x400c = VT_VARIANT | VT_BYREF

documentation 告诉我们,VT_VARIANT 的实际含义。

Either the specified type, or the type of the element or contained field MUST be VARIANT

不是很有启发性,但仍然如此。看来选择传一个ctypes.c_float并不是一个很好的选择。相反,我现在传递一个变体,我可能应该受到 this 讨论的启发。

var = win32com.client.VARIANT(pythoncom.VT_VARIANT | pythoncom.VT_NULL | pythoncom.VT_BYREF, None)
spectrum = doc.GetFrame(frame, var)

进行此更改后,我不再观察到此代码部分的崩溃,因此原始问题已为我解决。