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

Using SWIG to wrap structures containing const char * without memory leak

我正在尝试使用 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;
}

然而,当 Settings 对象在 Python 中被垃圾回收时,分配给 log_file 的内存似乎仍然会泄漏。

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

您可以告诉 SWIG 对 log_file 使用 char* 语义。不幸的是,似乎无法使用 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()。)

编辑:添加自定义析构函数以在垃圾回收时删除 log_file 内存。 (并且还有一个构造函数来确保未初始化的 log_filenullptr,而不是一些随机内存。)这样做的目的是向包装文件添加一个内部函数 delete_Settings ,它在 _wrap_delete_Settings 中被调用,它在对象销毁时被调用。是的,语法有点奇怪,b/c 你有效地描述了 Python 的 __del__(采用 self),仅标记为 C++ 析构函数。

在这里做字符串有点笨拙。有几种方法可以回避您遇到的问题。最简单的是在结构中使用固定大小的数组,但现在是 2019 年了。就我个人而言,我全心全意地建议改用惯用的 C++(现在是 2019 年!),这意味着 std::string 然后整个问题就消失了。

如果您陷入制作界面 Pythonic 的情况,您将不得不做一些额外的工作。我们可以将工作总量保持在较低水平,而 SWIG 的好处在于我们可以选择我们付出额外努力的目标,没有 "all or nothing"。这里的主要问题是我们想要将存储 log_file 路径的缓冲区的生命周期与 Python Settings 对象本身的生命周期联系起来。我们可以通过多种不同的方式实现这一点,具体取决于您对编写 Python 代码、C 或 Python C API 调用的偏好。

我们无法真正解决的问题是,如果您被其他代码借用了指向 Settings 结构的指针(即它不是 Python 的 owned/managed ) 并且您想更改该借用对象中的 log_file 字符串。您所获得的 API 并没有真正为我们提供执行此操作的方法,但在您当前的模块中这似乎不是真正重要的案例。

因此,事不宜迟,下面是一些选项,用于将保存字符串的缓冲区的生命周期绑定到指向缓冲区的 Python 对象。


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

我们可以相当简单地通过在 Python 中为 Settings 类型提供一个构造函数来处理这个问题,它不会强制您使用 C++:

%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 代码来包装它并充当代理,每次你 "mutate" 它在Python 边。您也可以采用另一种方式,使设置的其他成员不可变。 (再考虑一下,如果 SWIG 可以选择自动为 aggregate/POD 类型自动合成一个 kwargs 构造函数,并且不会很难将其添加为补丁,那就太好了。

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


选项 #2a:创建另一个 Python 对象来管理字符串缓冲区的生命周期,然后 "stash" 对每个 [=15= 的 Python 一侧的引用] 属于 Python.

的结构
%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.
   = PyString_AsString($input);
  assert(); // 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:
   = $input;
%}

%include "lib.h"

这些类型映射一起工作以保持对 PyObject 字符串的引用作为属性存储在 Settings PyObject 中。它只能在这里安全地工作,因为 a) 我们假设 Python 拥有该对象,并且我们没有在 SWIG 中使用 -builtin,所以我们可以安全地将东西存储在属性中以将它们保存在周围,并且 b) 因为它是const char *,而不是 char * 我们可以非常确定(除非有一些 K&R 愚蠢行为)没有人会改变缓冲区。


选项 #2b:总体思路相同,但不使用类型映射,这意味着编写 Python C API 调用使用如下内容:

%extend Settings {
    %pythoncode {
      @property
      # ....
    }
}

做同样的事情。如果愿意,也可以使用 %pythonprepend 生成类似的代码。然而,这是我最不喜欢的解决方案,所以我没有完全充实它。