【问题标题】:Python3 + ctypes callback causes memory leak in simple examplePython3 + ctypes回调在简单示例中导致内存泄漏
【发布时间】:2019-03-28 18:13:06
【问题描述】:

在处理一个使用 ctypes 结合 Python 3 代码和 C++ 代码的复杂程序时,我发现了一个内存泄漏,可以通过下面的精简示例轻松重现。

我的 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 中的错误引起的吗?

【问题讨论】:

    标签: python c++ memory-leaks garbage-collection ctypes


    【解决方案1】:

    您正在传递 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。这在调用 C 代码时不会释放 GIL。由于您正在传递 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
    

    【讨论】:

    • 这正是我一直在寻找的答案,甚至更多关于 PyDLL 和装饰器的信息;谢谢。是否有任何关于 ctypes.py_object 如何处理引用计数的公共文档?例如,对我来说,将 Python 对象作为参数传递给回调会永久增加引用计数,这对我来说并不明显。我希望从回调中返回会再次减少引用计数。
    • @ygramoel lib.test() 创建了一个对象(ref1),noop 被传递给了该对象(ref2...参数名称引用它)。 noop 将引用返回给调用者。调用者负责在对象完成后对它进行 deref。 noop 是由 lib.test 从 C 中调用的,所以 lib.test 必须递减它。 lib.test 将它创建的对象返回给 Python。 Python 代码没有将其分配给变量,因此 Python 减少了引用计数。如果 Python 代码将其分配给一个变量,它将保持引用计数,并且 Foo 不会被垃圾回收。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-26
    • 2012-01-21
    • 2019-08-26
    • 2013-06-15
    • 2020-06-28
    • 2021-08-11
    • 1970-01-01
    相关资源
    最近更新 更多