Question

J'écris une bibliothèque que je voudrais être portable. Ainsi, cela ne devrait pas dépendre des extensions glibc ou Microsoft ou de tout autre élément ne figurant pas dans la norme. J'ai une belle hiérarchie de classes dérivées de std :: exception que j'utilise pour gérer les erreurs de logique et d'entrée. Savoir qu'un type particulier d'exception a été lancé sur un fichier particulier et sur un numéro de ligne est utile, mais savoir comment l'exécution s'y est déroulée aurait potentiellement beaucoup plus de valeur. C'est pourquoi j'ai cherché des moyens d'obtenir la trace de la pile.

Je suis conscient du fait que ces données sont disponibles lors de la compilation contre glibc à l'aide des fonctions de execinfo.h (voir question 76822 ) et via l'interface StackWalk dans l'implémentation C ++ de Microsoft (voir question 126450 ), mais j’aimerais beaucoup éviter tout ce qui n’est pas portable.

Je pensais mettre en oeuvre cette fonctionnalité moi-même sous cette forme:

class myException : public std::exception
{
public:
  ...
  void AddCall( std::string s )
  { m_vCallStack.push_back( s ); }
  std::string ToStr() const
  {
    std::string l_sRet = "";
    ...
    l_sRet += "Call stack:\n";
    for( int i = 0; i < m_vCallStack.size(); i++ )
      l_sRet += "  " + m_vCallStack[i] + "\n";
    ...
    return l_sRet;
  }
private:
  ...
  std::vector< std::string > m_vCallStack;
};

ret_type some_function( param_1, param_2, param_3 )
{
  try
  {
    ...
  }
  catch( myException e )
  {
    e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" );
    throw e;
  }
}

int main( int argc, char * argv[] )
{
  try
  {
    ...
  }
  catch ( myException e )
  {
    std::cerr << "Caught exception: \n" << e.ToStr();
    return 1;
  }
  return 0;
}

Est-ce une idée terrible? Cela signifierait beaucoup de travail en ajoutant des blocs try / catch à chaque fonction, mais je peux vivre avec cela. Cela ne fonctionnerait pas si la cause de l'exception était une corruption de mémoire ou un manque de mémoire, mais à ce stade, vous êtes quand même assez mal pris. Cela peut fournir des informations trompeuses si certaines fonctions de la pile ne capturent pas les exceptions, ne s'ajoutent pas à la liste et ne s'affichent plus, mais je peux au moins garantir que toutes les fonctions de ma bibliothèque le feront. Contrairement à un " réel " trace de pile Je ne vais pas obtenir le numéro de ligne dans les fonctions d’appel, mais au moins j’aurais quelque chose

Ma principale préoccupation est la possibilité que cela provoque un ralentissement même si aucune exception n'est réellement levée. Tous ces blocs try / catch nécessitent-ils une configuration et une suppression supplémentaires à chaque appel de fonction, ou sont-ils gérés d'une manière ou d'une autre au moment de la compilation? Ou y a-t-il d'autres problèmes que je n'ai pas considérés?

Était-ce utile?

La solution

Je pense que c'est une très mauvaise idée.

La portabilité est un objectif louable, mais pas dans les cas où elle aboutit à une solution intrusive, à une perte de performances et à une implémentation inférieure.

Chaque plate-forme (Windows / Linux / PS2 / iPhone / etc.) sur laquelle j'ai travaillé offre un moyen de gérer la pile lorsqu'une exception se produit et de faire correspondre les adresses aux noms de fonctions. Oui, aucun de ceux-ci n'est portable, mais le cadre de reporting peut l'être et l'écriture d'une version du code de la pile spécifique à la plate-forme prend généralement moins d'un jour ou deux.

Non seulement cela prend-il moins de temps que de créer / maintenir une solution multiplate-forme, mais les résultats sont bien meilleurs;

  • Pas besoin de modifier les fonctions
  • Interruptions bloquantes dans les bibliothèques standard ou tierces
  • Pas besoin d'essayer / attraper dans chaque fonction (lent et gourmand en mémoire)

Autres conseils

Recherchez le contexte de diagnostic imbriqué une fois. Voici un petit indice:

class NDC {
public:
    static NDC* getContextForCurrentThread();
    int addEntry(char const* file, unsigned lineNo);
    void removeEntry(int key);
    void dump(std::ostream& os);
    void clear();
};

class Scope {
public:
    Scope(char const *file, unsigned lineNo) {
       NDC *ctx = NDC::getContextForCurrentThread();
       myKey = ctx->addEntry(file,lineNo);
    }
    ~Scope() {
       if (!std::uncaught_exception()) {
           NDC *ctx = NDC::getContextForCurrentThread();
           ctx->removeEntry(myKey);
       }
    }
private:
    int myKey;
};
#define DECLARE_NDC() Scope s__(__FILE__,__LINE__)

void f() {
    DECLARE_NDC(); // always declare the scope
    // only use try/catch when you want to handle an exception
    // and dump the stack
    try {
       // do stuff in here
    } catch (...) {
       NDC* ctx = NDC::getContextForCurrentThread();
       ctx->dump(std::cerr);
       ctx->clear();
    }
}

Les frais généraux sont liés à la mise en œuvre du NDC. Je jouais avec une version paresseusement évaluée ainsi qu'une version qui ne conservait qu'un nombre fixe d'entrées. Le point clé est que, si vous utilisez des constructeurs et des destructeurs pour gérer la pile, vous n'avez pas besoin de tous ces vilains try / catch et de la manipulation explicite partout.

Le seul casse-tête spécifique à la plate-forme est la méthode getContextForCurrentThread () . Vous pouvez utiliser une implémentation spécifique à la plate-forme utilisant le stockage local des threads pour gérer le travail dans la plupart des cas, voire dans tous les cas.

Si vous êtes plus axé sur les performances et vivez dans le monde des fichiers journaux, modifiez l'étendue de manière à maintenir un pointeur sur le nom du fichier et le numéro de ligne et omettez complètement le code NDC:

class Scope {
public:
    Scope(char const* f, unsigned l): fileName(f), lineNo(l) {}
    ~Scope() {
        if (std::uncaught_exception()) {
            log_error("%s(%u): stack unwind due to exception\n",
                      fileName, lineNo);
        }
    }
private:
    char const* fileName;
    unsigned lineNo;
};

Cela vous donnera une belle trace de pile dans votre fichier journal quand une exception est levée. Pas besoin de pile réelle, juste un petit message de log quand une exception est lancée;)

Je ne pense pas qu'il existe une "plate-forme indépendante". façon de le faire - après tout, s’il y en avait, il n’y aurait pas besoin de StackWalk ou des fonctionnalités spéciales de traçage de pile gcc que vous mentionnez.

Cela serait un peu brouillon, mais la façon dont je l’implémenterais serait de créer une classe offrant une interface cohérente pour accéder à la trace de pile, puis d’avoir #ifdefs dans l’implémentation qui utilise les méthodes appropriées propres à la plate-forme pour: effectivement mettre la trace de la pile ensemble.

Ainsi, votre utilisation de la classe dépend de la plate-forme. Seule cette classe devra être modifiée si vous souhaitez cibler une autre plate-forme.

Dans le débogueur:

Pour obtenir la trace de pile où une exception est levée, je stcik le point de rupture dans le constructeur std :: exception.

Ainsi, lorsque l’exception est créée, le débogueur s’arrête et vous pouvez voir la trace de la pile à cet endroit. Pas parfait mais ça marche la plupart du temps.

La gestion de pile fait partie de ces choses simples qui se compliquent très rapidement. Mieux vaut le laisser aux bibliothèques spécialisées. Avez-vous essayé libunwind? Fonctionne très bien et autant que je sache, c’est portable, même si je ne l’ai jamais essayé sous Windows.

Cela sera plus lent, mais cela devrait fonctionner.

D'après ce que je comprends de la difficulté à créer une trace de pile rapide et portable, l'implémentation de la pile dépend à la fois du système d'exploitation et du processeur. Il s'agit donc implicitement d'un problème spécifique à la plate-forme. Une autre solution consisterait à utiliser les fonctions MS / glibc et à utiliser #ifdef et les définitions de préprocesseur appropriées (par exemple, _WIN32) pour implémenter les solutions spécifiques à la plate-forme dans différentes versions.

Étant donné que l'utilisation de la pile dépend fortement de la plate-forme et de la mise en œuvre, il n'existe aucun moyen de le faire directement et entièrement portable. Cependant, vous pouvez créer une interface portable pour une plate-forme et une implémentation spécifique du compilateur, en localisant les problèmes autant que possible. IMHO, ce serait votre meilleure approche.

L’implémentation du traceur serait alors liée aux librairies auxiliaires spécifiques à la plate-forme disponibles. Il ne fonctionnerait alors que lorsqu'une exception se produirait, et même si vous l'appeliez depuis un bloc catch. Son API minimale renverrait simplement une chaîne contenant la trace entière.

Demander au codeur d’injecter le traitement des captures et du renvoi dans la chaîne d’appel entraîne des coûts d’exécution considérables sur certaines plates-formes et impose un coût de maintenance futur élevé.

Cela dit, si vous choisissez d'utiliser le mécanisme catch / throw, n'oubliez pas que même le pré-processeur C de C ++ est toujours disponible, et que les macros __ FILE __ et __ LIGNE __ sont définis. Vous pouvez les utiliser pour inclure le nom du fichier source et le numéro de ligne dans vos informations de trace.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top