Skip to content
academia.sh

Lesson 13 / 13

Input and Output

A format string's text is known at compile time, but what it means is only known at runtime: of nine observations, one is decided at the preprocessor, two at the compiler, one at the linker, five at runtime. The bounding measurement shows that the language itself does not guarantee the match between a format string and its arguments.

Published Updated

In this lesson

  • Measures which part of a format string is decided at compile time and which at runtime
  • Reads a write function's return value and notices a truncated write
  • Distinguishes a failed read by counting how many fields a text read assigned
Contents

The question asked throughout the course was always the same: at which stage is this decision made. Input and output fits the course’s closing lesson well, because it can spread that question across all four stages within a single line. In a write call, a macro inside the format string is substituted at the preprocessor, the string’s text is fixed at the compiler, the called function’s definition is found at linking, and what the string means is only known at runtime.

This lesson measures that distribution. The measurement continues from where the core’s value computed from data line left off: there, a sum was computed at runtime; here, the question is how that sum is written.

Nine Observations

// inputoutput.c — at which stage a format string is decided
#include <stdio.h>
#include <string.h>

#define LABEL "measurement"

static void observe(const char *name, const char *stage, const char *evidence) {
    printf("%-34s%-14s%s\n", name, stage, evidence);
}

int main(void) {
    char buffer[64];
    char small[8];

    printf("%-34s%-14s%s\n", "observation", "stage", "evidence");

    /* preprocessor */
    snprintf(buffer, sizeof buffer, "the %s label was placed into the text", LABEL);
    observe("macro in the format string", "preprocessor", buffer);

    /* compiler */
    snprintf(buffer, sizeof buffer, "element count %d", (int)(sizeof buffer / sizeof buffer[0]));
    observe("buffer length", "compiler", buffer);

    const char *constant = "value: %d";
    snprintf(buffer, sizeof buffer, "text is %d characters", (int)strlen(constant));
    observe("text of the format string", "compiler", buffer);

    /* linker */
    observe("formatting function definition", "linker", "no definition in the source, the call was still resolved");

    /* runtime */
    int value = 42;
    const char *format = (value < 100) ? "%d" : "[%d]";
    snprintf(buffer, sizeof buffer, format, value);
    observe("format string selection", "runtime", buffer);

    int width = (int)strlen("four") + 1;
    snprintf(buffer, sizeof buffer, "%*d", width, value);
    observe("field width argument", "runtime", buffer);

    int wanted = snprintf(small, sizeof small, "%s", "hello, world!");
    snprintf(buffer, sizeof buffer, "wanted %d written %d", wanted, (int)strlen(small));
    observe("truncated write", "runtime", buffer);

    int assigned = 0, number = 0;
    assigned = sscanf("temperature 21 degrees", "temperature %d", &number);
    snprintf(buffer, sizeof buffer, "assigned fields %d value %d", assigned, number);
    observe("reading from text", "runtime", buffer);

    int unmatched = sscanf("temperature twentyone", "temperature %d", &number);
    snprintf(buffer, sizeof buffer, "assigned fields %d", unmatched);
    observe("non-matching read", "runtime", buffer);

    printf("\n%-14s%d\n", "preprocessor", 1);
    printf("%-14s%d\n", "compiler", 2);
    printf("%-14s%d\n", "linker", 1);
    printf("%-14s%d\n", "runtime", 5);
    printf("total observations: %d\n", 9);
    return 0;
}
observation                       stage         evidence
macro in the format string        preprocessor  the measurement label was placed into the text
buffer length                     compiler      element count 64
text of the format string         compiler      text is 9 characters
formatting function definition    linker        no definition in the source, the call was still resolved
format string selection           runtime       42
field width argument              runtime          42
truncated write                   runtime       wanted 13 written 7
reading from text                 runtime       assigned fields 1 value 21
non-matching read                 runtime       assigned fields 0

preprocessor  1
compiler      2
linker        1
runtime       5
total observations: 9

BS14. The LABEL macro was placed into the format string before the compiler ever saw the source. To the preprocessor this is a text substitution; it does not know the string is a format string, nor does it know the difference between %s and %d. This is why a format string built with a macro passes the preprocessor stage without any trouble, even if it does not match the argument list.

BS15. The buffer’s length is known at compile time: sizeof buffer / sizeof buffer[0] is a constant expression and gives 64. That an array’s length is known at compile time was measured in the Arrays and Strings lesson; here the same information is used as a write call’s bound. The format string’s text falls into the same class — value: %d is nine characters, and this number is read from the source, not from runtime.

BS16. The formatting function’s definition is not in the source. Only its declaration, coming from a header file, is present, and yet the call was resolved regardless: the linker found the definition. This is the same as the function in another unit line in the Functions lesson; the only difference is that this time the definition sits not in a separate source unit, but in the language’s standard library.

BS17. The remaining five observations fall to runtime, and what they all share is this: the result can change without the source text changing. The format string was chosen by a condition (value < 100 is true, so the bracket-free form was used), the field width came from an argument ("four" opened a five-character field), a write was truncated, a number was read from a piece of text, and one read was unable to assign any field at all.

What the Return Value Says

BS18. The truncated write line shows what the return value is: wanted 13 written 7. A write function returns not the number of characters it managed to fit into the buffer, but how many characters it would have written had it been able to fit them all. When a thirteen-character piece of text is placed into an eight-byte buffer, seven characters and a terminator were written, but the return value stayed at thirteen. This divergence between the two numbers is the only deterministic way of telling whether truncation occurred: if the return value is smaller than the buffer’s length, the write is complete.

BS19. On the read side, the return value counts something else: how many fields were assigned. The same format string assigned one field in one piece of text, zero in the other. The second piece of text was also valid text and the program did not crash — only the number variable stayed untouched. A read’s failure is reported not with an error value but with a count, and a source that does not read that count assumes an unassigned variable is assigned.

The Bounding Measurement

BS20. The language itself does not guarantee the match between a format string and its arguments. In the measurement above, the format string came, at one point, from a variable — the format pointer, chosen by a condition. In that call, the compiler had no information available to check the arguments against, by looking at the string’s contents; the string was chosen at runtime. When a format string is a constant written in the source, the compiler does have the information to check the match, and common compilers offer this as an optional warning. The check itself is not a guarantee of the language: the standard classifies an argument mismatching its format specifier directly as undefined behavior and prescribes no check at all. When the string comes from a variable, even that information is gone — the check drops, and the responsibility passes entirely to the writer.

This is the sharpest result of the course’s four-stage question: the same call shifts from the compiler class to the runtime class depending on where the format string comes from. The source text is almost identical; the only thing that changes is at which stage a piece of information is available.

Closing Table

The decisions measured across thirteen lessons are gathered below. The stage cell is taken from what is written in each lesson’s own text; lessons measuring more than one stage carry both.

Lesson Decision measured Stage Bounding measurement
The Place of C The distribution of the core’s fourteen observations all four Four stages are not a complexity, they are four separate checkpoints
Compilation Stages The side-by-side run of one line from each stage all four Stages run in sequence and do not go back; the compiler does not correct the text the preprocessor produced
Header Files A declaration copied as text preprocessor · linker A header file is not a contract; the language does not check that a declaration matches its definition
The Preprocessor A macro placing text without knowing type preprocessor A macro is not a function; its argument can be substituted more than once
Compiler Warnings and Standards At which stage a warning is born compiler A warning is not an error, and a program that compiles without warnings is not thereby correct
Data Types A type’s range and integer promotion compiler Overflow behavior depends on a type’s sign: defined for unsigned, undefined for signed
Variables, Scope and Storage Duration A name’s visibility and how long a value lives compiler · runtime · linker The same name can be two separate objects in two units; it is the linker that determines this
Operators An operator changing meaning by type compiler Precedence determines grouping, it does not always determine evaluation order
Control Flow Which branch is followed runtime · preprocessor Which branches exist can be determined by the preprocessor rather than runtime
Functions A call bound to a definition linker Neither the compiler nor the linker checks a mismatch between declaration and definition across units
Arrays and Strings Array length versus string length compiler · runtime An array’s length is known at compile time, but this information does not pass to a function
Structs, Unions and Enumerations Field layout and constants’ values compiler A union does not track on its own which field was last written
Input and Output A format string’s interpretation runtime The compiler’s match check drops once a format string comes from a variable

Summary

  • A single write call can carry all four stages at once: the macro was decided at the preprocessor, the buffer length and the string’s text at the compiler, the function’s definition at the linker, the string’s meaning at runtime.
  • Of the nine observations, one was measured at the preprocessor, two at the compiler, one at the linker, five at runtime; input and output is where the course’s four classes mix most densely.
  • A write function’s return value is not the number of characters written but the number wanted: thirteen were wanted, seven were written, the return stayed at thirteen.
  • A read function’s return value is the number of assigned fields; in a read that returns zero, the target variable stays untouched, and the only way to notice this is to read that count.
  • While a format string is a constant in the source, the compiler can check argument agreement; once the string comes from a variable, the same call shifts to the runtime class and the check drops.

Course Wrap-Up

One single question was asked across thirteen lessons: at which stage is this decision made. The common definition’s fourteen observations had been distributed as 4 preprocessor, 4 compiler, 3 linker, 3 runtime, and this distribution never changed throughout the course — what changed was which mechanism fell into which class.

The Language and Compilation topic put the question to the path along which a source turns into a program: to the existence of the four stages, to declarations being shared as text, to the preprocessor not knowing type, and to where a warning is born. The Basic Syntax topic carried the same question into the language’s own structures: to the value a type carries, to a name’s visibility and a value’s storage duration being separate questions, to an operator changing meaning by type, to where a call is resolved, to when a length is known, and to how a record type’s fields are laid out.

The course’s single most repeated finding is this: the same source text behaves differently depending on at which stage a piece of information is available. An array’s length is known at compile time, a string’s at runtime; a macro does not know type, a compiler does; a name is written the same way in two units but may not be the same object.

The next course looks at memory and pointers: where a value sits, who allocates it, and when it is released. In C, all of these questions are written in the source, and none of them is checked on its own.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close