Error Handling in C



    Introduction


    Errors, alas, are inevitable, therefore their processing occupies a very important place in programming. And if algorithmic errors can be detected and corrected while writing and testing the program, then runtime errors cannot be avoided in principle. Today we look at the functions of the standard library ( C Standard Library ) and POSIX used in error handling.

    Errno variable and error codes



    errno is a variable storing the integer code of the last error. Each thread has its own local version of errno , which determines its safety in a multi-threaded environment. Typically, errno is implemented as a macro that unfolds into a function call that returns a pointer to an integer buffer. When the program starts, errno is zero.

    All error codes have positive values, and can be used in the #if preprocessor directives . For convenience and portability, the header file defines macros that correspond to the error codes.

    The ISO C standard defines the following codes:

    • To EDOM - ( E rror dom ain) error domain.
    • EILSEQ - ( E rror i nva l id seq uence) erroneous byte sequence.
    • To ERANGE - ( E rror range ) the result is too large.

    Other error codes (several tens) and their descriptions are defined in the POSIX standard. In addition, standard function specifications usually indicate the error codes they use and their descriptions.

    A simple script prints error codes, their symbolic names and descriptions to the console:

    #!/usr/bin/perl
    use strict;
    use warnings;
    use Errno;
    foreach my $err (sort keys (%!)) {
        $! = eval "Errno::$err";
        printf "%20s %4d   %s\n", $err, $! + 0, $!
    }

    If the function call fails, then it sets the errno variable to a non-zero value. If the call succeeds, the function usually does not check or change the errno variable . Therefore, before calling the function, it must be set to 0.

    Example:

    /* convert from UTF16 to UTF8 */
    errno = 0;	
    n_ret = iconv(icd, (char **) &p_src, &n_src, &p_dst, &n_dst);   
    if (n_ret == (size_t) -1) {
        VJ_PERROR();
        if (errno == E2BIG)  
            fprintf(stderr, " Error : input conversion stopped due to lack of space in the output buffer\n");
        else if (errno == EILSEQ)  
            fprintf(stderr, " Error : input conversion stopped due to an input byte that does not belong to the input codeset\n");
        else if (errno == EINVAL)  
            fprintf(stderr, " Error : input conversion stopped due to an incomplete character or shift sequence at the end of the input buffer\n");
    /* clean the memory */   
        free(p_out_buf);
        errno = 0;
        n_ret = iconv_close(icd);      
        if (n_ret == (size_t) -1)  
            VJ_PERROR();
        return (size_t) -1; 
    }

    As you can see, error descriptions in a function specification are iconv()more informative than in .

    Errno Functions


    Having received the error code, I want to immediately get its description from it. Luckily, ISO C offers a whole host of useful features.


    void perror(const char *s);

    Prints the contents of a string in stderrs , followed by a colon, a space, and an error message. Then prints a newline character '\n'.

    Example:

    /*
    //  main.c
    //  perror example
    //
    //  Created by Ariel Feinerman on 23/03/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) 
    {
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(file_name, "rb");
        if (file) {
            // Do something useful. 
            fclose(file);
        }
        else {
            perror("fopen() ");
        }
        return EXIT_SUCCESS;
    }


    char * strerror (int errnum);
    Returns a string containing a description of the error errnum. The language of the message depends on the locale (German, Hebrew, and even Japanese), but usually only English is supported.

    /*
    //  main.c
    //  strerror example
    //
    //  Created by Ariel Feinerman on 23/03/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) 
    {
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(file_name, "rb");
        // Save error number. 
        errno_t error_num = errno;
        if (file) {
            // Do something useful. 
            fclose(file);
        }
        else {
            char *errorbuf = strerror(error_num);
            fprintf(stderr, "Error message : %s\n", errorbuf);
        }
        return EXIT_SUCCESS;
    }

    strerror()not a safe function. Firstly, the string returned by it is not constant. At the same time, it can be stored in static or in dynamic memory, depending on the implementation. In the first case, changing it will lead to a runtime error. Secondly, if you decide to save a pointer to a string, and then call the function with the new code, all previous pointers will already point to a new string, because it uses one buffer for all lines. Thirdly, its behavior in a multi-threaded environment is not defined in the standard. However, in QNX it is declared as thread safe.

    Therefore, two very useful features were proposed in the new ISO C11 standard .

    size_t strerrorlen_s(errno_t errnum);

    Returns the length of the string describing the error errnum.

    errno_t strerror_s(char *buf, rsize_t buflen, errno_t errnum);

    Copy line with error descriptionerrnumto the buffer buflength buflen.

    Example:

    /*
    //  main.c
    //  strerror_s example 
    //
    //  Created by Ariel Feinerman on 23/02/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #define __STDC_WANT_LIB_EXT1__ 1
    #include 
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) 
    {
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(file_name, "rb");
        // Save error number. 
        errno_t error_num = errno;
        if (file) {
            // Do something useful. 
            fclose(file);
        }
        else {
    #ifdef __STDC_LIB_EXT1__
        size_t error_len = strerrorlen_s(errno) + 1;
        char error_buf[error_len];
        strerror_s(error_buf, error_len, errno);
        fprintf(stderr, "Error message : %s\n", error_buf);
    #endif
        }
        return EXIT_SUCCESS;
    }

    Functions are included in Annex K (Bounds-checking interfaces), which caused a lot of controversy . It is not required to be executed and is not fully implemented in any of the free libraries. Open Watcom C / C ++ (Windows), Slibc (GNU libc) and Safe C Library (POSIX), in the latter, unfortunately, these two functions are not implemented. However, they can be found in commercial development environments and real-time systems, Embarcadero RAD Studio , INtime RTOS , QNX .

    The POSIX.1-2008 standard defines the following functions:

    char *strerror_l(int errnum, locale_t locale);

    Returns a string containing a localized error description errnumusinglocale. Safe in multi-threaded environments. Not implemented on Mac OS X , FreeBSD , NetBSD , OpenBSD , Solaris, and other commercial UNIX. Implemented on Linux, MINIX 3, and Illumos (OpenSolaris).

    Example:

    /*
     //  main.c
     //  strerror_l example – works on Linux, MINIX 3, Illumos
     //
     //  Created by Ariel Feinerman on 23/03/17.
     //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
     */
    #include 
    #include 
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) 
    {
        locale_t locale = newlocale(LC_ALL_MASK, "fr_FR.UTF-8", (locale_t) 0);
        if (!locale) {
            fprintf(stderr, "Error: cannot create locale.");
            exit(EXIT_FAILURE);
        }
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(tmpnam(file_name, "rb");
        // Save error number. 
        errno_t error_num = errno;
        if (file) {
            // Do something useful. 
            fclose(file);
        }
        else {
            char *error_buf = strerror_l(errno, locale);
            fprintf(stderr, "Error message : %s\n", error_buf);
        }
        freelocale(locale);
        return EXIT_SUCCESS;
    }

    Conclusion:

    Error message : Aucun fichier ou dossier de ce type

    int strerror_r(int errnum, char *buf, size_t buflen);

    Copies the line with the description of the error errnumto the buffer buflength buflen. If buflenless than the length of the string, the excess is truncated. Safe in multi-threaded environment. Implemented on all UNIX.

    Example:

    /*
    //  main.c
    //  strerror_r POSIX example
    //
    //  Created by Ariel Feinerman on 25/02/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    #include 
    #define MSG_LEN 1024 
    int main(int argc, const char * argv[]) 
    {
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(file_name, "rb");
        // Save error number. 
        errno_t error_num = errno;	
        if (file) {
            // Do something useful.
            fclose(file);
        }
        else {
            char error_buf[MSG_LEN];
            errno_t error = strerror_r (error_num, error_buf, MSG_LEN);
            switch (error) {
                case EINVAL:
                        fprintf (stderr, "strerror_r() failed: invalid error code, %d\n", error);
                        break;
                case ERANGE:
                        fprintf (stderr, "strerror_r() failed: buffer too small: %d\n", MSG_LEN);
                case 0:
                        fprintf(stderr, "Error message : %s\n", error_buf);
                        break;
                default: 
                        fprintf (stderr, "strerror_r() failed: unknown error, %d\n", error);
                        break;
            }
        }
        return EXIT_SUCCESS;
    }
    

    Alas, strerrorlen_s()they did not determine any analogue in POSIX, so the string length can only be determined experimentally. Usually 300 characters are enough for the eyes. The GNU C Librarystrerror() uses a 1024-character buffer in its implementation . But you never know, what if?

    Example:

    /*
     //  main.c
     //  strerror_r safe POSIX example
     //
     //  Created by Ariel Feinerman on 23/03/17.
     //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
     */
    #include 
    #include 
    #include 
    #include 
    #define MSG_LEN 1024 
    #define MUL_FACTOR 2
    int main(int argc, const char * argv[]) 
    {
        // Generate unique filename.
        char *file_name = tmpnam((char[L_tmpnam]){0});
        errno = 0;
        FILE *file = fopen(file_name, "rb");
        // Save error number. 
        errno_t error_num = errno;
        if (file) {
            // Do something useful.
            fclose(file);
        }
        else {
            errno_t error = 0;
            size_t error_len = MSG_LEN; 
            do {
                char error_buf[error_len];
                error = strerror_r (error_num, error_buf, error_len);
                switch (error) {
                        case 0:
                                fprintf(stderr, "File : %s\nLine : %d\nCurrent function : %s()\nFailed function : %s()\nError message : %s\n", __FILE__, __LINE__, __func__, "fopen", error_buf);
    	                    break;
                        case ERANGE: 
                                error_len *= MUL_FACTOR;
                                break;
                        case EINVAL: 
                                fprintf (stderr, "strerror_r() failed: invalid error code, %d\n", error_num);
                                break;
                        default:
                                fprintf (stderr, "strerror_r() failed: unknown error, %d\n", error);
                                break;
                }
            } while (error == ERANGE);
        }
        return EXIT_SUCCESS;
    }

    Conclusion:

    File : /Users/ariel/main.c
    Line : 47
    Current function : main()
    Failed function : fopen()
    Error message : No such file or directory

    Macro assert ()



    void assert(expression)

    A macro checking a condition expression(its result must be a number) at runtime. If the condition is not satisfied ( expressionzero), it prints a stderr values __FILE__, __LINE__, __func__and expressionas a string, and then calls the function abort().

    /*
    //  main.c
    //  assert example
    //
    //  Created by Ariel Feinerman on 23/03/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) {
        double x = -1.0;
        assert(x >= 0.0);
        printf("sqrt(x) = %f\n", sqrt(x));   
        return EXIT_SUCCESS;
    }

    Conclusion:

    Assertion failed: (x >= 0.0), function main, file /Users/ariel/main.c, line 17.

    If a macro is NDEBUGdefined before inclusion , it assert()expands to ((void) 0)and does nothing. Used for debugging purposes.

    Example:

    /*
    //  main.c
    //  assert_example
    //
    //  Created by Ariel Feinerman on 23/03/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #NDEBUG
    #include 
    #include 
    #include 
    #include 
    int main(int argc, const char * argv[]) {
        double x = -1.0;
        assert(x >= 0.0);
        printf("sqrt(x) = %f\n", sqrt(x));   
        return EXIT_SUCCESS;
    }

    Conclusion:

    sqrt(x) = nan

    Atexit () , exit (), and abort () functions



    int atexit(void (*func)(void));

    It registers the functions that are called upon normal completion of the program in the reverse order of their registration. You can register up to 32 functions.

    _Noreturn void exit(int exit_code);

    It causes normal program termination, returns a number on Wednesday exit_code. ISO C standard defines only three possible values 0, EXIT_SUCCESSand EXIT_FAILURE. In this case, the functions registered through atexit()are dropped and the input / output streams are dropped and temporary files are destroyed, after which control is transferred to the environment. The function exit()is called main()when executing return or reaching the end of the program.

    The main advantage exit()is that it allows you to complete the program not only frommain(), but also from any nested function. For example, if a condition is fulfilled (or not fulfilled) in a deeply nested function, after which further execution of the program loses all meaning. A similar technique (early exit) is widely used when writing daemons, system utilities, and parsers. In interactive programs with an infinite main loop, exit()you can use to exit the program when you select the desired menu item.

    Example:

    /*
    //  main.c
    //  exit example
    //
    //  Created by Ariel Feinerman on 17/03/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    void third_2(void) 
    {
        printf("third #2\n");          // Does not print.
    }
    void third_1(void) 
    {
        printf("third #1\n");          // Does not print.
    }
    void second(double num) 
    {
        printf("second : before exit()\n");	// Prints.
        if ((num < 1.0f) && (num > -1.0f)) {
            printf("asin(%.1f) = %.3f\n", num, asin(num));
            exit(EXIT_SUCCESS);
        }
        else {
            fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]\n", num);
            exit(EXIT_FAILURE);
        }
        printf("second : after exit()\n");	// Does not print.
    }
    void first(double num) 
    {
        printf("first : before second()\n")
        second(num);
        printf("first : after second()\n");          // Does not print.
    }
    int main(int argc, const char * argv[]) 
    {
        atexit(third_1); // Register first handler. 
        atexit(third_2); // Register second handler.
        first(-3.0f);
        return EXIT_SUCCESS;
    }

    Conclusion:

    first : before second()
    second : before exit()
    Error: -3.0 is beyond the range [-1.0; 1.0]
    third #2
    third #1

    _Noreturn void abort(void);

    Causes a program to crash if the signal has not been intercepted by the signal handler. Temporary files are not destroyed; closing streams is determined by the implementation. The most important difference between abort () calls is exit(EXIT_FAILURE)that the first sends a signal to the program SIGABRT, you can intercept it and perform the necessary actions before the program ends. The core dump file is written , if enabled. When launched in the debugger, it intercepts the signal SIGABRTand stops the program, which is very convenient in debugging.

    Example:

    /*
    //  main.c
    //  abort example
    //
    //  Created by Ariel Feinerman on 17/02/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    void third_2(void) 
    {
        printf("third #2\n");          // Does not print.
    }
    void third_1(void) 
    {
        printf("third #1\n");          // Does not print.
    }
    void second(double num) 
    {
        printf("second : before exit()\n");	// Prints.
        if ((num < 1.0f) && (num > -1.0f)) {
            printf("asin(%.1f) = %.3f\n", num, asin(num));
            exit(EXIT_SUCCESS);
        }
        else {
            fprintf(stderr, "Error: %.1f is beyond the range [-1.0; 1.0]\n", num);
            abort();
        }
        printf("second : after exit()\n");	// Does not print.
    }
    void first(double num) 
    {
        printf("first : before second()\n");
        second(num);
        printf("first : after second()\n");          // Does not print.
    }
    int main(int argc, const char * argv[]) 
    {
        atexit(third_1); // register first handler 
        atexit(third_2); // register second handler
        first(-3.0f);
        return EXIT_SUCCESS;
    }

    Conclusion:

    first : before second()
    second : before exit()
    Error: -3.0 is beyond the range [-1.0; 1.0]
    Abort trap: 6

    The output in the debugger:

    $ lldb abort_example 
    (lldb) target create "abort_example"
    Current executable set to 'abort_example' (x86_64).
    (lldb) run
    Process 22570 launched: '/Users/ariel/abort_example' (x86_64)
    first : before second()
    second : before exit()
    Error: -3.0 is beyond the range [-1.0; 1.0]
    Process 22570 stopped
    * thread #1: tid = 0x113a8, 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
        frame #0: 0x00007fff89c01286 libsystem_kernel.dylib`__pthread_kill + 10
    libsystem_kernel.dylib`__pthread_kill:
    ->  0x7fff89c01286 <+10>: jae    0x7fff89c01290            ; <+20>
        0x7fff89c01288 <+12>: movq   %rax, %rdi
        0x7fff89c0128b <+15>: jmp    0x7fff89bfcc53            ; cerror_nocancel
        0x7fff89c01290 <+20>: retq   
    (lldb) 

    In case of a critical error, use the function abort(). For example, if an error occurred while allocating memory or writing to a file. Any further actions may aggravate the situation. If you complete the execution in the usual way, in which the input / output streams are reset, you can lose still intact data and temporary files, so the best solution would be to write a dump and exit the program instantly.

    In the case of a non-critical error, for example, you could not open the file, you can safely exit through exit().

    The functions setjmp () and longjmp ()


    So we come to the most interesting - the functions of non-local transitions. setjmp()and they longjmp()work according to the goto principle , but unlike it, they allow you to jump from one place to another within the entire program, and not just one function.


    int setjmp(jmp_buf env);

    Saves information about the context of program execution (microprocessor registers, etc.) c env. Returns 0if it was called directly or valueif from longjmp().

    void longjmp(jmp_buf env, int value);

    Restores the execution context of the program from env, returns control setjmp()and passes to it value.

    Example:

    /*
    //  main.c
    //  setjmp simple
    //
    //  Created by Ariel Feinerman on 18/02/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    static jmp_buf buf;
    void second(void) 
    {
        printf("second : before longjmp()\n");	// prints
        longjmp(buf, 1);						// jumps back to where setjmp was called – making setjmp now return 1
        printf("second : after longjmp()\n");	// does not prints
        // <- Here is the point that is never reached. All impossible cases like your own house in Miami, your million dollars, your nice girl, etc.
    }
    void first(void) 
    {
        printf("first : before second()\n");
        second();
        printf("first : after second()\n");          // does not print
    }
    int main(int argc, const char * argv[]) 
    {
        if (!setjmp(buf))
            first();                // when executed, setjmp returned 0
        else                        // when longjmp jumps back, setjmp returns 1
            printf("main\n");       // prints
        return EXIT_SUCCESS;
    }

    Conclusion:

    first : before second()
    second : before longjmp()
    main

    Using setjmp()and longjmp(), an exception mechanism can be implemented. In many high-level languages ​​(for example, in Perl ), exceptions are implemented through them.

    Example:

    /*
    //  main.c
    //  exception simple
    //
    //  Created by Ariel Feinerman on 18/02/17.
    //  Copyright  2017 Feinerman Research, Inc. All rights reserved.
    */
    #include 
    #include 
    #include 
    #include 
    #define str(s) #s
    static jmp_buf buf;
    typedef enum {
        NO_EXCEPTION    = 0,
        RANGE_EXCEPTION = 1,
        NUM_EXCEPTIONS
    } exception_t;
    static char *exception_name[NUM_EXCEPTIONS] = {
        str(NO_EXCEPTION),
        str(RANGE_EXCEPTION)
    };
    float asin_e(float num) 
    {
        if ((num < 1.0f) && (num > -1.0f)) {
            return asinf(num);
        }	
        else {
            longjmp(buf, RANGE_EXCEPTION);        // | @throw  
        }
    }
    void do_work(float num) 
    {
        float res = asin_e(num);
        printf("asin(%f) = %f\n", num, res);         
    }
    int main(int argc, const char * argv[]) 
    {
        exception_t exc = NO_EXCEPTION;
        if (!(exc = setjmp(buf))) {        // |	
            do_work(-3.0f);                // | @try
        }                                  // |
        else {                                                                               // | 
            fprintf(stderr, "%s was hadled in %s()\n", exception_name[exc], __func__);       // | @catch
        }                                                                                    // | 
        return EXIT_SUCCESS;
    }

    Conclusion:

    RANGE_EXCEPTION was hadled in main()

    Attention! Functions setjmp()and longjmp() are primarily used in system programming, and their use in client code is not recommended. Their use impairs the readability of the program and can lead to unpredictable errors. For example, what happens if you do not jump up the stack — into the calling function, but into a parallel one that has already completed execution?

    Information



    Also popular now: