2010-08-02 3 views
2

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.

Répondre

2

Je ne sais rien de SWIG spécifiquement, mais si l'utilisation de la mémoire est signalée par memory_get_usage, alors la mémoire utilisée est allouée avec le gestionnaire de mémoire de Zend Engine.

Lorsque le script se termine proprement (pas CTRL + C ou die), le gestionnaire de mémoire vous renseignera sur la mémoire des fuites il se trouve aussi longtemps que:

  • PHP est compilé en mode débogage (--enable-debug)
  • vous avez report_memleaks = true dans votre fichier php.ini

Cela vous indiquera où la mémoire qui n'a pas été libéré a été alloué.

Cela dit, il n'y a rien de particulièrement drôle avec votre extrait; la seule variable allouée sans pile est correctement éliminée.

+0

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). –

+0

@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

+0

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). –