SWIG 内存泄漏

Memory leak with SWIG

我有一个 class,它在初始化时保留一个指针数组 "new"。销毁后,该指针数组随 "delete[]".

释放

运行代码没有python时没有问题。但是,当我 "swig'it" 并将其用作 python 模块时,会发生一些奇怪的事情。垃圾回收时正确调用了析构函数,但这样做时会发生内存泄漏!一个完全的谜(至少对我而言)。非常感谢帮助!

(1) 编译的准备工作:

setup.py

from setuptools import setup, Extension, find_packages
import os
import copy
import sys

def make_pps():
  d=[]
  c=[]
  l=[]
  lib=[]
  s=[]
  s+=["-c++"]
  return Extension("_pps",sources=["pps.i","pps.cpp"],include_dirs=d,extra_compile_args=c,extra_link_args=l,libraries=lib,swig_opts=s)

ext_modules=[]
ext_modules.append(make_pps())  

setup(
  name = "pps",
  version = "0.1",
  packages = find_packages(),
  install_requires = ['docutils>=0.3'],
  ext_modules=ext_modules
)

pps.i

%module pps
%{
#define SWIG_FILE_WITH_INIT
%}

%inline %{


class MemoryManager {

public:
  MemoryManager(int n);
  ~MemoryManager();
};

%}

(2) C++ 代码本身:

pps.cpp

#include <iostream>                             
#include <stdio.h>
#include <typeinfo>
#include <sys/time.h>
#include <stdint.h>  
#include <cmath>                       

#include <cstring>
#include <string.h>
#include <stdlib.h>

#include<sys/ipc.h> // shared memory
#include<sys/shm.h>


/*
Stand-alone cpp program: 
  - UNComment #define USE_MAIN (see below)
  - compile with

    g++ pps.cpp

  - run with

    ./a.out

    => OK

  - Check memory leaks with

    valgrind ./a.out

    => ALL OK/CLEAN


Python module:
  - Comment (i.e. use) the USE_MAIN switch (see below)
  - compile with

    python3 setup.py build_ext; cp build/lib.linux-x86_64-3.5/_pps.cpython-35m-x86_64-linux-gnu.so ./_pps.so

  - run with

    python3 test.py

    => CRASHHHHH

  - Check memory leaks with

    valgrind python3 test.py

    => Whoa..

  - Try to enable/disable lines marked with "BUG?" .. 

*/

// #define USE_MAIN 1 // UNcomment this line to get stand-alone c-program


using std::cout; 
using std::endl;
using std::string;


class MemoryManager {

public:
  MemoryManager(int n);
  ~MemoryManager();

private:
  int nmax;
  int nc; // next index to be used
  uint nsum;
  int* ids;
  void** buffers;
};


MemoryManager::MemoryManager(int n) : nmax(n),nc(0) {
  cout << "MemoryManager: nmax="<<this->nmax<<"\n";

  this->buffers  =new void*[this->nmax]; // BUG?
  this->ids      =new int  [this->nmax];
  this->nsum     =0;
}


MemoryManager::~MemoryManager() {
  printf("MemoryManager: destructor\n");
  delete[] this->buffers;               // BUG?
  delete[] this->ids;
  printf("MemoryManager: destructor: bye\n");
}


#ifdef USE_MAIN
int main(int argc, char *argv[]) {
  MemoryManager* m;

  m=new MemoryManager(1000);
  delete m;

  m=new MemoryManager(1000);
  delete m;
}
#endif

(3) 一个测试python程序:

test.py

from pps import MemoryManager
import time

print("creating MemoryManager")
mem=MemoryManager(1000)
time.sleep(1)
print("clearing MemoryManager")
mem=None
print("creating MemoryManager (again)")
time.sleep(1)
mem=MemoryManager(1000)
time.sleep(1)
print("exit")

编译:

python3 setup.py build_ext; cp build/lib.linux-x86_64-3.5/_pps.cpython-35m-x86_64-linux-gnu.so ./_pps.so

运行 与:

python3 test.py

编辑和题外话

由于这个特性的问题总是吸引那些认为他们可以通过使用容器而不是原始指针结构来解决所有问题的人,这里是容器版本(可能是错误的.. 没有太多使用矢量,但无论如何,它关闭了-主题):

class MemoryManager {

public:
  MemoryManager(int n);
  ~MemoryManager();

private:
  int nmax;
  int nc; // next index to be used
  uint nsum;

  // "ansi" version
  //int* ids;
  //void** buffers;

  // container version
  vector<int>   ids;
  vector<void*> buffers;
  // vector<shared_ptr<int>> buffers; // I feel so modern..
};


MemoryManager::MemoryManager(int n) : nmax(n),nc(0) {
  cout << "MemoryManager: nmax="<<this->nmax<<"\n";

  /* // "ansi" version
  this->buffers  =new void*[this->nmax]; // BUG?
  this->ids      =new int  [this->nmax];
  */

  // container version
  this->ids.reserve(10);
  this->buffers.reserve(10);

  this->nsum     =0;
}


MemoryManager::~MemoryManager() {
  printf("MemoryManager: destructor\n");

  /* // "ansi" version
  delete[] this->buffers;               // BUG?
  delete[] this->ids;
  */
  printf("MemoryManager: destructor: bye\n");
}

也不行

最后的评论

感谢 Flexos 的 amazing/detailed 分析。

我最初使用不一致的 class 声明的原因是,我通常不想在 python 中公开我的 class 的所有细节。

我忘记了在 Swig 接口文件中,

中的所有内容

%{..}

被添加到生成的包装器代码中......现在它丢失了,所以包装器代码仅从 %inline 部分获得了 class 声明..!

我们仍然可以使用以下 pps.i 文件包装 class 的最小部分:

%module pps
%{
#define SWIG_FILE_WITH_INIT
#include "pps.h"
%}

class MemoryManager {

public:
  MemoryManager(int n);
  ~MemoryManager();
};

其中 "pps.h" 应该有正确的 class 声明。现在 #included "pps.h" 出现在 "pps_wrap.cpp" 的开头。

"pps.i"中的"class declaration"只告诉Swig我们要包装什么..

..对吧..? (至少没有更多的内存错误)

您这里有一些未定义的行为。这很有趣,所以让我们仔细看看。对于那些想跟进的人,我手边有一个 ARM 设备,所以我将对其进行反汇编以显示影响是什么。

根本原因在于您 %inline 的使用不当。最终发生的事情是,您向编译器展示了 class MemoryManager 的两个 不同 定义。在您的 .i 文件中,您写道:

%inline %{
class MemoryManager {    
public:
  MemoryManager(int n);
  ~MemoryManager();
};
%}

MemoryManager 的定义中重要的是两件事:

  1. 由于您使用了 %inline,SWIG 和编译 wrap_pps.cpp t运行slation 单元的 C++ 编译器都可以看到 class 的这个特定定义。
  2. 此定义中没有成员变量,public 或 private。所以 size/layout 是错误的。 (它是 class 的定义而不是声明,即使 constructor/destructor 只是声明)。

因为没有成员变量(空基 class 优化在这里不适用)这个 class 的大小至少是一个字节(所以它是唯一可寻址的),但是有没有 gua运行tee 它超过 1 个字节。这显然是错误的,因为在定义构造函数和析构函数的 t运行slation 单元中,您已经告诉编译器它比 1 个字节大得多。从这一点开始,所有赌注都取消了,编译器可以自由地做任何它想做的事。但在这个例子中,让我们看看它在 ARM 上实际做了什么。

首先我 运行: objdump -d build/temp.linux-armv7l-3.4/pps_wrap.o |c++filt 并查看了 SWIG 生成的 _wrap_new_MemoryManager 符号。这个函数是创建一个新的 Python 实例和在 C++ 中调用 new 之间的桥梁,我们在这里寻找是因为这是错误表现出来的地方(在这个例子中)。大多数说明都是无关紧要的,因此为了简洁起见,我删掉了一堆无关紧要的内容:

00000930 <_wrap_new_MemoryManager>:
     930:       4b20            ldr     r3, [pc, #128]  ; (9b4 <_wrap_new_MemoryManager+0x84>)
     932:       4608            mov     r0, r1
     934:       b570            push    {r4, r5, r6, lr}
     ...
     # [snip]
     ...
     966:       2001            movs    r0, #1  <-- THIS IS IT
     968:       f7ff fffe       bl      0 <operator new(unsigned int)>
     96c:       4631            mov     r1, r6
     96e:       4604            mov     r4, r0
     970:       f7ff fffe       bl      0 <MemoryManager::MemoryManager(int)>

r0 is the first argumentoperator new(unsigned int)。在上面的代码中,编译器将其设置为仅分配 1 个字节。这是错误的,它已经这样做了,因为在这个 t运行slation 单元中,MemoryManager 的定义只需要 1 个字节的存储空间来满足唯一可寻址的要求。

所以当我们调用MemoryManager::MemoryManager(int)时,this指向的指针只是一个1字节的堆分配。一旦我们 read/write 超过了我们正在对我们的堆做坏事。在这一点上,所有的赌注都没有了。稍后可以在那里分配其他东西,Python 运行时需要的任何东西。或者您的 vector/new[] 调用产生的分配。或者它可能只是未映射的内存,或者其他东西。但是无论发生什么都是不好的。

为了比较,如果我们在启用 main 函数的情况下编译你的 C++(g++ -Wall -Wextra pps.cpp -DUSE_MAIN -S 以回避 objdump),我们会得到这个:

main:
        .fnstart
.LFB1121:
        @ args = 0, pretend = 0, frame = 16
        @ frame_needed = 1, uses_anonymous_args = 0
        push    {r4, r7, lr}
        .save {r4, r7, lr}
        .pad #20
        sub     sp, sp, #20
        .setfp r7, sp, #0
        add     r7, sp, #0
        str     r0, [r7, #4]
        str     r1, [r7]
        movs    r0, #20    <-- This looks much more sensible!
.LEHB0:
        bl      operator new(unsigned int)
.LEHE0:
        mov     r4, r0
        mov     r0, r4
        mov     r1, #1000
.LEHB1:
        bl      MemoryManager::MemoryManager(int)

(我有点惊讶 gcc 在默认优化设置下生成该代码)

所以要真正解决这个问题,我建议做以下两件事之一:

  1. 所有 你的 C++ 放入 %inline 并且根本没有任何其他 .cpp 文件。
  2. 将 class 的定义移动到 .h 文件中,并使用 %include + #include 在任何地方引用该一致的定义。

对于除最小项目之外的所有项目,我认为数字 2 是最佳选择。因此,您的 SWIG 文件将变得简单:

%module pps
%{
#define SWIG_FILE_WITH_INIT
#include "pps.h"
%}

%include "pps.h"
%}

(您还违反了自我管理缓冲区代码中的 rule of three,并且没有提供您在基于向量的代码中免费获得的强异常安全保障 运行 tees,所以坚持下去真的很值得)。