#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
, если я хочу вернуть achar*
по ссылке (под которой я подразумеваю , что моя функция 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)
: это освобождает mallocedchar*
, и в конце функции, уменьшающей последний счетчик ссылок, в конечном итоге собирается мусор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*
сам по себе это не так.