我正试图使用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"
这些类型映射一起工作以保持对隐藏在Settings
PyObject
中的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_file
是nullptr
,而不是一些随机内存。)这样做的目的是向包装文件添加一个内部函数delete_Settings
,该函数在_wrap_delete_Settings
中被调用,在对象销毁时被调用。是的,语法有点奇怪,b/c您实际上是在描述Python的__del__
(采用self
),它只标记为c++析构函数。