首页 > 解决方案 > SWIG 中的垃圾收集和自定义 getter

问题描述

我不是作者,但我使用的一个公共软件包似乎正在泄漏内存(Github issue)。我试图弄清楚如何修补它以使其正常工作。

为了缩小问题范围,有一个结构体,调用它xxx_t。First%extend用于使结构的成员在 Python 中可用:

%extend xxx_t {
    char *surface;
}

然后是一个自定义的 getter。它在这里究竟做了什么并不重要,只是它new用来创建一个char*.

%{
char* xxx_t_surface_get(xxx *n) {
  char *s = new char [n->length + 1];
  memcpy (s, n->surface, n->length);
  s[n->length] = '\0';
  return s;
}
%}

目前代码有这一行来处理垃圾收集:

%newobject surface;

这似乎没有按预期工作。%newobject xxx_t::surface;也不起作用。如果我用它替换它%newobject xxx_t_surface_get;不起作用,因为 getter 函数被转义(在内部%{ ... %})。

告诉 SWIGchar*以便释放它的正确方法是什么?

标签: pythonswig

解决方案


在开始之前,值得指出一件事:因为您返回char*它最终会使用 SWIG 的普通字符串类型映射来生成 Python 字符串。

话虽如此,让我们了解当前生成的代码是什么样的。我们可以从以下 SWIG 接口定义开始我们的调查以进行试验:

%module test 

%inline %{
  struct foobar {
  };
%}

%extend foobar {
  char *surface;
}

如果我们通过 SWIG 运行类似的东西,我们将看到一个生成的函数来包装您的_surface_get代码,如下所示:

SWIGINTERN PyObject *_wrap_foobar_surface_get(PyObject *SWIGUNUSEDPARM(self), PyObject *args) {
  PyObject *resultobj = 0;
  foobar *arg1 = (foobar *) 0 ;
  void *argp1 = 0 ;
  int res1 = 0 ;
  PyObject * obj0 = 0 ;
  char *result = 0 ;

  if (!PyArg_ParseTuple(args,(char *)"O:foobar_surface_get",&obj0)) SWIG_fail;
  res1 = SWIG_ConvertPtr(obj0, &argp1,SWIGTYPE_p_foobar, 0 |  0 );
  if (!SWIG_IsOK(res1)) {
    SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "foobar_surface_get" "', argument " "1"" of type '" "foobar *""'"); 
  }
  arg1 = reinterpret_cast< foobar * >(argp1);
  result = (char *)foobar_surface_get(arg1);
  resultobj = SWIG_FromCharPtr((const char *)result);
  /* result is never used again from here onwards */
  return resultobj;
fail:
  return NULL;
}

然而,这里要注意的是,当这个包装器返回时,调用你的 getter 的结果会丢失。也就是说,它甚至与返回的 Python 字符串对象的生命周期无关。

所以有几种方法可以解决这个问题:

  • 一种选择是确保生成的包装器delete[]在调用 getter 的结果SWIG_FromCharPtr发生后调用。这正是%newobject在这种情况下所做的。(见下文)。
  • 另一种选择是在调用之间保留分配的缓冲区,可能在某些线程本地存储中并跟踪大小以最小化分配
  • 或者,我们可以使用某种基于 RAII 的对象来拥有临时缓冲区并确保它被删除。(如果我们愿意,我们可以做一些整洁的事情operator void*)。

如果我们改变我们的界面添加%newobject如下:

%module test 

%inline %{
  struct foobar {
  };
%}

%newobject surface;

%extend foobar {
  char *surface;
}

然后我们看到我们生成的代码现在看起来像这样:

  // ....
  result = (char *)foobar_surface_get(arg1);
  resultobj = SWIG_FromCharPtr((const char *)result);
  delete[] result;

我们也可以在来自 github 的真实代码中看到这一点,所以这不是您要寻找的错误。


通常对于 C++,我倾向于 RAII 选项。碰巧,从 SWIG 的角度和 C++ 的角度来看,有一种巧妙的方法可以做到这一点:std::string. 因此,我们可以通过执行以下操作以简单干净的方式修复您的泄漏:

%include <std_string.i> /* If you don't already have this... */

%extend xxx_t {
    std::string surface;
}

%{
std::string xxx_t_surface_get(xxx *n) {
  return std::string(n->surface, n->length);
}
%}

(不过,您也需要更改 setter 以匹配,除非您将其设为 const 所以没有 setter)

不过,关于这一点的事情是它仍然为相同的输出进行两组分配。首先,std::string对象进行一次分配,然后对 Python 字符串对象进行分配。这就是缓冲区已经存在于 C++ 中的所有内容。因此,虽然这种更改足以解决泄漏问题并且是正确的,但您还可以进一步编写一个减少重复复制的版本:

%extend xxx_t {
    PyObject *surface;
}

%{
PyObject *xxx_t_surface_get(xxx *n) {
  return SWIG_FromCharPtrAndSize(n->surface, n->length);
}
%}

推荐阅读