Это скорее эстетический вопрос, чем чисто функциональный, но я хочу знать, есть ли на него ответ. Видите ли, я программирую генератор отчетов об ошибках для проекта, над которым недавно начал работать, и он в основном основан на коде для простоты использования функций. Подумайте о errno
с различными значениями, определенными для обозначения конкретных проблем. Это распространенная система среди обработчиков ошибок. Однако я не хочу просто давать пользователю код, потому что еще до того, как начал программировать, я знаю, что случайный массив чисел часто может оказаться ошеломляющим при попытке самостоятельно диагностировать проблему.
Я хочу дать им строковое представление кода. Однако я не могу придумать прагматичного способа сделать это. Макрос stringify (#
) не будет работать, поскольку код ошибки, отправленный в функцию, неизвестен во время компиляции. Есть ли что-то помимо большого переключателя, которое могло бы решить эту проблему?
Для контекста: это мой маленький журнал ошибок; это последний шаг конвейера ошибок, если программа доберется сюда, произошла фатальная ошибка.
// Notes: We don't do a lot of error-checking in this function, beyond
// the stuff built into PushNotification. The reason for that is
// fairly simple; at this point, we don't give a darn. If the program
// gets here, something's irreversibly wrong regardless.
// Last edit: July 3rd, 2024
_Noreturn void InternalLogError(const char* caller, const char* file, u32 line,
u32 code)
{
char error_message[128];
// This is the place in which I want to add the code-to-string bit.
snprintf(error_message, 128,
"\nMemphim failed catastrophically.\n"
"Code: %d.\nCaller: %s @ %s\nLine: %d\n",
code, caller, file, line);
if (CheckShellAvailability()) puts(error_message);
else PushNotification("Memphim Error Reporter", error_message);
exit(EXIT_FAILURE);
}
@Lundin Справедливое замечание, но оно необходимо. Однако название может немного ввести в заблуждение. Неизвестная ошибка представляет собой ошибку с неизвестной обратной трассировкой, которая когда-то была правильной, а теперь нет. Очень серьезная ошибка, которой никогда не должно быть в производстве. Это была бы полностью моя вина, как и следовало бы программисту.
@Zenais Зачем использовать "%d"
с u32
?. Я бы порекомендовал int
, "%d"
для code
и unsigned
, "%u"
для line
.
@chux-ReinstateMonica Ну, code
не может быть отрицательным, поэтому беззнаковое значение кажется здесь наиболее подходящим, хотя я согласен с использованием модификаторов формата.
@chux-ReinstateMonica Прошу прощения, я не совсем понимаю, что вы имеете в виду. Под типом вы имеете в виду 32-битное целое число без знака?
@chux-ReinstateMonica Прежде чем реализовать ответ @Lundin, я просто передал целочисленный литерал, определенный препроцессором; то есть #define failed_syscall 1
и тому подобное. Теперь я передаю значение перечисления, получившее название ErrorType
.
@Zenais Хорошо, с подходом Лундина code
должно быть типа err_t
. При вашем подходе code
должен быть любым типом в диапазоне всех целочисленных констант, сформированных на основе ваших определений ошибок.
Таблица поиска строк может быть одним из очевидных способов сделать это.
typedef enum
{
ERR_NONE,
ERR_SERVERS_ON_FIRE,
ERR_LIVE_BEAVERS,
ERR_N // total number of supported errors
} err_t;
static const char* err_str[] =
{
[ERR_NONE] = "No error",
[ERR_SERVERS_ON_FIRE] = "The servers are on fire",
[ERR_LIVE_BEAVERS] = "Live beavers in the server room",
};
static_assert(sizeof(err_str)/sizeof(*err_str) == ERR_N,
"err_t and err_str are not consistent with each other");
...
void InternalLogError(..., err_t code)
{
puts(err_str[code]);
}
Или, если необходимо избегать хранения значений в отдельных местах, версия X-макроса:
#define ERR_LIST(X) \
/* code str */ \
X(ERR_NONE, "No error") \
X(ERR_SERVERS_ON_FIRE, "The servers are on fire") \
X(ERR_LIVE_BEAVERS, "Live beavers in the server room")
typedef enum
{
#define ERR_T_ENUM(code, str) code,
ERR_LIST(ERR_T_ENUM)
ERR_N // total number of supported errors
} err_t;
static const char* err_str[] =
{
#define ERR_STR_LUT(code, str) [code] = str,
ERR_LIST(ERR_STR_LUT)
};
Это гораздо более прагматичное решение, чем быстрое решение, которое я использовал до сих пор. Спасибо.
В каком-то проекте я решил, что хочу исправить аналогичную проблему. Я решил, что определение кода и текста ошибки в нескольких местах не для меня, я хочу, чтобы все было в одном месте. Вместо этого я решил, что определю код и описание ошибки в одном месте и предварительно обработаю файлы проекта, чтобы извлечь текст и сгенерировать коды ошибок в текстовую таблицу. Рассмотрим следующий файл исходного кода:
#include <errorgen.h>
int some_library_function() {
return ERROR_GEN(ERROR_SOME_ERROR, "This is error description")
}
Перед компиляцией сценарий CMake или Python просматривал все файлы. Для каждого файла, содержащего ERROR_GEN
, было создано два файла — errorgen_list.h
и errorgen_str.h
. errorgen_list.h
содержал все перечисления, собранные из ERROR_GEN
функций. errorgen_str.h
содержал "This is error description",
таблицу для преобразования кода ошибки в строку. Скрипт выглядит примерно так:
def get_all_errors_from_sources():
rereplace = re.compile(
r'ERROR_GEN\s*\(\s*(.*)\s*,\s*(".*")\s*\)\s*;',
flags=re.MULTILINE,
)
dir = os.path.dirname(__file__)
errors: List[Err] = []
for path in Path(dir).glob("**/*.c"):
for line in open(path).read():
res = rereplace.findall(line)
if res:
errors += [Err(*res)]
return errors
def prepare_sources(errors):
enumout = "/* @file */\n"
for k, v in errors:
enumout += "\t" + k + ", /// " + v + "\n"
msgout = "/* @file */\n"
for k, v in errors:
msgout += "\t" + v + ", // " + k + "\n"
return (enumout, msgout)
errors = get_all_errors_from_sources()
enumout, msgout = prepare_sources(errors)
with Path("errorgen_list.h", "w") as f:
f.write(enumout)
with Path("errorgen_str.h", "w") as f:
f.write(msgout )
Затем со следующим errorgen.h
:
enum errorgen_errors_e {
ERRORGEN_OK = 0,
ERRORGEN_START = -1000,
#include "errorgen_list.h"
ERRORGEN_STOP,
};
#define ERROR_GEN(a, b) a
const char *error_to_str(int);
И следующее errorgen.c
:
static const char *const errorsgen_str[] = {
#include "errorgen_str.h"
};
const char *error_to_str(int err) {
// error handling...
return errorsgen_str[err - ERRORGEN_START - 1];
}
Я мог бы легко сохранить код ошибки вместе со строкой ошибки в исходном коде. Это привело к появлению множества кодов ошибок, их стало легче отслеживать, а коды ошибок стали более читаемыми. Правильная зависимость должна быть смоделирована в системе сборки, чтобы система сборки знала, что нужно генерировать errorgen_*
перед компиляцией файлов.
«Я решил, что определение кода ошибки и текста ошибки в нескольких местах не для меня». В этом случае пример в опубликованном мной ответе может быть сгенерирован из X-макросов. То есть #define ERROR_LIST(X) X(0, ERR_NONE, "No error")
и так далее. Я не публиковал такой ответ, потому что это не было требованием макросов OP и X, что может немного сбить с толку.
Я добавил в свой ответ версию X-макроса.
Чтобы увидеть альтернативу, вот вариант очень хорошего ответа @Lundin.
Это инвертирует использование макросов для достижения аналогичного результата.
// errors.tbl - NB: no header guards!
#ifdef ERR_AS_STR
const char *errStrs[];
static const char *errToStr( int code ) { return errStrs[code]; }
static const char *errStrs[] = {
# define X(e,s) s
#else
enum {
# define X(e,s) e
#endif
X( ERR_NONE, "No error" ),
X( ERR_SERVERS_ON_FIRE, "The servers are on fire" ),
X( ERR_LIVE_BEAVERS, "Live beavers in the server room" ),
X( ERR_N, NULL )
# undef X
};
и
// main.c
#include <stdio.h>
#include "errors.tbl" // once as enums
#define ERR_AS_STR
#include "errors.tbl" // and now as array of strings (and getter)
#undef ERR_AS_STR
int main( void ) {
for( int i = ERR_NONE; i < ERR_N; i++ ) puts( errToStr( i ) );
return 0;
}
Это всего лишь минимальный рабочий прототип, который нужно адаптировать в соответствии с требованиями, касающимися видимости перечисления, массива и функции получения.
Я работаю на C всего около двух месяцев и поэтому совершенно не разбираюсь в этих темах, но какой смысл в файле «.tbl»? Я вижу, вы заметили что-то об отсутствии защиты заголовка, но какая разница?
@Zenais Если хочешь, можешь использовать #include "borkum.riff"
. Это просто имя файла... Я использовал .tbl
(«таблица»), чтобы указать, что его содержимое и назначение (включено дважды) отличаются от типичного .h
файла... Существуют соглашения и ожидания относительно расширений имен файлов, известные редакторам, компиляторам и человеческие читатели. В данном случае я выбрал имя .tbl
; ничего больше. Ваше здоровье! :-)
Оххх, окей! Это имеет смысл, на большинстве других языков, которые я использовал, это было именно так, понятия не имею, почему я думал, что в C все будет иначе. Спасибо :)
Почему в вашей системе есть ошибки, неизвестные во время компиляции? Наверняка вы знаете список всех возможных ошибок, которые могут произойти?