Обратный вызов Python3 + ctypes вызывает утечку памяти в простом примере - PullRequest
0 голосов
/ 24 октября 2018

Работая над сложной программой, комбинирующей код Python 3 и код C ++ с использованием ctypes, я обнаружил утечку памяти, которую можно легко воспроизвести с помощью приведенного ниже примера.

Мой код C ++ создает объект Python с использованиемфункция обратного вызова.Затем он вызывает другой обратный вызов для объекта Python, который просто возвращает свой аргумент.Второй обратный вызов приводит к увеличению счетчика ссылок объекта.В результате объект никогда не получает мусор.

Это код Python (файл bug.py):

import ctypes

CreateObjectCallback = ctypes.CFUNCTYPE( ctypes.py_object )
NoopCallback = ctypes.CFUNCTYPE( ctypes.py_object, ctypes.py_object )

lib = ctypes.cdll.LoadLibrary("./libbug.so")

lib.test.restype = ctypes.py_object
lib.test.argtypes = [ CreateObjectCallback, NoopCallback ]

class Foo:
    def __del__(self):
        print("garbage collect foo");

def create():
    return Foo()

def noop(object):
    return object

lib.test(CreateObjectCallback(create), NoopCallback(noop))

Это код C ++ (файл bug.cpp):

#include <python3.6m/Python.h>
#include <iostream>
#include <assert.h>

extern "C" {

  typedef void *(*CreateObjectCallback)();
  typedef void *(*NoopCallback)(void *arg);

  void *test(CreateObjectCallback create, NoopCallback noop)
  {
    void *object = create();
    std::cerr << "ref cnt = " << ((PyObject*)(object))->ob_refcnt << std::endl;
    object = noop(object);
    std::cerr << "ref cnt = " << ((PyObject*)(object))->ob_refcnt << std::endl;
    return object;
  }
}

А вот команды, которые я использую для компиляции и запуска:

g++ -O3 -W -Wextra -Wno-return-type -Wall -Werror -fPIC -MMD   -c -o bug.o bug.cpp
g++ -shared -Wl,-soname,libbug.so -o libbug.so bug.o 
python3 bug.py

Вывод:

ref cnt = 1
ref cnt = 2

Другими словами, вызовдля функции noop неправильно увеличивается счетчик ссылок, и объект Foo не подвергается сборке мусора.Без вызова функции noop объект Foo является сборщиком мусора.Ожидаемый результат:

ref cnt = 1
ref cnt = 1
garbage collect foo

Это известная проблема?Кто-нибудь знает обходной путь или решение?Это вызвано ошибкой в ​​ctypes?

1 Ответ

0 голосов
/ 24 октября 2018

Вы передаете объекты Python.Один из ваших объектов передается в ваш C-код и не передается, поэтому вы несете ответственность за этот счетчик ссылок.Вот кое-что, что работает, но я изменил void* на PyObject*, поскольку это то, чем они являются:

#include <Python.h>
#include <iostream>
#include <assert.h>

extern "C" {

  typedef PyObject* (*CreateObjectCallback)();
  typedef PyObject* (*NoopCallback)(PyObject* arg);

  __declspec(dllexport) PyObject* test(CreateObjectCallback create, NoopCallback noop)
  {
    // Create the object, with one reference.
    PyObject* object = create();
    std::cerr << "ref cnt = " << object->ob_refcnt << std::endl;

    // Passing object back to Python increments its reference count
    // because the parameter of the function is a new reference.
    // That python function returns an object (the same one), but
    // now you own deleting the reference.
    PyObject* object2 = noop(object);
    Py_DECREF(object2);

    std::cerr << "ref cnt = " << object->ob_refcnt << std::endl;

    // Your return the created object, but now that Python knows
    // it is a Python object instead of void*, it will decref it.
    return object;
  }
}

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

Я также изменяюсь на ctypes.PyDLL.Это не освобождает GIL при вызове кода C.Поскольку вы передаете объекты Python, это кажется хорошей идеей.

import ctypes

CreateObjectCallback = ctypes.CFUNCTYPE( ctypes.py_object )
NoopCallback = ctypes.CFUNCTYPE( ctypes.py_object, ctypes.py_object )

lib = ctypes.PyDLL('test')

lib.test.restype = ctypes.py_object
lib.test.argtypes = [ CreateObjectCallback, NoopCallback ]

class Foo:
    def __del__(self):
        print("garbage collect foo");

@CreateObjectCallback
def create():
    return Foo()

@NoopCallback
def noop(object):
    return object

lib.test(create,noop)

Вывод:

ref cnt = 1
ref cnt = 1
garbage collect foo
...