使用 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_file
是 nullptr
,而不是一些随机内存。)这样做的目的是向包装文件添加一个内部函数 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
生成类似的代码。然而,这是我最不喜欢的解决方案,所以我没有完全充实它。
我正在尝试使用 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_file
是 nullptr
,而不是一些随机内存。)这样做的目的是向包装文件添加一个内部函数 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
生成类似的代码。然而,这是我最不喜欢的解决方案,所以我没有完全充实它。