Je rencontre un problème de fuite de mémoire envelopper une bibliothèque C++ en PHP en utilisant SWIG. Cela semble se produire lorsque des rappels de types complexes contenant C++ sont envoyés à PHP alors que les directeurs sont activés. Voici un exemple autonome pour reproduire la fuite:Fuite de mémoire de l'extension générée par SWIG
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_ */
Donc, pour l'utiliser, vous créez un Client
, passer un sous-classé ProcedureCallback
à la méthode invoke
du client, et le client va alors et appelle la méthode callback
de ce que vous lui avez donné, et passe un vecteur int vide.
Ceci est le fichier d'interface SWIG:
%module(directors="1") debugasync
%feature("director");
%{
#include "Client.hpp"
#include "ProcedureCallback.hpp"
%}
%include "Client.hpp"
%include "ProcedureCallback.hpp"
Sa sortie est très grande, donc je l'ai mis sur la place pastebin: debugasync_wrap.cpp. D'intérêt dans ce dossier est probablement SwigDirector_ProcedureCallback :: rappel (ligne 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());
}
Cela peut aussi être d'intérêt (ligne 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);
}
Et pour démontrer la fuite de mémoire en PHP (debugasync.php est un ensemble de classes proxy générées par SWIG que j'ai téléchargé sur 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);
}
}
Cette imprime utilisation de la mémoire, est-1k invocations, et se répète. Son exécution montre un espace mémoire à croissance rapide:
$ 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
A noter également que si le rappel C++ passe une primitive (à savoir int
) au lieu d'un type complexe (à savoir std::vector<int>
), il n'y a pas de fuite de mémoire.
Quelle est la cause de cette fuite de mémoire?
Et plus généralement, quels outils puis-je utiliser pour résoudre ce problème? Le massif de Valgrind n'a pas vraiment été en mesure d'affiner ce qui se passe, même après avoir construit PHP avec des symboles de débogage.
J'ai fait 5 000 invocations dans la boucle ci-dessus pour cette fonctionnalité et PHP a signalé 5 000 fuites de mémoire provenant de la ligne 860 de debugasync_wrap.cpp: [Tue Aug 3 11:49:58 2010] Script: 'test.php' bin/cpp/debugasync_wrap.cpp (860): Freeing 0x085B96D0 (19 octets), script = test.php fuite Dernière fois répété 4999 === total 5000 fuites de mémoire détectée === J'ai ajouté efree (classname- > value.str.val) avant le FREE_ZVAL à la fin. Avec cette addition, PHP se termine sans une fuite de mémoire, mais j'observe toujours la même mémoire croissante, mais à un rythme légèrement plus lent (6.1M contre 5.7M après 5k invocations). –
@Ed Belle prise, j'avoue que je n'utilise pas 'FREE_ZVAL', donc je ne savais pas qu'il n'appelait pas le destructeur. Vous pouvez utiliser 'zval_ptr_dtor' ou' zval_dtor' suivi de 'FREE_ZVAL', vous ne devriez pas vraiment libérer la chaîne directement. En ce qui concerne votre utilisation de la mémoire toujours croissante sans fuites de mémoire rapportés ... Peut-être que vous tenez une ressource quelque part, peut-être que c'est normal. Essayez d'ajouter plusieurs milliers d'invocations et vérifiez si PHP manque de mémoire. (en passant, choisissez un nom d'utilisateur qui a au moins trois caractères avant l'espace, sinon les notifications avec @username ne fonctionneront pas) – Artefacto
Merci @Artefacto. Il s'avère que la fuite que PHP a attrapé qui a été résolue avec efree() était en effet environ 10% du problème. L'autre 90% a été résolu en disant "$ intVector = null" dans le corps du rappel. L'utilisation de unset() ne fonctionnait pas parce que son compte de référence est de 3 lorsqu'il est passé au callback (vérifié en utilisant xdebug). –