开发者

Memory leak from SWIG-generated extension

开发者 https://www.devze.com 2023-01-10 03:35 出处:网络
I\'m having a memory leak problem wrapping a C++ library in PHP using SWIG. It seems to happen when callbacks from C++ containing complex types are sent to PHP while directors are enabled. Here is a s

I'm having a memory leak problem wrapping a C++ library in PHP using SWIG. It seems to happen when callbacks from C++ containing complex types are sent to PHP while directors are enabled. Here is a standalone example to reproduce the leak:

Client.hpp:

#ifndef CLIENT_HPP_
#define CLIENT_HPP_

#include <vector>
#include "ProcedureCallback.hpp"

class Client {
public:
    void invoke(ProcedureCallback *callback) {
        callback->callback开发者_如何学运维(std::vector<int>(0));
    }
};

#endif /* CLIENT_HPP_ */

ProcedureCallback.hpp:

#ifndef PROCEDURECALLBACK_HPP_
#define PROCEDURECALLBACK_HPP_

#include <vector>

class ProcedureCallback {
public:
    virtual void callback(std::vector<int>) = 0;
};

#endif /* PROCEDURECALLBACK_HPP_ */

So to use this, you create a Client, pass a subclassed ProcedureCallback to Client's invoke method, and Client then goes and calls the callback method of what you gave it, and passes an empty int vector.

This is the SWIG interface file:

%module(directors="1") debugasync
%feature("director");

%{
#include "Client.hpp"
#include "ProcedureCallback.hpp"
%}

%include "Client.hpp"
%include "ProcedureCallback.hpp"

Its output is very large, so I put it on pastebin instead: debugasync_wrap.cpp. Of interest in this file is probably SwigDirector_ProcedureCallback::callback (line 1319):

void SwigDirector_ProcedureCallback::callback(std::vector< int > arg0) {
  zval *args[1];
  zval *result, funcname;
  MAKE_STD_ZVAL(result);
  ZVAL_STRING(&funcname, (char *)"callback", 0);
  if (!swig_self) {
    SWIG_PHP_Error(E_ERROR, "this pointer is NULL");
  }

  zval obj0;
  args[0] = &obj0;
  {
    SWIG_SetPointerZval(&obj0, SWIG_as_voidptr(&arg0), SWIGTYPE_p_std__vectorT_int_t, 2);
  }
  call_user_function(EG(function_table), (zval**)&swig_self, &funcname,
    result, 1, args TSRMLS_CC);
  FREE_ZVAL(result);
  return;
fail:
  zend_error(SWIG_ErrorCode(),"%s",SWIG_ErrorMsg());
}

This may also be of interest (line 827):

static void
SWIG_ZTS_SetPointerZval(zval *z, void *ptr, swig_type_info *type, int newobject TSRMLS_DC) {
  swig_object_wrapper *value=NULL;
  /*
   * First test for Null pointers.  Return those as PHP native NULL
   */
  if (!ptr ) {
    ZVAL_NULL(z);
    return;
  }
  if (type->clientdata) {
    if (! (*(int *)(type->clientdata)))
      zend_error(E_ERROR, "Type: %s failed to register with zend",type->name);
    value=(swig_object_wrapper *)emalloc(sizeof(swig_object_wrapper));
    value->ptr=ptr;
    value->newobject=newobject;
    if (newobject <= 1) {
      /* Just register the pointer as a resource. */
      ZEND_REGISTER_RESOURCE(z, value, *(int *)(type->clientdata));
    } else {
      /*
       * Wrap the resource in an object, the resource will be accessible
       * via the "_cPtr" member. This is currently only used by
       * directorin typemaps.
       */
      value->newobject = 0;
      zval *resource;
      MAKE_STD_ZVAL(resource);
      ZEND_REGISTER_RESOURCE(resource, value, *(int *)(type->clientdata));
      zend_class_entry **ce = NULL;
      zval *classname;
      MAKE_STD_ZVAL(classname);
      /* _p_Foo -> Foo */
      ZVAL_STRING(classname, (char*)type->name+3, 1);
      /* class names are stored in lowercase */
      php_strtolower(Z_STRVAL_PP(&classname), Z_STRLEN_PP(&classname));
      if (zend_lookup_class(Z_STRVAL_P(classname), Z_STRLEN_P(classname), &ce TSRMLS_CC) != SUCCESS) {
        /* class does not exist */
        object_init(z);
      } else {
        object_init_ex(z, *ce);
      }
      Z_SET_REFCOUNT_P(z, 1);
      Z_SET_ISREF_P(z);
      zend_hash_update(HASH_OF(z), (char*)"_cPtr", sizeof("_cPtr"), (void*)&resource, sizeof(zval), NULL);
      FREE_ZVAL(classname);
    }
    return;
  }
  zend_error(E_ERROR, "Type: %s not registered with zend",type->name);
}

And to demonstrate the memory leak in PHP (debugasync.php is a set of proxy classes generated by SWIG which I have also uploaded to pastebin):

<?php

require('debugasync.php');

class MyCallback extends ProcedureCallback {
    public function callback($intVector) {}
}

$client = new Client();
$callback = new MyCallback();

while (true) {
    print(number_format(memory_get_usage()) . "\n");
    for ($j = 0; $j < 1000; $j++) {
        $client->invoke($callback);
    }
}

This prints memory usage, does 1k invocations, and repeats. Running it shows a quickly-growing memory space:

$ php test.php 
692,664
1,605,488
2,583,232
3,634,776
4,538,784
5,737,760
6,641,768
7,545,816
^C

Also of note is that if the C++ callback passes a primitive (i.e. int) instead of a complex type (i.e. std::vector<int>), there is no memory leak.

What is the cause of this memory leak?

And more generally, what tools can I use to solve this? Valgrind's massif hasn't really been able to narrow down what's going on, even after building PHP with debugging symbols.


I know nothing about SWIG specifically, but if the memory usage is reported by memory_get_usage, then the taken memory is allocated with the Zend Engine memory manager.

When the script finishes cleanly (no CTRL+C or die), the memory manager will tell you about the memory leaks it's found as long as:

  • PHP is compiled in debug mode (--enable-debug)
  • You have report_memleaks = true in your php.ini file

This will tell you where the memory that wasn't freed was allocated.

That said, there isn't anything specially funny with your snippet; the only non-stack allocated variable is properly disposed of.

0

精彩评论

暂无评论...
验证码 换一张
取 消