Переносная трассировка стека С++ при исключении
Я пишу библиотеку, которая хотела бы быть переносной. Таким образом, он не должен зависеть от расширений glibc или Microsoft или чего-либо еще, что не соответствует стандарту. У меня хорошая иерархия классов, полученных из std:: exception, которые я использую для обработки ошибок в логике и вводе. Зная, что конкретный тип исключения был брошен в конкретный файл, и номер строки полезен, но знание того, как получившееся выполнение было потенциально намного более ценным, поэтому я рассматриваю способы получения трассировки стека.
Я знаю, что эти данные доступны при создании против glibc с использованием функций в execinfo.h(см. вопрос 76822) и через интерфейс StackWalk в реализации Microsoft С++ (см. вопрос 126450), но я очень хотел бы избежать всего, что не переносится.
Я думал о реализации этой функции в этой форме:
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;
}
Это ужасная идея? Это означало бы большую работу, добавляющую блоки try/catch к каждой функции, но я могу жить с этим. Это не сработает, когда причиной исключения станет повреждение памяти или нехватка памяти, но в этом случае вы все равно ввернуты. Это может привести к вводящей в заблуждение информации, если некоторые функции в стеке не поймают исключения, добавят себя в список и реконструируют, но я могу хотя бы предоставить гарантию, что все мои библиотечные функции сделают это. В отличие от "реальной" трассировки стека я не получу номер строки в вызывающих функциях, но, по крайней мере, у меня было бы что-то.
Моя главная проблема - это возможность того, что это приведет к замедлению, даже если на самом деле нет исключений. Все ли эти блоки try/catch требуют дополнительной настройки и срыва при каждом вызове функции или как-то обрабатываются во время компиляции? Или есть другие проблемы, которые я не рассматривал?
Ответы
Ответ 1
Я думаю, что это действительно плохая идея.
Переносимость - очень достойная цель, но не тогда, когда это приводит к навязчивому решению, ухудшению производительности и более низкой реализации.
Каждая платформа (Windows/Linux/PS2/iPhone/и т.д.), над которой я работал, предложила способ ходить по стеку, когда возникает исключение, и сопоставлять адреса с именами функций. Да, ни один из них не является переносимым, но структура отчетности может быть такой, и обычно требуется меньше одного или двух дней, чтобы написать версию кода стека для конкретной платформы.
Мало того, что это меньше времени, чем при создании/поддержке кросс-платформенного решения, но результаты намного лучше,
- Нет необходимости изменять функции
- Ловушки сбой в стандартных или сторонних библиотеках
- Нет необходимости в попытке/уловке в каждой функции (медленная и интенсивная память)
Ответ 2
Посмотрите Nested Diagnostic Context
один раз. Вот небольшой намек:
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();
}
}
Накладные расходы - это реализация НДЦ. Я играл с лениво оцененной версией, а также с той, которая содержала только фиксированное количество записей. Ключевым моментом является то, что если вы используете конструкторы и деструкторы для обработки стека, чтобы вам не нужны все эти неприятные блоки try
/catch
и явные манипуляции повсюду.
Единственной специфической головной болью для платформы является метод getContextForCurrentThread()
. Вы можете использовать конкретную платформу, используя локальное хранилище потоков, чтобы обрабатывать работу в большинстве, если не во всех случаях.
Если вы более ориентированы на производительность и живете в мире файлов журналов, измените область действия, чтобы удерживать указатель на имя файла и номер строки и вообще опустить предмет 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;
};
Это даст вам хорошую трассировку стека в вашем файле журнала при вызове исключения. Нет необходимости в какой-либо реальной ходьбе стека, просто небольшое сообщение журнала, когда бросается исключение;)
Ответ 3
Я не думаю, что существует такой "независимый от платформы" способ сделать это. В конце концов, если бы не было, не было бы необходимости в StackWalk или специальных атрибутах трассировки gcc, которые вы упомянули.
Это было бы немного грязно, но способ, которым я бы это реализовал, - создать класс, который предлагает последовательный интерфейс для доступа к трассировке стека, а затем иметь #ifdefs в реализации, которые используют соответствующие методы для платформы на самом деле положить трассировку стека вместе.
Таким образом, ваше использование класса не зависит от платформы, и просто этот класс нужно будет изменить, если вы хотите настроить таргетинг на другую платформу.
Ответ 4
В отладчике:
Чтобы получить трассировку стека, где исключение выбрасывается, я просто stcik точку останова в std:: exception конструкторе.
Таким образом, когда создается исключение, отладчик останавливается, и вы можете увидеть трассировку стека в этой точке. Не идеально, но он работает большую часть времени.
Ответ 5
Управление стеками - одна из тех простых вещей, которые усложняются очень быстро. Лучше оставить его для специализированных библиотек. Вы пробовали либнвинд? Отлично работает и AFAIK это портативный, хотя я никогда не пробовал его в Windows.
Ответ 6
Это будет медленнее, но похоже, что он должен работать.
Из того, что я понимаю, проблема в создании быстрой, переносимой трассировки стека заключается в том, что реализация стека - это как ОС, так и ЦП, поэтому это неявно проблема конкретной платформы. Альтернативой может быть использование функций MS/glibc и использование #ifdef и соответствующих препроцессоров (например, _WIN32) для реализации конкретных решений платформы в разных сборках.
Ответ 7
Поскольку использование стека сильно зависит от платформы и реализации, нет никакого способа сделать это напрямую, что полностью переносится. Тем не менее, вы можете создать портативный интерфейс для реализации платформы и компилятора, локализируя проблемы как можно больше. ИМХО, это был бы ваш лучший подход.
Затем реализация трассировки будет привязана к любым доступным библиотекам вспомогательной платформы. Он будет работать только тогда, когда возникает исключение, и даже тогда, только если вы вызвали его из блока catch. Его минимальный API просто вернет строку, содержащую всю трассировку.
Требование, чтобы кодер вводил обработку улова и повторной обработки в цепочке вызовов, имеет значительные затраты времени выполнения на некоторых платформах и налагает большие будущие затраты на обслуживание.
Тем не менее, если вы решите использовать механизм catch/throw, не забывайте, что даже С++ все еще имеет препроцессор C и что макросы __FILE__
и __LINE__
определены. Вы можете использовать их для включения имени исходного файла и номера строки в информацию о трассировке.