Part II · C and the Machine

09 Files, Bits, and Function Pointers: ADTs Behind a Header

TutorWeek 6 and 8Lab: The station reads its logMemoryInterfaces

What still holds

An interface is a promise; the representation behind it is a secret.

The agent may: explain file I/O errors and hex dumps; write no code you submit.

Learning objectives

After this chapter you should be able to:

Do unaided:

  1. Write C that reads and writes text and binary files, checks every fopen, fgets, fread, fwrite and fclose, and tells a clean end of file from a short read and from an I/O error.
  2. Compute &, |, ^, ~, << and >> on the station's flag byte by hand, and explain which shifts are undefined.
  3. Predict, byte by byte, the hex dump of a little-endian binary log written field by field.
  4. Declare and call a function pointer, write a qsort comparator, and dispatch readings through a table of handlers.
  5. Explain what an opaque type hides from its callers, and what hiding it costs.

May delegate:

  1. Explanations of file I/O error messages and of a hex dump, to the tutor, after you have written your own reading first.

Opening case

A composite case, not a real student or course.

At 06:13 on a cold morning the field station lost power while writing its log. Sam found a day.bin of 31,262 bytes instead of the usual 120,968. The logger had restarted on its own an hour later and begun a fresh file, so only this one was damaged. The week before, they had asked an agent for a program that reads the binary log and prints each reading, and it had worked on every file they tried. On this file it printed 1,488 readings that looked right, then this:

The chapter continues from here.

The loop in this chapter

Open a step to see what it asks of you and how it plays out in the Tutor phase.

  1. Predictthe eight results of Listing 9.15 and the first 29 bytes of one.bin, on paper, with confidence.

    Step 1 of 6 · You, no AI

    What you hand in: A written prediction: the output, the memory state, the operation count, and how sure you are.

    Why this step: A prediction written before the run is the only way to learn whether your model of the machine is right. A miss is the most useful result there is, because it shows exactly where the model is wrong.

    The first prediction is never delegated.

    Ask yourself: What will this print, what will memory look like when it ends, and how many operations will it take?

  2. Writestn_pack_flags (Listing 9.4), then the lab reader and its handlers, by hand.

    Step 2 of 6 · You, no AI

    What you hand in: One small piece written by hand, to build the mental model.

    Why this step: You can only check what you could have built. Writing a small piece yourself gives you the model you will need to judge the agent's version later.

    Ask yourself: Could I write this piece again tomorrow without notes?

  3. Specifythe opaque sensor_db.h with its ownership comments, the stn_log status enum, and the question you put to the tutor about the dump.

    Step 3 of 6 · You

    What you hand in: The header or class interface, unit tests, a complexity budget and the ownership rules.

    Why this step: The specification is the contract the code will be held to. Tests written first, a cost budget and ownership rules turn "looks right" into something you can check.

    Tutor In the Tutor phase you specify a question instead of code: what you want explained, quizzed or drilled.

    The ownership and lifetime section of a specification is never delegated.

    Ask yourself: Who allocates, who frees, what does it cost, and what input would break it?

  4. Delegatethe tutor explains one dump or errno message you have already annotated, and writes no code you submit.

    Step 4 of 6 · The agent

    What you hand in: The implementation from your specification, or an explanation in the Tutor phase, with the prompts and transcript saved.

    Why this step: The agent does the typing; you keep the judgment. Saving the prompts and the transcript makes the delegation something you can audit afterward.

    Tutor Tutor phase: the agent explains, quizzes and makes practice problems. It writes no code you submit.

    Ask yourself: Did I hand over my specification and tests, and did I save what the agent said?

  5. Audittruncated-file tests, the dump checked with od -j -N, ASan and UBSan on every build, Valgrind on your reader, and Listing 9.14's compile error.

    Step 5 of 6 · You and the tools

    What you hand in: The Audit Report: tests, sanitizer and Valgrind output, your own cost derivation against a timing plot, the defects found, and a keep-or-rewrite decision.

    Why this step: What the agent says is testimony until an instrument confirms it. Tests, sanitizers, Valgrind and a timing plot are evidence.

    Tutor In the Tutor phase the audit checks the tutor's claims as well as your own code.

    The final judgment that the code is correct is never delegated.

    Ask yourself: What did the sanitizer report, does the timing plot match my derivation, and would I keep this code or rewrite it?

  6. Defendwhy the struct is hidden, why the reader checks the count, what the file looks like when the power fails.

    Step 6 of 6 · You, live

    What you hand in: A five-minute oral defense.

    Why this step: If you can explain it live, you own it. The oral checks that the understanding is yours and not the agent's.

    Ask yourself: Why this bound, what input breaks it, and where is this memory freed?

What agents get wrong here

Patterns observed in agent-written code, to test for, not guarantees. If the agent no longer makes the mistake, your job is to prove it got it right, with the same instrument.

Unchecked fopen, fread, fclose; while (!feof(f))

How to catch it Files truncated mid-record and at a record boundary; ASan; Valgrind --track-origins=yes

Section 9.3.1 · AddressSanitizer, Valgrind

Shifting a promoted byte past the width of int

How to catch it UBSan; one test value above 2³²

Section 9.3.2 · UndefinedBehaviorSanitizer, Tests

Exposing the representation through an accessor that returns the internal array

How to catch it A test that crosses a growth boundary under ASan; a lifetime comment on every pointer the header hands out

Section 9.3.3 · AddressSanitizer, Tests, Timing and log-log plots

What the chapter covers

  1. 9.1 The concept, by hand
    • 9.1.1 Streams and how they fail
    • 9.1.2 Text lines, errno and perror
    • 9.1.3 The short read
    • 9.1.4 Bits, masks and the station's flags
    • 9.1.5 Shifts and the width of a type
    • 9.1.6 Endianness and a portable binary log
    • 9.1.7 Function pointers and callbacks
    • 9.1.8 Opaque types and abstract data types
  2. 9.2 Predict and check
  3. 9.3 What agents get wrong here
    • 9.3.1 Unchecked I/O
    • 9.3.2 Shifts past the width
    • 9.3.3 Exposing the representation
  4. 9.4 Working with the agent
  5. 9.5 Lab: The station reads its log
  6. 9.6 Defend it

Cost thread

This chapter counts bytes and time. A station day is 4 sensors × 1,440 minutes = 5,760 readings. The binary log is 8 + 21 × 5,760 = 120,968 bytes, and record k sits at offset 8 + 21k, reachable without reading the records before it.

Summary

  • Each file function fails its own way: NULL from fopen and fgets, a smaller count from fread and fwrite, EOF from fclose. Ignore them and the program runs on bytes it never read.
  • Classify every short read: feof and ferror separate end from failure, a byte count separates a clean end from a partial record, and the header count catches a cut at a record boundary.
  • Set with |, clear with & ~, toggle with ^, test with &. Promotion makes f << 5 352 and ~f −12 for f = 0x0B; one undefined shift gave three answers on four builds.
  • A portable binary format is fixed on paper and written field by field with shifts: little-endian, 8 + 21n bytes, no padding.
  • Function pointers let general code call specific code: a qsort comparator, a dispatch table, a visitor with void *ctx.
  • An opaque header keeps the representation secret, and the compiler enforces it; an accessor that hands out the internal array, with no stated lifetime, breaks the wall.

Key terms

  • stream: a FILE object through which C reads or writes a file, with its own buffer and end-of-file and error indicators.
  • short read: a read that returns fewer items or bytes than requested, because of end of file or an error.
  • errno: an integer that library functions set on failure, meaningful only right after a call that reported failure.
  • mask: a value whose one bits mark the positions an operation tests, sets, clears or extracts.
  • integer promotion: the conversion of a narrow integer such as uint8_t to int before arithmetic or shifting.
  • endianness: the order of a multi-byte value's bytes; little-endian stores the least significant byte first, big-endian the most significant.
  • function pointer: a variable that holds a function's address and can call it.
  • callback: a function passed to other code so that the other code can call it.
  • comparator: a callback that compares two elements and returns a negative, zero or positive result.
  • dispatch table: an array of function pointers indexed by a kind or code, used to choose a handler.
  • opaque type: a struct declared in a header without its fields, so callers can hold pointers to it but not see inside.
  • abstract data type (ADT): a type defined by its operations and their promises, not by its representation.

Companion code

24 files for this chapter, compiled with the book's flags.

Read the code Download ch09.zip

Connections

Where this chapter and another point to each other most often.

See it on the map

Further reading

  • Parnas (1972): the argument for hiding each likely-to-change decision behind an interface; short, and the root of Section 9.1.8.
  • Liskov and Zilles (1974): the original statement of abstract data types as types defined by their operations.
  • Kernighan and Ritchie (1988), Chapter 7 and Appendix B: the standard I/O functions and their return values, from the language's authors.
  • Wang et al. (2013): real cases of compilers using undefined behavior, shifts included, to remove code programmers thought was safe.