LINUX.ORG.RU

Возврат ошибки с контекстом в C

 


0

4

Язык C не имеет встроенного механизма работы с ошибками. Но работать с ошибками как-то нужно, значит это придётся делать вручную.

Конечная цель — писать такие программы, где ошибка считается нормальным поведением. После ошибки можно прибрать за собой, написать в лог, в телеграм-канал, и самостоятельно заершить работу.

Прежде чем задать вопрос по поводу работы с ошибками, опишу своё исследование чтобы были понятны требования к механизму работы с ошибками.

Есть, например, функция, которая работает с файлами. Попробуем в случае ошибки завершить работу программы:

void
kill_running_instance
        (char *pid_file)
{
        FILE *f = fopen(pid_file, "r");
        if (f == NULL) {
                fprintf(stderr, "can not open file for reading: %s: %s\n", pid_file, strerror(errno));
                exit(1);
        }
        ....
}

Проблема в том, что так мы не сможем вызвать код очистки. Попробуем исправить функцию:

void
kill_running_instance
        (bool *ok, char *pid_file)
{
        // reset error
        *ok = true;

        FILE *f = fopen(pid_file, "r");

        // indicate error
        if (f == NULL) {
                *ok = false;
                return;
        }

        ....
}

Так мы сможем извне узнать — функция нормально выполнила свою работу или что-то пошло неправильно.

Здесь появляется главная проблема. Как узнать что конкретно пошло неправильно? Хотя бы для того чтобы записать ошибку в лог и в консоль:

bool *ok;
if (kill_running_instance(&ok, pid_file), !ok) {
        // how can we find out what went wrong?
}

Это произошла ошибка открытия файла или ошибка чтения? Узнать это невозможно.

Небольшая заметка: посмотреть в errno не получится. Эта переменная используется для уточнения ошибки, а не полного описания ошибки. Например, strerror(errno) скажет Permission denied, а как извне узнать к чему это ошибка относится — всё ещё неизвестно.

Чтобы решить эту проблему, я придумал возвращать не bool *ok, а сообщение об ошибке и тип ошибки. Выглядит вот так:

void
kill_running_instance
        (struct error *e, char *pid_file)
{
        FILE *f = fopen(pid_file, "r");
        if (f == NULL) {
                ERROR_SET(e, SYSTEM, "can not open file for reading: %s: %s", pid_file, strerror(errno));
                return;
        }
        ....
}

....

struct error e;
INIT_ERROR(&e);
if (kill_running_instance(&e, pid_file), e.type != UNSET) {
        print_error(&e);
        deinit_error(&e);
        goto cleanup;
}

Контекст ошибки нам известен извне. Мы можем вывести её в консоль, в лог, отправить по почте, etc. Конечная цель выполнена!

Кстати, благодаря макросам известны имя файла и номер линии где был вызван ERROR_SET.

error определён так:

enum error_type {
        UNSET,

        /*
                Standard library function fails
        */
        SYSTEM,
};

struct error {
        enum error_type type;
        char *msg;
        char *filename;
        size_t line;
        bool is_set;
};

INIT_ERROR присваивает структуре дефолтные значения.

ERROR_SET раскрывает функцию, которая присваивает ошибке тип, сообщение, имя файла и номер линии где возникла ошибка. Стринги копируются, всё норм.

Теперь, собственно, вопросы.

  1. Много ли Open Source проектов, где работа с ошибками организована похожим образом? Буду благодарен, если покажете.

  2. Если в Open Source проектах с ошибками работают по-другому, значит, логично, они считают свой способ лучше моего. Можете показать как по-другому работают с ошибками и объяснить почему эти способы лучше моего?

  3. Буду благодарен, если подскажите какие есть риски и недостатки в моём методе работы с ошибками.

Заранее спасибо.



Последнее исправление: elonmusk (всего исправлений: 7)

Теперь, собственно, вопросы.

  1. не встречал
  2. обычно, после ошибки нет логики для продолжения работы проги
  3. трудно понять, что должен делать обработчик в случае отсутствия файла, и как должна завершиться прога.
Deleted
()
Ответ на: комментарий от Deleted

обычно, после ошибки нет логики для продолжения работы проги

Логика есть, иначе я бы не придумал свой надмозг.

Пример: нужно сохранить результаты работы пользователя хоть в каком-то виде, чтобы не потерять их.

Код будет написан примерно так:

  1. Мы вызываем функцию и она завершает свою работу с ошибкой
  2. Мы дальше не можем работать, но не завершаем работы программы сразу. Сохраняем результат работы пользователя
  3. Завершаем работу программы
elonmusk
() автор топика
Ответ на: комментарий от elonmusk

Логика есть: нужно сохранить результаты работы пользователя

Если в Open Source проектах с ошибками работают по-другому

Обычно для таких целей применяют логирование, а не делают прогу потенциально опасной.

Deleted
()

1. Достаточно много. sqlite3, libxml2, ...
2. Делать возвращаемое значение void, но возвращать ошибку через аргумент, а затем в коде делать конструкцию через запятую, - не логично. Возвращаемое значение обычно используют для получения кода ошибки (+ часто используют errno), а доп.информацию об ошибке позволяют загрузить при необходимости отдельной функцией
3. Если мы делаем код, который по нашему расчёту будет часто приводить к срабатыванию ошибки в функции, т.е. мы не будем считать, что эта ошибка должна останавливать нашу логику, то printf внутри функции могут сильно замедлить работу функции, хотя они нам могут и не требоваться (нам в этом случае достаточно только кода ошибки, чтобы понять, что это та самая ошибка, которую мы ждали)

Sorcerer ★★★★★
()
Ответ на: комментарий от Sorcerer
  1. Посмотрю, спасибо.
  2. Окей, буду знать.
  3. Я прошу прощения за неоднозначность, но я неправильно назвал макрос. ERROR_PRINTF не имеет ничего общего с printf(3). Я хотел показать, что для создания сообщения используется тот же формат, что и в printf. Сейчас исправлю, спасибо.
elonmusk
() автор топика
Ответ на: комментарий от Sorcerer

Наскольно я понял, в других проектах реализуют следующий флоу:

// error.h
#pragma once

enum error_type {
        E_DB_CORRUPT,
};

char *get_error_msg(enum error_type t);
// error.c
#include "error.h"

static char *_db_corrupt_msg = "database corrupted";

char *
get_error_msg
        (enum error_type t)
{
        switch (t) {
        case E_DB_CORRUPT:
                return _db_corrupt_msg;
        default:
                assert(false);
        }
}

Верно?

elonmusk
() автор топика

У тебя в си есть глобальный контекст. Посмотри как сделано то же errno и сделай так же.

FILE *f = call(fopen, pid_file, «r»);
FILE *f = call(fopen(pid_file, «r»));

call - макрос. У тебя есть имя функции, аргументы, строка и вся фигня. Можешь туда засунуть и проверку.

Но это всё фигня. Тебе это никак не поможет ничего откатить. Особенно это: " return;".

Варианта два. Либо используешь подобные конструкции как можно меньше(аллоцируешь сразу и заранее, в том числе и фдешки) и пишешь руками откаты где нужно, либо.

Либо просто прикручиваешь какой-нибудь лог, либо cleanup в тот макрос. Чистишь руками всё, что записалось в лог. Либо оно само чистится, если это cleanup

RibiKukan
()
Ответ на: комментарий от elonmusk

ERROR_PRINTF не имеет ничего общего с printf(3). Я хотел показать, что для создания сообщения используется тот же формат, что и в printf.

Раз используется тот же формат, то наверное вы либо будете использовать одну из printf-функций (snprintf, например). Даже если вы будете использовать свой аналог, то всё равно вы будете выполнять много действий, которых от вас, возможно, не требуют.

Sorcerer ★★★★★
()
Ответ на: комментарий от elonmusk

enum error_type

Осиль тайпдеф.

static char *_db_corrupt_msg = «database corrupted»;

Осиль си - литералы итак статические. Ну и форматирование кода.

Ну а в целом да. Возвращаешь коды/енамы. Если нужно превращать их в строки - превращаешь в строки.

Тебе так же никто не мешает сделать какое-нибудь err_t и возвращать его.

typedef struct {
  bool ok;
  uint32_t line;
} err_t;

err_t f() {
  if(true) return (err_t){.line = __LINE__, .ok = false};
  return {.ok = true};
}

if(f().ok) {}
RibiKukan
()
Ответ на: комментарий от elonmusk

Наскольно я понял, в других проектах реализуют следующий флоу: ... Верно?

Где-то да, а где-то возвращают не просто строку с ошибкой, а структурированное описание ошибки.

В каких-то проектах предоставляют возможность установить callback-функции для печати дебага и ошибок, и тогда тоже можно понять, что случилось, но при этом нет проблем с ненужным исполнением кода по печати строки, т.к. когда печатать, а когда не печатать - решает вызвавший функцию.

Sorcerer ★★★★★
()

Всегда так делаю, только вместо структуры у меня строка, и следую стандартному сишкиному протоколу:

int foo(..., int *result, char **errmsg) {
  if (condition) {
    if (errmsg) asprintf(errmsg, ...);
    return -1;
  }

  // ok
  *result = 42;
  return 0;
}

Почему строка - при ожидаемых ошибках file:line нафиг не сдались, а для неожиданных и так везде стоит принт+аборт. Почему 0-хорошо, <0-плохо - это стандарт де-факто в libc, winapi и прочих классических либах. Почему возвращается статус, а не результат:

int a, b; char *e;
if (foo(..., &a, &e) || bar(&b, &e) || ...) {
  printf(...);
  free(e);
  return;
}
printf("%d", a+b);

Попробуй свой вариант так расписать и увидишь разницу. Само собой все повторяющиеся части в макросах, тут развернул их для ясности.

При выкидывании наверх сообщения дополняются, получается что-то вроде: can’t connect to host: cannot open config file «hosts.conf»: Permission denied.

anonymous
()
Ответ на: комментарий от Deleted

Ты путаешь ожидаемые ощибки и поломку инвариантов. Нет ничего потенциально опасного в том, что очередной меню-файл-открыть наткнулся на пермишены.

anonymous
()
Ответ на: комментарий от elonmusk

Да, но так делать неудобно в разработке. Когда у тебя строгий устоичивый апи, то да. А когда ты пишешь активно код, то тебя заколебет лазить в эту таблицу соответствий и придумывать имена-енумы ошибкам, которые ты только и делаешь, что получаешь текст по коду, сообщаешь юзеру и выкидываешь. Это нахрен не нужно, если ты не либдизайнер охреневший какой-то мощной либы типа скулайта или либси.

anonymous
()
Ответ на: комментарий от Sorcerer

Вызвавший функцию обычно в трех-пяти фреймах от ошибки и ее контекст давно просрал, чтобы что-то вразумительное напечатать. Генерить текст ошибки по месту - нормально. Не надо дергать на скорость в ущерб ясности программы, пока этого не требует крайняя нужда.

anonymous
()
Ответ на: комментарий от anonymous

О боже. Какой треш.

Попробуй свой вариант так расписать и увидишь разницу.

И? Что же это дало? Кроме создания тысяч ненужны говнопеременных, синтаксического мусора и прочего?

К тому же, чего ты вообще этим хотел добиться? Проверить, что все функции были успешны? Как чистить будешь? Как узнаешь какая из них упала?

RibiKukan
()
Ответ на: комментарий от RibiKukan

Ты тоже попробуй, увидишь что дало.

К тому же, чего ты вообще этим хотел добиться? Проверить, что все функции были успешны?

Писать меньше кода, т.к. можно за один раз обработать фейл, где бы он ни случился в серии вызовов.

Как чистить будешь?

Что семантически не пустое, то и чистишь. if (fd > -1) clise(fd), if (p) foo_destroy(p), etc.

Как узнаешь какая из них упала?

Юзер/тестер - по тексту ошибки. Самой программе это не требуется.

трещ

Нетреш ты конечно же покажешь быстро, решительно? Чтобы и отладка простая была, и логи понятные, и error-path без абортов. А то на ваши поделия без слез не взглянешь, то memory cannot be read, то exception 217 file error.

anonymous
()

В С обычно делают как и в других языках. С ничем не отличается от С++ в этом плане.

То есть два варианта.

Вариант 1. Нам нужно просто узнать об ошибке

std::optional<double> my_sqrt(double d);

struct optional_double
{
    bool ok;
    double value;
};
optional_double my_sqrt_c(double d);
Вариант 2. Нам нужно различать разные ошибки которые могут произойти в функции
// error handling
struct success { string hostname; };
struct io_failure { int system_code; };
struct timed_out {};

using get_hostname_result = variant<
	success, io_failure, timed_out
>;

get_hostname_result get_hostname(ip_type ip);

struct success { char* hostname; };
typedef struct success success;

struct io_failure { int system_code; };
typedef struct io_failure io_failure;
struct timed_out {};
typedef struct timed_out timed_out;

enum hostname_error_codes { success_code , io_failure_code , timed_out_code };
typedef enum hostname_error_codes hostname_error_codes;

struct get_hostname_result_c
{
    hostname_error_codes code;
    union { // anonymous union
      success value;
      io_failure io_fail;
      timed_out t;
   };
};
typedef struct get_hostname_result_c get_hostname_result_c;

get_hostname_result_c get_hostname_c(ip_type ip);
Плюсы по сравнению с твоим подходом, мы меньше тратим память

Если при работе с ошибками мы используем enum, это признак того что нам нужно использовать union. Опять же если нам ничего не нужно, кроме факта успешно/неуспешно, то нужно посмотреть на optional типы.

fsb4000 ★★★★★
()
Ответ на: комментарий от elonmusk

Код будет написан примерно так:
Мы вызываем функцию и она завершает свою работу с ошибкой
Мы дальше не можем работать, но не завершаем работы программы сразу. Сохраняем результат работы пользователя

Ваш метод сужает область использования, возможно вам же для переиспользования написанных функций. Возьмем, для примера, тривиальную утилиту «cat». При вызове со списком файлов, один из которых несуществует, прерывания работы не производится. Просто ошибка выводится на stderr, а конечный код возврата утилиты не нулевой. Так что не надо ничего изобретать.

vodz ★★★★★
()
Ответ на: комментарий от hateyoufeel

Когда кто-то будет учиться писать на haskell и создаст тему с вопросом как правильно писать какую-нибудь штуку, другим следует писать

Программисты на Haskell вновь изобретали то, что уже 30 лет есть в других языках.

?

Программисты на С и 30 лет назад как-то обрабатывали ошибки...

fsb4000 ★★★★★
()
Ответ на: комментарий от fsb4000

Вроде в темах про хацкель принято писать про борщи.

anonymous
()
Ответ на: комментарий от hateyoufeel

И тут ты перечисляешь языки, которые могут иерархически описывать эксепшен меньшей писаниной, а не просто ловить «low level yoba encountered -127» на уровне main(). Error-path - самый не продуманный элемент в них.

anonymous
()

Язык C не имеет встроенного механизма работы с ошибками. Но работать с ошибками как-то нужно, значит придётся сменить язык.

Fixed во имя добра.

Miguel ★★★★★
()

Лучший способ - эмулировать exeptions

// begin bar.c
#include <stdlib.h>
#include <stdint.h>
extern void *__cxa_allocate_exception(size_t thrown_size);
extern void __cxa_throw (void *thrown_exception, void* *tinfo, void (*dest) (void *) );
extern void * _ZTIl; // typeinfo of long

int bar1()
{
   int64_t * p = (int64_t*)__cxa_allocate_exception(8);
   *p = 1976;
   __cxa_throw(p,&_ZTIl,0);
  return 10;
}

#include <stdint.h>
#include <cstdio>
extern "C" int bar1();
void foo()
{
  try{
    bar1();
  }catch(int64_t x){
    printf("good %ld",x);
  }
}
int main(int argc, char *argv[])
{
  foo();
  return 0;
}
Miha
()
Ответ на: комментарий от hateyoufeel

Шёл 2019 год. Посади в комнату пару-тройку сишников и спроси у них как работать со строками/обрабатывать ошибки/etc и они поубивают друг друга, доказывая, чьё кунг-фу лучше. Языку почти 50 лет, а судя по сишкотредам best practice у каждого свой.

anonymous
()
Ответ на: комментарий от Deleted

трудно понять, что должен делать обработчик в случае отсутствия файла, и как должна завершиться прога.

Нормальные языки в этом случае спрашивают у пользователя как дальше жить.

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

Нормальные - это «языки» для написания лаб? Как эти языки в плане реального софта? Кому они нужны за дверями школы?

RibiKukan
()
Ответ на: комментарий от RibiKukan

Извиняюсь за оффтоп.

Будь добр, посоветуй материал для изучения сишки. Ещё хотелось бы знать, какие тулзы использовать посоветуешь (valgrind, cppcheck, ?), какой текстовый редактор/ide, каких проектов сорцы почитать (от простого к сложным). Хотелось бы не терять время на то, чтобы самому всё находить и пробовать, а ты вроде шаришь. Благодарю.

anonymous
()
Ответ на: комментарий от anonymous

Будь добр, посоветуй материал для изучения сишки.

Доки gcc. man. В сишке нет ничего такого, что требовало какого-то там материала. Может на просторах интернета есть какие-нибудь языковые туры. Достаточно какого-нибудь справочника.

Ещё хотелось бы знать, какие тулзы использовать посоветуешь (valgrind, cppcheck, ?)

Для чего тулзы? valgrind - днище. Есть санитайзеры вместо этого. perf.

какой текстовый редактор/ide

У С/С++ был и есть только один адекватный редактор/ide - это kdevelop.

каких проектов сорцы почитать (от простого к сложным)

Не знаю даже - я никогда особо ничего не читал с мотивацией «научится». Я читал в основном то, что мне было интересно. Интересно с ТЗ «как сделано», а не как написано. Тут всё зависит от того, что интересно и нужно тебе.

Хотелось бы не терять время на то, чтобы самому всё находить и пробовать

Ну в любом случае ты будешь это делать. Ты изучаешь то, что нужно тебе, а я то, что нужно мне. Области/задачи у нас не обязательно пересекаются.

а ты вроде шаришь.

Да я не так что-бы шарю. Я в основном занимаюсь «спортивным» программированием и своей локальной фигнёй. И не факт, что ты хочешь/захочешь заниматься тем же. Тут нужна внутренняя потребность и какие-то особенности строения.

Поэтому я не особо советник по рядовому применению программирования.

RibiKukan
()
Ответ на: комментарий от RibiKukan

Что случилось? Вас обидели и вы хотите кому-то что-то доказать? Поверьте, оно этого не стоит. Просто расслабьтесь и отпустите свою проблему.

ugoday ★★★★★
()
Ответ на: комментарий от RibiKukan

Но это всё фигня

Вот я и не буду пользоваться этой фигнёй :)

Либо используешь подобные конструкции как можно меньше(аллоцируешь сразу и заранее, в том числе и фдешки) и пишешь руками откаты

Вынести весь код, который может породить ошибки, в функцию main? Я походу не понял.

Либо просто прикручиваешь какой-нибудь лог, либо cleanup в тот макрос. Чистишь руками всё, что записалось в лог. Либо оно само чистится, если это cleanup

Ничего не понял кроме того, чтобы в макрос вставить код очистки. Я не хочу так делать. Я хочу делать так:

void func1(void)
{
        FILE *f = fopen(bullshit, "r");
        if (f == NULL) {
                goto cleanup;
        }
        ....

        // код очистки находится здесь
cleanup:
        // очищаем что-нибудь и как-то сообщаем caller'у что произошла ошибка
}

void func2(char *str)
{
        // переводим строку в число
        char *end;
        errno = 0;
        long v = strtol(str, &end, 10);

        // если в строке не число
        if (str[0] == '\0' || isspace(s[0]) || end[0] != '\0' || errno) {
                goto cleanup;
        }
        ....
cleanup:
        // выполняем какую-то очистку, совсем другую чем в func1()
        // и сообщаем caller'у что произошла ошибка
}

Либо я, опять же, ННП, либо идея создать единый код очистки – не работает.

elonmusk
() автор топика
Ответ на: комментарий от i-rinat

Спасибо за наводку, хорошая либа. Но я пытался сделать не это. Я хочу:

  1. До завершения работы функции выполнять код очистки. В C используют goto, во многих ЯП – finally, в Go – defer. С этим никаких проблем нет.
  2. Вернуть контекст ошибки. Во многих ЯП кидают исключение, в Go – возвращают ошибку вторым возвращаемым «аргументом». Выглядит так:
v, err := strconv.Atoi(str)
if err != nil {
        // error context is obtained
}

Вот такое я хочу иметь в C.

elonmusk
() автор топика
Ответ на: комментарий от Sorcerer

Понял. Действительно, я использую snprintf(NULL, 0)+sprintf. Спасибо за наводку.

elonmusk
() автор топика
Ответ на: комментарий от ugoday

Сел в лужу и решил поиграть в клоуна? Играй. Только на вопрос - «зачем ты пастил люда недоязычки для лаб?» - ты не ответил. На вопрос - «зачем ты называл мёртное убогое дерьмо из-за парты „нормальным языком“?» - ты не ответил. Отвечай.

RibiKukan
()
Ответ на: комментарий от anonymous

Согласен. Поэтому я тоже выбрал именно строки.

elonmusk
() автор топика
Ответ на: комментарий от ugoday

А смысл сумасшедшим что-то отвечать? Вот вылечитесь, тогда и поговорим.

???

А «чем» вы собстно занимаетесь? Кривое, но общение. Не?

Deleted
()

Как узнать что конкретно пошло неправильно? Хотя бы для того чтобы записать ошибку в лог

я делаю обычно так:

...

ret = f1(..);
if(ret) {
  log_error(...);
}

if(!ret) {
  ret = f2(...);
  if(ret) {
    log_error(...);
  }
}

if(!ret) {
  ret = f3(...);
  if(ret) {
    log_error(...);
  }
}

if(!ret) {
  ret = f4(...);
  if(ret) {
    log_error(...);
  }
}

...

Так код не уезжает вправо и из функции одна точка выхода.

При сборке в дебаге в функции протаскиваются __FILE__, __LINE__ первыми параметрами и можно легко проследить по логу кто кого вызывал.

В log_error передаётся subsystem и level.

sergej ★★★★★
()
Ответ на: комментарий от vodz

Я, возможно, привёл неудачный пример. Вот, например, как я бы сделал утилиту cat:

void cat_file(struct error *e, char *path)
{
        struct stat buf;
        if (!stat(path, &buf)) {
                ERROR_SET(e, "can not get file status: %s: %s", path, strerror(errno));
                return;
        }
        if (!S_ISREG(buf.st_mode)) {
                ERROR_SET(e, "file not regular: %s", path);
                return;
        }
        ....
}

for (size_t i = 0; i < len; i++) {
        struct error e;
        cat_file(&e, files[i]);
        if (e.type != UNSET) {

                // выводим сообщение об ошибке
                print_error(&e);
                deinit_error(&e);

                // продолжаем работу
        }
}
elonmusk
() автор топика
Ответ на: комментарий от Deleted

А «чем» вы собстно занимаетесь?

Ну уж точно не ответами на глупые вопросы кем-то или чем-то обиженного человека.

ugoday ★★★★★
()
Ответ на: комментарий от vodz

Если функция не знает что делать с ошибкой (напечатать, записать в лог, etc.), она должна вернуть её caller’у.

вы физиологически неспособны?

Трудно разговаривать с человеком, который просто так переходит на личности.

elonmusk
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.