Управление памятью и устранение утечек в C и Python с типами ctypes на примере строк

#python #c #memory-management #memory-leaks #ctypes

Вопрос:

Я пытаюсь исправить утечки в привязке Python на основе ctypes над библиотекой C. Я в замешательстве относительно поведения управления памятью строк и других указателей при работе с типами ctypes.

Насколько мне известно, есть 3 случая, которые мне нужно решить, в основном:

А) Создание памяти в C и возврат a c_char_p в результате выполнения функции в Python.

Б) Создание строки в Python и передача ее в качестве char const* параметра функции C, а не освобождение ее в C: это единственное , что я решил уверенно: я могу просто сделать my_str.encode("utf-8") в a c_char_p , и Python Alloc/GC обрабатывает как выделение, так и освобождение.

C) Создание памяти в Python и сохранение ее в C (например, в виде char* поля в структуре), которое будет освобождено позже (с помощью функции из библиотеки C).

Мои вопросы:

А)

  • является c_char_p ли созданный таким образом второй указатель ? Если да, то что происходит с указателем, выделенным C ?
  • как мы освободим указатель, возвращенный из C ?
  • есть ли разница в поведении между тем, как c_void_p и c_char_p как restype обрабатываются GC ?

C)

  • Каков правильный способ «передать право собственности» на указатель памяти, выделенный в Python, на C ? .encode() явно создает объект Python, который GC’ed.
  • является ли строка, созданная create_string_buffer также Python-GC’ed ?

Ответ №1:

Важно понимать ctypes.c_char_p ctypes , что для этого типа возврата существует специальная обработка, при которой он копирует возвращенную байтовую строку с нулевым окончанием в bytes объект Python и возвращает ее вместо этого. Доступ к исходному указателю теряется, так как фактически полученный тип возвращаемого значения является bytes объектом, а не указателем. Обратите внимание, что то же самое происходит с c_wchar_p тем, что преобразуется в str объект.

Для A) , решение заключается в использовании ctypes.POINTER(ctypes.c_char_p) , и преобразование не произойдет, но при необходимости может быть выполнено вручную. Возвращаемое значение может быть передано обратно в функцию C, которая освобождает память.

Потому B) что у вас есть правильная идея. Просто передайте bytes объект функциям C, которые принимают const char* , и Python будет управлять памятью. Если функции C требуется, чтобы объект был действителен после вызова функции (возможно, для последующего обратного вызова), обязательно сохраняйте ссылку на объект до тех пор, пока он больше не понадобится.

Для C) Вы можете использовать любую из этих техник. Если C будет управлять выделением памяти в функции C, не используйте c_char_p ее для ее получения, а освободите ее в функции C; в противном случае используйте create_string_buffer() Python для управления и сохранения ссылки до тех пор, пока это необходимо для кода C.

Пример всех техник:

тест.c

 #include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define API __declspec(dllexport)

API char* funca() {
    char* p = malloc(6);
    strcpy_s(p, 6, "hello");
    return p;
}

API void funca_free(void* p) {
    *(char*)p = 'x'; // overwrite just to prove accessing a freed buffer
    free(p);         // that hasn't been re-used yet.
}

API void funcb(const char* p) {
    printf("B: %sn", p);
}

static char* store = NULL;

API void funcc(char* p) {
    store = p;
}

API char* funcc_get() {
    return store;
}
 

test.py

 import ctypes as ct

dll = ct.CDLL('./test')
dll.funca.argtypes = ()
dll.funca.restype = ct.POINTER(ct.c_char) # NOT c_char_p, char* is lost
dll.funca_free.argtypes = ct.c_void_p,
dll.funca_free.restype = None
dll.funcb.argtypes = ct.c_char_p,
dll.funcb.restype = None
dll.funcc.argtypes = ct.c_char_p,
dll.funcc.restype = None
dll.funcc_get.argtypes = ()
dll.funcc_get.restype = ct.POINTER(ct.c_char) # NOT c_char_p

def funca_wrap():
    p = dll.funca() # C-managed
    s = ct.cast(p, ct.c_char_p).value # Make a copy as a Python byte string
    dll.funca_free(p) # Free by C as needed
    return s

print('A:', funca_wrap()) # Use a wrapper to capture string and free buffer

dll.funcb(b'input') # Python-managed and freed

s = ct.create_string_buffer(b'python') # Python-managed
dll.funcc(s)
p = dll.funcc_get()
print('C(py-alloc):', ct.cast(p,ct.c_char_p).value)
del s # Python-freed
print('C(py-freed):', ct.cast(p,ct.c_char_p).value) # access freed memory (could crash)

s = dll.funca() # C-managed (NOT c_char_p)
dll.funcc(s)
p = dll.funcc_get()
print('C(C-alloc):', ct.cast(p,ct.c_char_p).value)
dll.funca_free(s) # C-freed
print('C(C-freed):', ct.cast(p,ct.c_char_p).value) # access freed memory (could crash)
 

Выход:

 A: b'hello'
B: input
C(py-alloc): b'python'
C(py-freed): b'x88xc9?xf1x8ax01'      # note freed memory reused in my case
C(C-alloc): b'hello'
C(C-freed): b'xello'                       # freed memory wasn't reused yet
 

Комментарии:

1. Очень полезный ответ, спасибо! Однако у меня есть вопрос: учитывая особенности c_char_p , если я хочу вернуть a char* по ссылке (под которой я подразумеваю , что моя функция C подписана int my_func(char* *address_of_result); и вызывается с char* str = NULL; status = my_func(amp;str); помощью ), как мне следует действовать в соответствии с вашей схемой ? Что-то вроде POINTER(POINTER(c_char)) , или скорее POINTER(c_char_p) ?

2. @TristanDuquesne Последнее работает. Поскольку вы должны создать s = c_char_p() и передать его по ссылке (например, по ссылке(ссылкам)) на функцию, c_char_p при возврате функции у вас будет фактический обновленный объект s , который позже может быть освобожден при необходимости. s.value вернет char* данные в виде bytes объекта.

3. Насколько я понимаю, это c_char_p никогда не следует использовать для чего-либо, что исходит из C, кроме строкового литерала, так как это обычно приводит к потере памяти. В любом случае, приятно это знать.

4. @TristanDuquesne Если значение является строкой, заканчивающейся нулем, и вам не нужно управлять указателем для C, используйте его; в противном случае не делайте этого.

Ответ №2:

Это адаптация обмена, который я провел на сервере Python discord с кем-то, кто ответил на вопросы:

  • является c_char_p ли созданный таким образом второй указатель ? Если да, то что происходит с указателем, выделенным C ?

Да, это так. Это c_char_p PyObject* указатель, который находится в куче, и в нем хранится исходный char* указатель C внутри него.

  • как мы освободим указатель, возвращенный из C ?

Передайте c_char_p на free() использование ctypes

  • есть ли разница в поведении между тем, как c_void_p и c_char_p обрабатываются как restype GC ?

Нет, в обоих случаях, когда c_void_p or c_char_p собирает мусор, он ничего не делает, т. Е. Не освобождает исходный объект C, на который был указан.

  • Каков правильный способ «передать право собственности» на указатель памяти, выделенный в Python, на C ?

Обычно вы этого не делаете — память, созданная Python, обычно освобождается Python, память, созданная C, обычно освобождается C. Если вам нужно передать что-то из одного домена в другой, лучше всего скопировать это. Можно заставить сторону C сохранять ссылку Python на объект Python с помощью Python C API, но это, вероятно, не лучшая идея.

  • является ли строка, созданная create_string_buffer также Python-GC’ed ?

В этом случае, когда объект Python обрабатывается GC’d, он также освобождает память C. A c_char_p знает, владеет ли он памятью, на которую он указывает, или нет, и когда он проходит проверку подлинности, он освобождает память C, если она ему принадлежит.

========

Поскольку моя (я = спрашивающий) цель состоит в том, чтобы иметь как можно меньше (нет) ссылок на типы ctypes для конечного пользователя Python, учитывая ответы выше, я бы сделал следующее:

  • когда я указываю a char* в C в функции char* do_c_func() , я возвращаю ее как c_char_p тип restype в Python. В моем Python у меня есть функция-оболочка do_py_func() , которая вызывает do_c_func() , получает этот c_char_p вызов my_c_char_p и сохраняет его содержимое в строке Python , через что-то вроде s = str(my_c_char_p) . Затем я вызываю ctypes.free(my_c_char_p) : это освобождает malloced char* , и в конце функции, уменьшающей последний счетчик ссылок, в конечном итоге собирается мусор my_c_char_p .
  • Я создаю функцию преобразования python_str_to_c_str(py_str) , которая принимает обычную строку python и возвращает stdc.strdup(py_str.encode('utf-8')) stdc.strdup.restype = c_char_p ), тем самым предоставляя новую char* для C (которую Python распознает как созданную в C). Я использую эту функцию преобразования везде, где мне нужно передать строку из Python в C. Это делает так , что буфер , созданный my_string.encode('utf-8')) Python, и c_char_p возвращаемый stdc.strdup им, являются мусором, собранным Python, но char* сам по себе это не так.