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



我正试图使用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 *结构值的生存期,以避免这些内存泄漏?

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

如果做不到这一点,你就会陷入这样一种情况:要使接口Python化,你必须做一些额外的工作。我们可以将工作量保持在较低的水平,SWIG的好处在于,我们可以选择我们所做的额外努力的目标,不存在"要么全有要么全无"的问题。这里的主要问题是,我们希望将存储log_file路径的缓冲区的寿命与PythonSettings对象本身的寿命联系起来。我们可以通过多种不同的方式实现这一点,具体取决于您对编写Python代码、C或Python C API调用的偏好。

我们真正不能解决的是,如果其他代码给了你一个指向Settings结构的借用指针(即它不由Python拥有/管理),并且你想更改该借用对象中的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代码来包装它,并充当一个代理,每次你在Python端"变异"它时,它都会创建一个新的不可变对象。您也可以走另一条路,使设置的其他成员不可变。(仔细想想,如果SWIG可以选择性地为聚合/POD类型自动合成kwargs构造函数,并且将其添加为补丁也不会太难,那就太好了)。

这是我个人的偏好,我喜欢不可变的东西,总的来说,这是对生成的接口的一个相当小的调整,以获得一些正常的东西。


选项#2a:制作另一个管理字符串缓冲区寿命的Python对象,然后"隐藏"对Python拥有的每个Settings结构的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.
$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"

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


选项#2b:总体想法是一样的,但不是使用类型映射,这意味着编写Python C API调用使用这样的东西:

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

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

您可以告诉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()。)

EDIT:添加了一个自定义析构函数来删除垃圾收集上的日志文件内存。(为了更好地衡量,还有一个构造函数来确保未初始化的log_filenullptr,而不是一些随机内存。)这样做的目的是向包装文件添加一个内部函数delete_Settings,该函数在_wrap_delete_Settings中被调用,在对象销毁时被调用。是的,语法有点奇怪,b/c您实际上是在描述Python的__del__(采用self),它只标记为c++析构函数。

最新更新