views:

90

answers:

1

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.

+2  A: 

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.

Artefacto
I did 5,000 invocations in the above for loop with this enabled and PHP reported 5,000 memory leaks originating from line 860 of debugasync_wrap.cpp:[Tue Aug 3 11:49:58 2010] Script: 'test.php'bin/cpp/debugasync_wrap.cpp(860) : Freeing 0x085B96D0 (19 bytes), script=test.phpLast leak repeated 4999 times=== Total 5000 memory leaks detected ===I added efree(classname->value.str.val) before the FREE_ZVAL at the end. With that addition, PHP exits without a memory leak, but I still observe the same growing memory, though at a slightly slower rate (6.1M vs. 5.7M after 5k invocations).
Edward Mazur
@Ed Nice catch, I confess I don't use `FREE_ZVAL`, so I didn't know it didn't call the destructor. You can use `zval_ptr_dtor` or `zval_dtor` followed by `FREE_ZVAL`, you really shouldn't be freeing the string directly. As to your still growing memory usage without memory leaks reported... Maybe you're holding a resource somewhere, maybe it's normal. Try to add several more thousand invocations and check if PHP runs out of memory. (by the way, pick a username that has at least three characters before the space, otherwise notifications with @username won't work)
Artefacto
Thanks @Artefacto. It turns out the leak that PHP caught that was solved with efree() was indeed about 10% of the problem. The other 90% was solved by saying "$intVector = null" in the body of the callback. Using unset() didn't work because its reference count is 3 when it is passed to the callback (verified using xdebug).
Edward Mazur
@Edw I'm assuming the underlying zval had the reference flag on, because otherwise using `$intVector = null` should have the same effect as `unset($intVector)` in the reference count of the zval. (if the flag is set, `$intVector = null` leaves the ref count untouched but turns the zval into a PHP null, which would cause a call to `zval_dtor` on the previous value, thereby potentially freeing resources).
Artefacto