SWIG导致内存泄漏

时间:2017-06-09 10:15:30

标签: python c++ swig

我有一个类,在初始化时,保留一个带有" new"的指针数组。在销毁时,使用"删除[]"。

释放该指针数组

在没有 python的情况下运行代码时没有问题。然而,当我" swig'它"并使用它作为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的惊人/详细分析。

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

我忘记了在Swig界面文件中,所有内容都放在了

%{..}

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

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

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

class MemoryManager {

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

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

&#34;类声明&#34; in&#34; pps.i&#34;只告诉Swig我们要包装的内容..

..对..? (至少没有内存错误)

1 个答案:

答案 0 :(得分:3)

你在这里有一些未定义的行为。这是一个有趣的,所以让我们仔细看看。对于那些想要跟随我们的人,我有一个方便的ARM设备,所以我将从中进行反汇编,以显示其影响。

这种情况的根本原因在于您错误地使用了%inline这一事实。最后发生的事情是,您已经向您的编译器显示了类MemoryManager的两个不同的定义。在你的.i文件中写道:

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

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

  1. 由于您使用了%inline,SWIG和编译wrap_pps.cpp转换单元的C ++编译器都会看到该类的特定定义。
  2. 此定义中不包含任何成员变量,public或private。所以尺寸/布局会出错。 (并且它是类的定义而不是声明,即使构造函数/析构函数只是声明)。
  3. 由于没有成员变量(并且空基类优化在这里不适用),这个类的大小至少是一个字节(因此它是唯一可寻址的),但是那里有#c;不能保证它超过1个字节。这显然是错误的,因为在您定义构造函数和析构函数的转换单元中,您告诉编译器它比1字节大得多。从这一点开始,所有的赌注都已关闭,编译器可以自由地做任何想做的事情。但在这种情况下,让我们看看它在ARM上实际做了什么。

    首先我运行了objdump -d build/temp.linux-armv7l-3.4/pps_wrap.o |c++filt并查看了SWIG生成的_wrap_new_MemoryManager符号。这个函数是创建一个新的Python实例和在C ++中调用new之间的桥梁,我们在这里看,因为那里的bug表现在自己(在这个例子中)。大多数说明都是无关紧要的,所以为了简洁起见,我剪掉了一堆不相干的东西:

    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)>
    
    operator new(unsigned int)的{​​p> r0 is the first argument。在上面的代码中,编译器将其设置为仅分配了1个字节。这是错误的,它已经完成了,因为在这个翻译单元中,MemoryManager的定义只需要1个字节的存储空间来满足唯一的可寻址要求。

    因此,当我们调用MemoryManager::MemoryManager(int)时,this指向的指针只是一个1字节的堆分配。一旦我们通过读取/写入过去,我们就会对我们的堆做坏事。此时所有赌注都已关闭。稍后可以在其他地方分配其他东西,这是Python运行时所需要的。或者矢量/ 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. 将类的定义移动到.h文件中,并使用%include + #include在任何地方引用该一致定义。
    3. 对于除了最小的项目之外的所有项目,2号是我认为的最佳选择。因此,您的SWIG文件将变得简单:

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

      (您还违反了自我管理缓冲区代码中的rule of three,并且没有提供您在基于矢量的代码中免费获得的强大的异常安全保证,所以它确实值得坚持用那个)。