ctypes: управление памятью при возврате указателей в COM-сервере

У меня возникли некоторые странные проблемы после перехода с Win XP на Server 2008. Я пытался исправить эти проблемы, однако я до сих пор не уверен, как работает управление памятью через COM при возврате указателей на структуры.

Допустим, мне нужно вернуть что-то типа POINTER(MyStruct) в функцию COM-сервера, написанную на Python. Внутри функции я создаю объект:

struct = MyStruct()
struct.field = 4

тогда я вернусь

return POINTER(MyStruct)(struct)

Должен ли я сохранять ссылку python на struct, чтобы избежать освобождения памяти на сервере до того, как произойдет сортировка? Если я действительно это сделаю, COM-клиент выйдет из строя. Если я этого не сделаю, иногда данные, содержащиеся в этих структурах, будут повреждены после приема на клиенте.

Я предполагаю, что я делаю что-то не так, но я не мог понять, что, прочитав ctypes и comtypes.

EDIT1: я только что нашел этот сообщение, которое, кажется, связано, так как содержимое структуры также перезаписывается. Ответ также предполагает то, что ожидалось, а именно то, что память освобождается «случайно». Однако в ответе не объясняется, как решить эту проблему.

Как я объяснял ранее, если я сохраняю ссылку, например

self.struct = struct

клиент вылетает.

EDIT2: я размещаю определение COM-интерфейса и сигнатуру метода python по запросу eryksun. В своем вопросе я немного упростил проблему, чтобы упростить обзор. Фактический метод возвращает указатель на массив структур:

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

Относительно указателя на указатель ** в спецификации интерфейса сказано:

Обратите внимание на синтаксис size_is(,dwCount) в IDL, используемый в сочетании с указателями на указатели. Это указывает на то, что возвращаемый элемент является указателем на фактический массив указанного типа, а не указателем на массив указателей на элементы указанного типа.

И это метод python:

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

Предположим, что существует структура ctypes с именем OpcDa.tagOPCITEMRESULT().

Я создаю массив этих структур, вызывая

validation_results = (OpcDa.tagOPCITEMRESULT * count)()
errors = (HRESULT * count)()

и после установки полей всех элементов массива возвращаю указатели вот так:

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

EDIT3: я хочу суммировать комментарии к этому сообщению и то, что я узнал на данный момент:

Как предложил eryksun, упрощенный оператор возврата, по крайней мере, приводит к тому же поведению и проблемам, но более читаем:

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

В цикле, где я заполняю элементы массива, я перезаписываю содержимое новым элементом просто потому, что я был в отчаянии. Интересно, что с помощью этого кода я смог увидеть повреждение памяти уже на сервере, тогда как предыдущий код выявляет повреждение только на стороне клиента.

  • When index=0, hServer gets assigned its value. When I check the value, it's fine.
  • When index=1, but before the assignment of [0][1].hServer, the value of [0][0].hServer is still fine.
  • When index=1, but after the assignment [0][1].hServer = val_result, the value of [0][0].hServer has been corrupted in the same way as mentioned before.
  • When index=2 and after the assignment [0][2].hServer = val_result, the value of [0][1].hServer is fine

Это означает, что hServer только первого элемента массива частично перезаписывается после того, как второму элементу будет присвоено новое значение.

Я предполагаю, что память для val_result первого цикла каким-то образом освобождается и перезаписывается, хотя я думал, что назначение some_pointer[0] = new_value фактически копирует содержимое как этот пост предполагает.

Но теперь это становится еще более странным. Когда я помню val_result в списке python, например, например.

self.items.append(val_result)

повреждение на стороне сервера исчезло. Но я снова получаю COMError на клиенте.

Проблема в том, что эта загадочная ошибка COMError не вызвана (уловимой) ошибкой на сервере. Кажется, все работает нормально. Это должно быть вызвано внутренностями смешивания COM.

Любые предложения, как действовать или получить больше информации о том, что происходит внутри COM?


person jaw    schedule 12.12.2013    source источник
comment
POINTER(MyStruct)(struct) содержит ссылку на struct в атрибуте _objects указателя. Ошибка в связанном вопросе заключается в том, что объект bytes используется в качестве указателя без сохранения ссылки. Он должен использовать DISK_GEOMETRY_EX.from_buffer_copy.   -  person Eryk Sun    schedule 13.12.2013
comment
@eryksun Если я верну этот указатель с COM-сервера, что произойдет с struct? Освобождается ли память после возврата метода python? Другими словами, нужно ли сохранять ссылку на объект или достаточно вернуть указатель?   -  person jaw    schedule 14.12.2013
comment
@eryksun Я добавил информацию об интерфейсе и реализации метода в первоначальный вопрос. PS: Как вы могли догадаться, я не использую COM, потому что это так весело ;)   -  person jaw    schedule 15.12.2013
comment
Да, Comtypes делает это. Например. если вам нужно вернуть указатель на целое число, вы просто возвращаете целое число, например return 42. В противном случае вы получите ошибку типа.   -  person jaw    schedule 16.12.2013
comment
@eryksun Интересно, что return add_results, errors работает с точки зрения того, что не выдает ошибку типа. Однако он ведет себя аналогично POINTER(tagOPCITEMRESULT)(add_results), POINTER(HRESULT)(errors), поскольку данные, которые получает клиент, также частично перезаписываются. Мы также попробовали реализацию с низким рычагом. Тип выходных аргументов LP_LP_tagOPCITEMRESULT. Когда мы разыменовываем второй уровень p_validation_results[0][0], мы получаем ошибку указателя NULL на стороне сервера. Использование p_validation_results[index] = POINTER(tagOPCITEMRESULT)(tagOPCITEMRESULT()) приводит к ошибке на стороне клиента.   -  person jaw    schedule 16.12.2013
comment
Я попробовал validation_results = (OpcDa.tagOPCITEMRESULT*count)(), затем заполнил поля validation_results[index].hServer = server_item_handle и установил массив, как вы предложили p_validation_results[0] = validation_results. К сожалению, это приводит к той же проблеме. hServer первого элемента массива частично перезаписывается.   -  person jaw    schedule 16.12.2013
comment
Хорошо, это просто вручную делать то, что высокий уровень return validation_results, errors уже делает для вас автоматически.   -  person Eryk Sun    schedule 16.12.2013
comment
Кстати, использование p_validation_results.contents = cast(validation_results, POINTER(OpcDa.tagOPCITEMRESULT)) приводит к ошибке доступа к нулевому указателю на стороне клиента, поэтому это явно не эквивалентно использованию оператора нижнего индекса.   -  person jaw    schedule 16.12.2013
comment
Это сообщение кажется связанным к нашему интерфейсу (хотя и с точки зрения клиента) и предлагает использовать 1-индексированные массивы. Мы попытались создать безопасный массив с индексом от 1. Однако при использовании p_validation_results[0] = _midlSAFEARRAY(OpcDa.tagOPCITEMRESULT).from_param(validation_results) мы получаем ошибку типа Cannot create SAFEARRAY type VT_RECORD without IRecordInfo.. У вас есть опыт или идеи по этому поводу?   -  person jaw    schedule 16.12.2013
comment
Извините, забудьте о SAFEARRAY. Это часть другого (автоматизированного) интерфейса. Мы перепробовали множество подходов, и все они сводятся к следующему: 1. Если мы возвращаем указатель на массив, содержимое частично перезаписывается. Мы смогли увидеть эти изменения даже на стороне сервера 2. Если мы сохраним ссылку python на массив, содержимое останется неизменным (по крайней мере, на стороне сервера), сервер не выдаст исключение, а клиент получит ошибка COM. Как мы можем узнать, что вызывает ошибку?   -  person jaw    schedule 16.12.2013


Ответы (1)


Вау, я уже почти собирался уйти в отставку, когда получил ответ с форума Microsoft со ссылкой на старая тема, которая указала мне правильное направление . На самом деле, создатель потока решил свою проблему, используя 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
person jaw    schedule 18.12.2013