ctypes:在COM Server中返回指针时的内存管理

时间:2013-12-12 14:34:47

标签: python pointers memory ctypes comtypes

自从从Win XP过渡到Server 2008以来,我遇到了一些奇怪的问题。我试图解决这些问题,但是,在返回指向结构的指针时,我仍然不确定内存管理是如何通过COM工作的。

假设我需要在用Python编写的COM服务器的函数中返回POINTER(MyStruct)类型的东西。在函数中,我创建了对象:

struct = MyStruct()
struct.field = 4

然后我回来了

return POINTER(MyStruct)(struct)

我是否必须保留对struct的python引用,以避免在编组发生之前释放服务器上的内存?如果我真的这样做,COM客户端崩溃。如果不这样做,有时这些结构中包含的数据在客户端接收后会被破坏。

我想我在这里做错了但是我无法通过阅读ctypescomtypes文档来弄清楚是什么。

EDIT1: 我刚发现this帖子似乎与之相关,因为结构的内容也被覆盖了。答案表明了期待的内容,即内存被“意外”释放。但是,答案并没有解释如何来解决这个问题。

正如我之前解释过的,如果我保留像

这样的引用
self.struct = struct

客户端崩溃。

EDIT2: 我正在请求eryksun请求COM接口定义和python方法签名。在我的问题中,我已经简化了一些问题,以便更容易获得概述。实际方法返回一个指向结构数组的指针:

IOPCItemMgt::ValidateItems
HRESULT ValidateItems(
[in] DWORD dwCount,
[in, size_is(dwCount)] OPCITEMDEF * pItemArray,
[in] BOOL bBlobUpdate,
[out, size_is(,dwCount)] OPCITEMRESULT ** ppValidationResults,
[out, size_is(,dwCount)] HRESULT ** ppErrors
);

关于指针**上的指针,接口规范说:

  

您将注意IDL中使用的语法size_is(,dwCount)以及指针指针。这表示返回的项是指向指示类型的实际数组的指针,而不是指向指向所指示类型的项的指针数组的指针。

这是python方法:

def ValidateItems(self, count, p_item_array, update_blob):

假设有一个名为OpcDa.tagOPCITEMRESULT()的ctypes结构。

我通过调用

创建了这些结构的数组
validation_results = (OpcDa.tagOPCITEMRESULT * count)()
errors = (HRESULT * count)()

在设置所有数组元素的字段后,我返回如下指针:

return POINTER(OpcDa.tagOPCITEMRESULT)(add_results), POINTER(HRESULT)(errors)

EDIT3: 我想总结一下这篇文章的评论以及到目前为止我发现的内容:

正如eryksun所说,简化的return语句至少会导致相同的行为和问题,但更具可读性:

return add_results, errors

与此同时,我做了一些实验。我尝试了eryksun建议的低级实现。

def ValidateItems(self, this, count, p_item_array, update_blob, p_validation_results, p_errors):
(...)
    p_validation_results[0].contents = (OpcDa.tagOPCITEMRESULT*count)()
    p_errors[0].contents = (HRESULT*count)()
    (...)
    for index (..)
        val_result = OpcDa.tagOPCITEMRESULT()
        p_validation_results[0][index] = val_result
        p_validation_results[0][index].hServer = server_item_handle

在我填充数组元素的循环中,我用新元素覆盖了内容,只是因为我很绝望。有趣的是,使用这段代码我能够看到服务器上已经存在内存损坏,而之前的代码只能揭示客户端的损坏。

  • index=0时,hServer被赋予其值。当我检查价值时,没关系。
  • index=1时,但在分配[0][1].hServer之前,[0][0].hServer的值仍然正常。
  • index=1时,但在作业[0][1].hServer = val_result之后,[0][0].hServer的值已经像以前提到的那样被破坏了。
  • index=2 分配[0][2].hServer = val_result后,[0][1].hServer的值很好

这意味着在为第二个元素分配新值后,只有第一个数组元素的hServer被部分覆盖。

我假设第一个循环的val_result的内存被释放并以某种方式被覆盖,尽管我认为赋值some_pointer[0] = new_value实际上将内容复制为this post建议。

但是现在,它变得更加奇怪了。当我记得python列表中的val_result时,例如

self.items.append(val_result)

服务器端的损坏消失。但是,我再次在客户端上获得COMError

问题是,这个神秘的COMError不是由服务器中的(可捕获的)错误引起的。一切似乎都很好。所以必须由COM编组的内部引起。

有关如何处理或获取有关COM内部情况的更多见解的任何建议?

1 个答案:

答案 0 :(得分:2)

哇,当我从微软论坛得到一个an older thread的链接时,我几乎要辞职,这指向了我正确的方向。实际上,线程启动器通过使用SAFEARRAY解决了它的问题,但是如果我没有错,那么当请求指向数组的指针时,你不能简单地返回SAFEARRAY。你必须改变我不能改变的界面。至少它对我不起作用。

然而,他的代码片段中有一行让我思考:

*ppServerId = (long*) CoTaskMemAlloc((*pSize) * sizeof(long));

CoTaskMemAlloc的显式调用似乎与实际在客户端崩溃的方法CoTaskMemFree相对应。所以我在想,当第三方C ++软件中的释放例程(应该对很多客户正常工作)崩溃时,可能是分配错误或丢失。

所以我在comtypes来源中搜索CoTaskMemAlloc的来电,但除了一个测试用例外,还可以找到。

所以我通过明确地为所有内存中的任何来释放所有内存,这是通过指针而不是通过值从COM方法返回的。这包括字符串(c_wchar_p),结构,数组和结构内部的字符串。

所以我写了这三个辅助方法(实际上可以简化一点):

def make_com_array(item_type, item_count):
    array_mem = windll.ole32.CoTaskMemAlloc(sizeof(item_type) * item_count)
    return cast(array_mem, POINTER(item_type))

def make_com_string(text, typ=c_wchar_p):
    text = unicode(text)
    size = (len(text) + 1) * sizeof(c_wchar)
    mem = windll.ole32.CoTaskMemAlloc(size)
    ptr = cast(mem, typ)
    memmove(mem, text, size)
    return ptr

def make_com_object(object_type):
    size = sizeof(object_type)
    mem = windll.ole32.CoTaskMemAlloc(size)
    ptr = cast(mem, POINTER(object_type))
    return ptr

然后,无论我在哪里都返回任何指针,我都会使用它。

所以ValidateItems方法现在看起来像这样:

def ValidateItems(self, count, p_item_array, update_blob):

    validation_results = make_com_array(OpcDa.tagOPCITEMRESULT, count)
    errors = make_com_array(HRESULT, count)

    (...)
    for index (...):
        validation_results[index].hServer = server_item_handle

    return add_results, errors