使用SWIG包装包含const char *的结构而不会发生内存泄漏

时间:2019-11-26 13:39:26

标签: swig

我正在尝试使用SWIG封装一个预先存在的库接口,该接口期望调用方管理某些const char *值的生存期。

struct Settings {
    const char * log_file;
    int log_level;
};

// The Settings struct and all members only need to be valid for the duration of this call.
int Initialize(const struct Settings* settings);
int DoStuff();
int Deinitialize();

我开始使用SWIG的最基本输入来包装库:

%module lib
%{
#include "lib.h"
%}

%include "lib.h"

这会导致出现关于可能的内存泄漏的SWIG警告:

lib.h(2) : Warning 451: Setting a const char * variable may leak memory.

通过查看lib_wrap.c完全可以理解,SWIG生成的代码会将malloc的缓冲区放入log_file的值中,但从不释放它:


SWIGINTERN PyObject *_wrap_Settings_log_file_set(PyObject *SWIGUNUSEDPARM(self), PyObject *args) {
  PyObject *resultobj = 0;
  struct Settings *arg1 = (struct Settings *) 0 ;
  char *arg2 = (char *) 0 ;
  void *argp1 = 0 ;
  int res1 = 0 ;
  int res2 ;
  char *buf2 = 0 ;
  int alloc2 = 0 ;
  PyObject *swig_obj[2] ;

  if (!SWIG_Python_UnpackTuple(args, "Settings_log_file_set", 2, 2, swig_obj)) SWIG_fail;
  res1 = SWIG_ConvertPtr(swig_obj[0], &argp1,SWIGTYPE_p_Settings, 0 |  0 );
  if (!SWIG_IsOK(res1)) {
    SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "Settings_log_file_set" "', argument " "1"" of type '" "struct Settings *""'"); 
  }
  arg1 = (struct Settings *)(argp1);
  res2 = SWIG_AsCharPtrAndSize(swig_obj[1], &buf2, NULL, &alloc2);
  if (!SWIG_IsOK(res2)) {
    SWIG_exception_fail(SWIG_ArgError(res2), "in method '" "Settings_log_file_set" "', argument " "2"" of type '" "char const *""'");
  }
  arg2 = (char *)(buf2);
  if (arg2) {
    size_t size = strlen((const char *)((const char *)(arg2))) + 1;
    arg1->log_file = (char const *)(char *)memcpy(malloc((size)*sizeof(char)), arg2, sizeof(char)*(size));
  } else {
    arg1->log_file = 0;
  }
  resultobj = SWIG_Py_Void();
  if (alloc2 == SWIG_NEWOBJ) free((char*)buf2);
  return resultobj;
fail:
  if (alloc2 == SWIG_NEWOBJ) free((char*)buf2);
  return NULL;
}

如果我将log_file的类型更改为char *,则警告消失,并且似乎多次尝试设置log_file的值将不再泄漏内存:

SWIGINTERN PyObject *_wrap_Settings_log_file_set(PyObject *SWIGUNUSEDPARM(self), PyObject *args) {
  PyObject *resultobj = 0;
  struct Settings *arg1 = (struct Settings *) 0 ;
  char *arg2 = (char *) 0 ;
  void *argp1 = 0 ;
  int res1 = 0 ;
  int res2 ;
  char *buf2 = 0 ;
  int alloc2 = 0 ;
  PyObject *swig_obj[2] ;

  if (!SWIG_Python_UnpackTuple(args, "Settings_log_file_set", 2, 2, swig_obj)) SWIG_fail;
  res1 = SWIG_ConvertPtr(swig_obj[0], &argp1,SWIGTYPE_p_Settings, 0 |  0 );
  if (!SWIG_IsOK(res1)) {
    SWIG_exception_fail(SWIG_ArgError(res1), "in method '" "Settings_log_file_set" "', argument " "1"" of type '" "struct Settings *""'"); 
  }
  arg1 = (struct Settings *)(argp1);
  res2 = SWIG_AsCharPtrAndSize(swig_obj[1], &buf2, NULL, &alloc2);
  if (!SWIG_IsOK(res2)) {
    SWIG_exception_fail(SWIG_ArgError(res2), "in method '" "Settings_log_file_set" "', argument " "2"" of type '" "char *""'");
  }
  arg2 = (char *)(buf2);
  if (arg1->log_file) free((char*)arg1->log_file);
  if (arg2) {
    size_t size = strlen((const char *)(arg2)) + 1;
    arg1->log_file = (char *)(char *)memcpy(malloc((size)*sizeof(char)), (const char *)(arg2), sizeof(char)*(size));
  } else {
    arg1->log_file = 0;
  }
  resultobj = SWIG_Py_Void();
  if (alloc2 == SWIG_NEWOBJ) free((char*)buf2);
  return resultobj;
fail:
  if (alloc2 == SWIG_NEWOBJ) free((char*)buf2);
  return NULL;
}

然而,当log_file对象在Python中被垃圾回收时,似乎仍然为Settings分配了内存。

在SWIG中管理char *结构值的生存期的建议方法是什么,以避免这些内存泄漏?

2 个答案:

答案 0 :(得分:2)

您可以告诉SWIG对char*使用log_file语义。不幸的是,似乎无法使用Settings::log_file(所需的memberin不会出现在模式匹配中),因此如果该数据成员名称也用于其他结构中,则可能会发生冲突。具有相同的类型但语义不同。看起来像:

%module lib
%{
#include "lib.h"
%}

%typemap(out) char const *log_file = char *;
%typemap(memberin) char const *log_file = char *;

%extend Settings {
    Settings() {
        Settings* self = new Settings{};
        self->log_file = nullptr;
        self->log_level = 0;
        return self;
    }
    ~Settings() {
        delete[] self->log_file; self->log_file = nullptr;
        delete self;
    }
}

%include "lib.h"

(请注意,在我的情况下,SWIG产生delete[],而不是free()。)

EDIT :添加了一个自定义析构函数,以删除垃圾回收上的log_file内存。 (而且,为了确保未初始化的log_filenullptr,而不是一些随机存储器,构造函数也是一种很好的措施。)这样做是在包装文件中添加内部函数delete_Settings ,它在_wrap_delete_Settings中被调用,在对象销毁时被调用。是的,语法有点奇怪,因为您实际上是在描述Python的__del__(采用self),只是标记为C ++析构函数。

答案 1 :(得分:2)

在这里,字符串有点尴尬。有几种方法可以避免遇到的问题。最简单的方法是在结构中使用固定大小的数组,但是它是2019年。我个人会全力推荐使用惯用的C ++(它是2019年!),这意味着std::string,然后整个问题就消失了。

如果您无法使用Pythonic界面,则必须做一些额外的工作。我们可以将工作量保持在较低水平,而关于SWIG的好处是,我们可以选择并选择我们所付出的额外努力的目标,没有“全有或全无”的情况。这里的主要问题是我们想将log_file路径存储在其中的缓冲区的寿命与Python Settings对象本身的寿命联系起来。我们可以根据您偏好编写Python代码,C或Python C API调用的方式,以多种不同方式实现这一目标。

我们无法真正解决的情况是,您是否通过其他代码(例如,它不是由Python拥有/管理)获得了借用的指向Settings结构的指针,而您想更改{{ 1}}在该借用对象中的字符串。您所拥有的API并没有真正为我们提供执行此操作的方法,但是在您当前的模块中,这似乎并不是真正重要的情况。

因此,下面不再赘述,有一些方法可以将缓冲区的寿命绑定在一起,该缓冲区将您的字符串保存在指向该缓冲区的Python对象上。


选项#1:使log_file完全或部分不变,使用单个Settings调用来保存结构本身及其所引用的字符串。对于这种用例,这可能是我的首选。

我们可以通过在Python中为malloc类型提供构造函数来完成此工作,而这并不会迫使您使用C ++:

Settings

如果您想使路径可变,则可以编写一些额外的Python代码,将其包装起来并充当代理,每次在Python端“对其进行“突变”时,该代理都会创建一个新的不可变对象。您也可以采用其他方法,使设置的其他成员不可变。 (仔细考虑一下,如果SWIG可以选择自动为聚合/ POD类型自动合成一个kwargs构造函数,并且将其添加为补丁也不会太难。)

这是我个人的喜好,我喜欢不变的东西,总的来说,它对生成的界面进行了相当小的调整,以获得理智的感觉。


选项#2a:创建另一个管理字符串缓冲区寿命的Python对象,然后“存储”对Python拥有的每个%module lib %{ #include "lib.h" %} // Don't let anybody change this other than the ctor %immutable Settings::log_file; %include "lib.h" %extend Settings { Settings(const char *log_file) { assert(log_file); // TODO: handle this properly // Single allocation for both things means the single free() is sufficient and correct struct Settings *result = malloc(strlen(log_file) + 1 + sizeof *result); char *buf = (void*)&result[1]; strcpy(buf, log_file); result->log_file = buf; return result; } } 结构的Python端内部的引用。

Settings

这些类型映射共同作用,以保留对存储在%module lib %{ #include "lib.h" %} %typemap(in) const char *log_file %{ // Only works for Python owned objects: assert(SWIG_Python_GetSwigThis($self)->own & SWIG_POINTER_OWN); // TODO: exception... // Python 2.7 specific, 3 gets more complicated, use bytes buffers instead. $1 = PyString_AsString($input); assert($1); // TODO: errors etc. // Force a reference to the original input string to stick around to keep the pointer valid PyObject_SetAttrString($self, "_retained_string", $input); %} %typemap(memberin) const char *log_file %{ // Because we trust the in typemap has retained the pointer for us this is sufficient now: $1 = $input; %} %include "lib.h" PyObject内的Settings字符串的引用作为属性。它只能在这里安全地工作,因为a)我们假设Python拥有该对象,并且我们不在SWIG中使用PyObject,因此我们可以安全地将属性存储在属性中以保持它们的存在; b)因为它是{{1} },而不是-builtin,我们可以确定(除非有一些K&R愚蠢的行为)没有人会更改缓冲区。


选项#2b:总体思路是相同的,但是不是使用类型映射,这意味着编写Python C API调用使用的是这样的方式:

const char *

要做同样的事情。如果愿意,也可以使用char *来产生类似的代码。但是,这是我最不喜欢的解决方案,因此我还没有完全充实它。