Proven C Book한국어 GitHub

75 Diagnosis and control — <errno.h>, <assert.h>, <signal.h>, <setjmp.h>

What to know first

chapter 51, Errors and contracts · reporting an error as a value
chapter 52, Undefined behaviour · after a contract is broken

Looking back

Chapter 51 said C’s ways of reporting an error are only three — the return value, global state, and stopping the program. Then exactly when can the global called errno be believed?

A. It can be believed only right after a failure has been confirmed. The standard’s rule is two-layered. First, a successful call may also put a value in errno — so the order must not be “nonzero means failure” but “read the reason after the function has reported failure”. Second, the value may be overwritten by the next library call at any time — so once read it is stored at once. These two rules are the price of the design that carries errors in global state.

The need for this chapter, and its context

Four headers are gathered into one chapter because they share a single job: what to do when things have gone wrong. And the slot matters: why chapter 51′s “errors are values” is such a strong discipline only becomes convincing after seeing what the alternatives actually look like.

By the end of this chapter

We look in one place at the four headers used when a program has gone wrong. The global holding an error number, the assertion that catches contract violations, signals flying in from outside, and the jump that skips over the stack. Why the discipline set up in chapter 51, “errors are values”, matters so much becomes clear on seeing the real shape of the alternatives.

The questions this chapter answers

  1. Why was assert made to switch off wholesale with NDEBUG — is it not better to always check?

75.1 errno — the price of carrying errors in a global

examples-en/ch75/errno_demo.c

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    /* (1) even a successful call may touch errno — so it is set to 0 just before */
    errno = 0;
    FILE *ok = fopen("errno_demo_tmp.txt", "w");
    printf("errno after success = %d\n", errno);
    if (ok) { fputs("x", ok); fclose(ok); }

    /* (2) on failure the reason is left in errno */
    errno = 0;
    FILE *f = fopen("/definitely/not/here.txt", "r");
    if (!f) {
        printf("failure: errno=%d, strerror=[%s]\n", errno, strerror(errno));
        perror("perror output");   /* context attached in front, sent to stderr */
    }

    /* (3) let another call in between and errno is overwritten */
    errno = 0;
    f = fopen("/definitely/not/here.txt", "r");
    int saved = errno;                    /* saved at once */
    printf("cleaning up...\n");           /* printf may change errno */
    printf("the saved errno=%d (%s)\n", saved, strerror(saved));

    /* (4) strtol reports going out of range through errno */
    errno = 0;
    long v = strtol("999999999999999999999", NULL, 10);
    printf("strtol out of range: errno==ERANGE? %s (value=%ld)\n",
           errno == ERANGE ? "yes" : "no", v);

    remove("errno_demo_tmp.txt");
    return 0;
}

Output

perror output: No such file or directory
errno after success = 0
failure: errno=2, strerror=[No such file or directory]
cleaning up...
the saved errno=2 (No such file or directory)
strtol out of range: errno==ERANGE? yes (value=9223372036854775807)

The output shows the rules exactly. Set it to 0 just before the call, read it after confirming failure, and store it before doing anything else. Merely slipping one line of printf in between can change the value.

errno looks like a variable but is a macro. In a program running along several strands each strand must see a different value, so the implementation defines it as a macro pointing at thread-local storage.

functionwhat it doesnote
strerror(n)error number → a sentence★ a static buffer. not thread-safe
perror(s)s: reason to stderrthe habit of attaching a context string
strerror_rfills a caller’s bufferPOSIX. there are two editions, hence confusion
strerror_sthe same intentannex K (chapter 78)

Table 76.1

The error numbers the standard names are only three — EDOM (domain), ERANGE (range), EILSEQ (encoding). The rest, such as ENOENT and EACCES, are settled by POSIX or the platform. That is, code comparing errno values is that much less portable.

Q. Why was assert made to switch off wholesale with NDEBUG — is it not better to always check?

A. Because what assert checks is the programmer’s assumption, not the user’s input. “By the time we are here, p is not null” must be true whenever the code is right; if it is false, that is a bug. The original design puts such checks densely during development and removes their cost from the shipped build.

Two things follow. First, no side effects inside assertassert(pop(&s) == 3) disappears entirely in the release build. Second, checks on user input, file contents and network data must be made by code that always runs, not by assert. In chapter 51′s vocabulary of contracts, assert confirms preconditions during development; reporting failure as a value is another job.

75.2 assert — the cheapest way to write a contract as code

The macro learned in chapter 51. Pinning down the rules again:

Counter-example. Making assert do work

assert(fclose(f) == 0);      /* in a release build the fclose itself vanishes */
assert(i++ < n);             /* there arises a build in which i is not incremented */

Separate the check from the side effect.

int rc = fclose(f);
assert(rc == 0);
(void)rc;                    /* prevents an unused warning in release */

C11 brought in static_assert (in C23 it can be used without _Static_assert). It confirms at compile time, so the run-time cost is zero, and it is used on sizeof and constant conditions.

static_assert(sizeof(int) >= 4, "this code assumes a 32-bit int");

75.3 Signals — see the next chapter

<signal.h> deals with events that come from outside the program (Ctrl+C, an invalid memory access, an arithmetic error). The standard defines only six signals (SIGINT, SIGSEGV, SIGFPE, SIGILL, SIGABRT, SIGTERM); the rest belong to the platform.

One discipline is worth putting down here — there is almost nothing a handler may do. Neither printf nor malloc may be called. So the working idiom becomes “the handler raises a flag, the main flow does the work.”

static volatile sig_atomic_t stop = 0;
static void on_int(int sig) { (void)sig; stop = 1; }
/* in the main loop: while (!stop) { ... } */

There is enough in this header to fill a chapter, so it has one — the history, the shape of the functions with their arguments and return values, what sig_atomic_t really is, POSIX’s sigaction and the inside of its structures, and the real uses in servers, shells and terminals, all in chapter 76.

75.4 Non-local jumps — setjmp/longjmp

A device that remembers the present place with setjmp and comes back later from somewhere deep with longjmp. It is an attempt to make something like exceptions in a language that has none, and the price is correspondingly large — nobody cleans up the resources held by the functions that were skipped over.

This header too has enough in it for a chapter of its own — the return-value rule of the two words, the four contexts in which setjmp may appear, the inside of jmp_buf, the volatile rule measured on a real build, the uses in real software such as libjpeg and Lua, and today’s alternatives, all in chapter 77.

In practice. The shape of code made by the way of handling errors

Write the same program three ways and its shape splits like this.

  • Return value + errno: an if attaches to every call and the error paths are visible. Cumbersome, but the flow is honest.
  • setjmp/longjmp: the body becomes clean but a human must remember all the resource cleanup, and where control jumps to is not visible in the code.
  • goto cleanup: the compromise most widely settled in C codebases. On failure everything gathers at one place and cleans up in reverse order. The Linux kernel pinned this pattern down as a convention, and it is exactly the disciplined use meant by chapter 30′s “use goto with restraint”.

All three solve the same problem of “handling failure as a value and not forgetting the cleanup”. Part XII’s library lays a fourth answer on top — making failure into a type.

Recap

Diagnosis and control in summary.

toolwhere it is usedrule
errnothe reason a library call failed0 just before, read after confirming failure, store at once
assertinternal invariantsno side effects, not used for checking input
static_assertcompile-time assumptionszero cost
signalexternal events such as Ctrl+Conly a flag in the handler
SIGSEGVcatching it does not allow recovery
setjmp/longjmpspecial recoveryvalues undetermined unless volatile, no cleanup
the practical idiomcleanup on failuregather at one place with goto cleanup

Table 76.2

That is as far as the headers that existed from the C89 days. The next chapter is what the standard has added since — and the story of what became of the attempt to make “safe functions”.