2009-03-05 16 views
13

Estoy escribiendo una biblioteca que me gustaría ser portátil. Por lo tanto, no debe depender de glibc o extensiones de Microsoft o cualquier otra cosa que no esté en el estándar. Tengo una buena jerarquía de clases derivadas de std :: exception que utilizo para manejar errores en lógica y entrada. Saber que un tipo particular de excepción fue arrojado a un número de línea y archivo en particular es útil, pero saber cómo llegó la ejecución sería potencialmente mucho más valioso, así que he estado buscando formas de adquirir el seguimiento de la pila.Rastreo de pila portátil C++ en la excepción

Soy consciente de que estos datos están disponibles en la construcción con glibc utilizando las funciones de execinfo.h (ver question 76822) ya través de la interfaz StackWalk en C de Microsoft aplicación ++ (ver question 126450), pero me gustaría mucho para evitar cualquier cosa eso no es portátil

Estaba pensando en implementar esta funcionalidad a mí mismo en esta 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; 
} 

¿Es esta una idea terrible? Significaría mucho trabajo agregar bloques try/catch a cada función, pero puedo vivir con eso. No funcionaría cuando la causa de la excepción es la corrupción de la memoria o la falta de memoria, pero en ese punto estás bastante jodida de todos modos. Puede proporcionar información engañosa si algunas funciones en la pila no detectan excepciones, se agregan a la lista y vuelven a lanzar, pero al menos puedo garantizar que todas las funciones de mi biblioteca lo hacen. A diferencia de una traza de pila "real", no obtendré el número de línea en las funciones de llamada, pero al menos tendría algo.

Mi principal preocupación es la posibilidad de que esto cause una desaceleración incluso cuando no se lanzan excepciones. ¿Todos estos bloques de prueba/captura requieren una configuración y extracción adicionales en cada invocación de función, o se manejan de alguna manera en tiempo de compilación? ¿O hay otros problemas que no he considerado?

Respuesta

21

Creo que esta es una muy mala idea.

La portabilidad es un objetivo muy valioso, pero no cuando resulta en una solución intrusiva, que mina el rendimiento y una implementación inferior.

Cada plataforma (Windows/Linux/PS2/iPhone/etc.) en la que he trabajado ha ofrecido una forma de recorrer la pila cuando se produce una excepción y unir direcciones a nombres de funciones. Sí, ninguno de estos es portátil, pero el marco de trabajo de informes puede serlo y generalmente toma menos de uno o dos días escribir una versión específica de la plataforma del código para caminar sobre la pila.

No solo es esto menos tiempo de lo que tomaría crear/mantener una solución multiplataforma, pero los resultados son mucho mejores;

  • No hay necesidad de modificar las funciones
  • Trampas de accidentes en las bibliotecas estándar de otros fabricantes o terceros
  • No hay necesidad de un try/catch en cada función (lentos y que requieren mucha memoria)
2

No creo que haya una forma de "plataforma independiente" para hacer esto; después de todo, si lo hubiera, no habría una necesidad de StackWalk o las características especiales de seguimiento de pila de gcc que mencione.

Sería un poco complicado, pero la forma en que implementaría esto sería crear una clase que ofrezca una interfaz consistente para acceder al seguimiento de la pila, y luego tener #ifdefs en la implementación que use los métodos apropiados específicos de la plataforma poner realmente el rastro de la pila juntos.

De esta manera, el uso de la clase es independiente de la plataforma, y ​​solo esa clase tendría que modificarse si desea orientarla a otra plataforma.

0

Ésta será más lento, pero parece que debería funcionar.

Por lo que entiendo, el problema al hacer un seguimiento rápido y portátil de la pila es que la implementación de la pila es específica del sistema operativo y de la CPU, por lo que es implícitamente un problema específico de la plataforma. Una alternativa sería usar las funciones MS/glibc y usar #ifdef y el preprocesador apropiado define (por ejemplo, _WIN32) para implementar las soluciones específicas de la plataforma en diferentes compilaciones.

0

Dado que el uso de la pila depende en gran medida de la plataforma y la implementación, no hay forma de hacerlo directamente que sea completamente portátil. Sin embargo, puede crear una interfaz portátil para una plataforma y una implementación específica del compilador, localizando los problemas tanto como sea posible. En mi humilde opinión, este sería su mejor enfoque.

La implementación del rastreador se vincularía a las bibliotecas auxiliares específicas de la plataforma disponibles. Entonces funcionaría solo cuando ocurra una excepción, e incluso entonces solo si lo llamó desde un bloque catch. Su API mínima simplemente devolvería una cadena que contiene toda la traza.

Requerir que el codificador inyecte el proceso de captura y reinicio en la cadena de llamadas tiene costos de tiempo de ejecución significativos en algunas plataformas e impone un gran costo de mantenimiento en el futuro.

Dicho esto, si opta por utilizar el mecanismo de atrapar/lanzar, no olvide que incluso C++ todavía tiene el preprocesador C disponible, y que las macros __FILE__ y __LINE__ están definidas. Puede usarlos para incluir el nombre del archivo fuente y el número de línea en su información de rastreo.

6

Busque Nested Diagnostic Context una vez. Aquí hay una pequeña sugerencia:

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

La sobrecarga está en la implementación del NDC. Estaba jugando con una versión perezosamente evaluada y una que solo mantuvo un número fijo de entradas también. El punto clave es que si usas constructores y destructores para manejar la pila, así no necesitarás todos esos desagradables bloques try/catch y manipulación explícita en todas partes.

El único dolor de cabeza específico de la plataforma es el método getContextForCurrentThread(). Puede utilizar una implementación específica de la plataforma utilizando el almacenamiento local de subprocesos para manejar el trabajo en la mayoría de los casos, si no en todos.

Si se orientan más rendimiento y vive en el mundo de los archivos de registro, a continuación, cambiar el alcance para mantener un puntero al nombre de archivo y número de línea y omitir lo NDC completo:

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

Esta voluntad darle un buen seguimiento de pila en su archivo de registro cuando se lanza una excepción.No hay necesidad de ninguna pila real de caminar, sólo un pequeño mensaje de registro cuando se produce una excepción;)

+1

Consulte la http://www.gotw.ca/gotw/047.htm de Herb Sutter para obtener otra perspectiva. – AJG85

1

En el depurador:

Para obtener el seguimiento de la pila de las excepciones que haya un tiro desde acabo Stcik la ruptura point en std :: constructor de excepciones.

Por lo tanto, cuando se crea la excepción, el depurador se detiene y luego puede ver el seguimiento de la pila en ese punto. No es perfecto, pero funciona la mayor parte del tiempo.

+1

No estaba pidiendo instrucciones de depuración. –

+1

¿Y esto no ayuda a eliminar errores? –

+0

+1 para una sugerencia útil – Mawg

1

La gestión de pila es una de esas cosas simples que se complican muy rápidamente. Mejor déjalo en bibliotecas especializadas. ¿Has probado libunwind? Funciona muy bien y AFAIK es portátil, aunque nunca lo he probado en Windows.

Cuestiones relacionadas