我正在尝试使用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 *
结构值的生存期的建议方法是什么,以避免这些内存泄漏?
答案 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_file
是nullptr
,而不是一些随机存储器,构造函数也是一种很好的措施。)这样做是在包装文件中添加内部函数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 *
来产生类似的代码。但是,这是我最不喜欢的解决方案,因此我还没有完全充实它。