2009-03-05 20 views
13

Sto scrivendo una libreria che mi piacerebbe essere portatile. Pertanto, non dovrebbe dipendere dalle estensioni di glibc o Microsoft o da qualsiasi altra cosa che non sia nello standard. Ho una bella gerarchia di classi derivate da std :: exception che uso per gestire gli errori in logica e input. Sapendo che un particolare tipo di eccezione è stato lanciato in un particolare file e il numero di riga è utile, ma sapere come l'esecuzione è arrivata sarebbe potenzialmente molto più prezioso, quindi ho cercato modi per acquisire la traccia dello stack.Traccia stack C++ portatile su Eccezione

Sono consapevole che questi dati sono disponibili quando si costruisce contro glibc utilizzando le funzioni a execinfo.h (vedi question 76822) e attraverso l'interfaccia StackWalk in C di Microsoft implementazione ++ (vedi question 126450), ma mi piacerebbe molto di evitare tutto ciò non è portatile

Stavo pensando di attuare questa funzionalità me stesso in questa forma:

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; 
} 

E 'una pessima idea? Significherebbe molto lavoro aggiungere blocchi try/catch a ogni funzione, ma posso conviverci. Non funzionerebbe quando la causa dell'eccezione è la corruzione della memoria o la mancanza di memoria, ma a quel punto sei praticamente fregato comunque. Può fornire informazioni fuorvianti se alcune funzioni nello stack non catturano eccezioni, si aggiungono alla lista e rethrow, ma posso almeno garantire che tutte le funzioni della mia libreria lo facciano. Diversamente da una traccia di stack "reale" non otterrò il numero di linea nelle funzioni di chiamata, ma almeno avrei qualcosa.

La mia preoccupazione principale è la possibilità che ciò causi un rallentamento anche quando non vengono effettivamente lanciate eccezioni. Tutti questi blocchi try/catch richiedono un'ulteriore impostazione e rimozione su ogni invocazione di funzione, o sono in qualche modo gestiti in fase di compilazione? O ci sono altri problemi che non ho considerato?

risposta

21

Penso che questa sia una pessima idea.

La portabilità è un obiettivo molto degno, ma non quando si traduce in una soluzione che è invadente, che riduce le prestazioni e un'implementazione inferiore.

Ogni piattaforma (Windows/Linux/PS2/iPhone/ecc.) Su cui ho lavorato ha offerto un modo per eseguire lo stack quando si verifica un'eccezione e abbinare gli indirizzi ai nomi delle funzioni. Sì, nessuno di questi è portatile ma il framework di reporting può essere e in genere richiede meno di un giorno o due per scrivere una versione specifica della piattaforma di codice stack walk.

Non solo ci vuole meno tempo di quanto ci vorrebbe per creare/mantenere una soluzione multipiattaforma, ma i risultati sono di gran lunga migliori;

  • Non c'è bisogno di modificare le funzioni
  • Trappole crash nelle biblioteche parti standard o terze
  • Non c'è bisogno di un try/catch in ogni funzione (lenti e di memoria ad alta intensità)
2

Non penso che ci sia un modo "indipendente dalla piattaforma" per farlo - dopotutto, se ci fosse, non ci sarebbe bisogno di StackWalk o delle caratteristiche speciali di traccia dello stack gcc che menzioni.

Sarebbe un po 'complicato, ma il modo in cui vorrei implementarlo sarebbe creare una classe che offra un'interfaccia coerente per l'accesso allo stack trace, quindi avere #ifdefs nell'implementazione che usa i metodi specifici della piattaforma appropriati per mettere effettivamente insieme la traccia dello stack.

In questo modo l'utilizzo della classe è indipendente dalla piattaforma e solo quella classe dovrebbe essere modificata se si desidera indirizzare un'altra piattaforma.

0

Questo sarà più lento ma sembra che dovrebbe funzionare.

Da quello che ho capito, il problema nel creare una traccia di stack veloce e portatile è che l'implementazione dello stack sia specifica del sistema operativo e della CPU, quindi è implicitamente un problema specifico della piattaforma. Un'alternativa sarebbe utilizzare le funzioni MS/glibc e utilizzare #ifdef e definire il preprocessore appropriato (ad es. _WIN32) per implementare le soluzioni specifiche della piattaforma in diverse build.

0

Poiché l'utilizzo dello stack dipende in larga misura dalla piattaforma e dall'implementazione, non è possibile eseguirlo direttamente in modo completamente portatile. Tuttavia, è possibile creare un'interfaccia portatile per un'implementazione specifica della piattaforma e del compilatore, localizzando i problemi il più possibile. IMHO, questo sarebbe il tuo approccio migliore.

L'implementazione del tracciante si collegherebbe quindi a qualsiasi libreria di helper della piattaforma specifica disponibile. Funzionerebbe quindi solo quando si verifica un'eccezione e anche in questo caso solo se lo si chiama da un blocco catch. La sua API minima restituirebbe semplicemente una stringa contenente l'intera traccia.

Richiedere al programmatore di iniettare l'elaborazione di catch e rethrow nella catena di chiamata ha costi di runtime significativi su alcune piattaforme e impone un notevole costo di manutenzione futuro.

Detto questo, se si sceglie di utilizzare il meccanismo catch/throw, non dimenticare che anche C++ ha ancora il preprocessore C disponibile e che le macro __FILE__ e __LINE__ sono definite. È possibile utilizzarli per includere il nome del file di origine e il numero di riga nelle informazioni di traccia.

6

Cercare Nested Diagnostic Context una volta. Ecco un piccolo suggerimento:

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(); 
    } 
} 

Il sovraccarico è nell'implementazione del NDC. Stavo giocando con una versione calcolata pigramente e una che manteneva solo un numero fisso di voci. Il punto chiave è che se si utilizzano costruttori e distruttori per gestire lo stack in modo da non aver bisogno di tutti quei brutti blocchi try/catch e la manipolazione esplicita in tutto il mondo.

L'unico mal di testa specifico per la piattaforma è il metodo getContextForCurrentThread(). È possibile utilizzare un'implementazione specifica della piattaforma utilizzando l'archiviazione locale del thread per gestire il lavoro nella maggior parte se non in tutti i casi.

se siete più orientato alla prestazione e vive nel mondo dei file di log, quindi modificare l'ambito di tenere un puntatore al nome del file e numero di riga e omettere la cosa NDC tutto:

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; 
}; 

Questa volontà darti una bella traccia di stack nel tuo file di log quando viene lanciata un'eccezione.Non c'è bisogno di alcuna piedi vera pila, solo un po 'messaggio di registro quando viene generata un'eccezione;)

+1

Dai un'occhiata a Herb Sutter's http://www.gotw.ca/gotw/047.htm per un'altra prospettiva. – AJG85

1

Nel debugger:

Per ottenere l'analisi dello stack di dove un'eccezione è un tiro da Ho appena stcik la pausa point in std :: costruttore di eccezioni.

Così, quando viene creata l'eccezione, il debugger si arresta ed è quindi possibile vedere la traccia dello stack in quel punto. Non perfetto ma funziona la maggior parte del tempo.

+1

Non stava chiedendo istruzioni per il debugger. –

+1

E questo non aiuta nel de-bugging? –

+0

+1 per un suggerimento utile – Mawg

1

La gestione dello stack è una di quelle cose semplici che si complicano molto rapidamente. Meglio lasciarlo per le biblioteche specializzate. Hai provato libunwind? Funziona alla grande e AFAIK è portatile, anche se non l'ho mai provato su Windows.

Problemi correlati