Homework #10 — Binary File I/O
For this week’s homework, we’re going to create a tiny binary save file.
The goal is to practice what we’ve learned about:
- structs
- binary files
fread()
fwrite()
- raw bytes
- multibyte values
- portability
- serialization
Use only concepts we’ve covered so far in Learn C With Me, up to and including Chapter 9 — File Input/Output.
The task
We’re going to save a small game character to a binary file and then read it back.
Start with:
struct character {
unsigned char health;
unsigned char attack;
unsigned char level;
};
Create a character:
struct character hero = {
.health = 100,
.attack = 25,
.level = 7
};
1. Create a binary file
Open:
save.bin
for binary writing.
Remember the appropriate mode for a binary file.
Check whether fopen() succeeded before trying to use the file.
2. Write the character
Write the three values:
health
attack
level
to save.bin using:
fwrite()
Each field is an unsigned char, so each value can be treated as a single byte.
Close the file when you’re finished.
Your file should contain exactly three pieces of character data.
3. Inspect the file
Use a tool such as:
hexdump
or:
xxd
to inspect save.bin.
Before looking at the result, predict what bytes you expect to see.
Then compare your prediction with the actual file.
Try changing the values in the character, writing the file again, and observing how the bytes change.
4. Load the character
Create another:
struct character loaded;
Open save.bin for binary reading and use:
fread()
to load the three values into loaded.
Then print the result:
Loaded character:
Health: 100
Attack: 25
Level: 7
If everything worked correctly, the loaded values should match the original character.
5. Make the save format explicit
Now change your program so that it does not depend on the memory layout of the struct.
Instead of writing:
fwrite(&hero, sizeof hero, 1, fp);
write each field individually.
For example, think in terms of:
BYTE 1: health
BYTE 2: attack
BYTE 3: level
Do the same thing when reading the file back.
The program now defines its own tiny binary file format.
6. Add a magic number
Let’s make the file recognizable as our save file.
The first four bytes of the file should contain:
KARO
After those bytes, store the character data.
Our file format is now:
K A R O health attack level
When loading the file, first read those four bytes.
Check that they contain:
KARO
If they don’t, print:
Invalid save file!
and don’t load the character.
7. Break the save file
Open save.bin in a hex editor, or otherwise modify some of its bytes.
Try:
- changing the health byte
- changing the level byte
- changing one of the
KARO bytes
- removing bytes from the end of the file
Then run your loader again.
Observe what happens.
Can your program detect an invalid header?
What happens if there isn’t enough data in the file?
Bonus challenge — A multibyte value
Change:
unsigned char health;
to:
unsigned short health;
Now health can occupy more than one byte.
Set it to:
1000
Before writing anything, think about today’s lesson.
If you simply write the unsigned short directly with fwrite():
Which byte comes first?
Inspect the resulting file with hexdump or xxd.
Can you determine whether your machine is storing the value little-endian or big-endian?
Bonus challenge — Portable health value
Instead of directly writing the unsigned short, manually split the value into individual bytes and decide which byte should be written first.
When loading the file, reconstruct the original value from those bytes.
The goal is to make the file format define the byte order, rather than allowing the CPU’s native byte order to define it for us.
Don’t worry if you don’t immediately know how to do this.
Experiment.
Think about what each individual byte needs to contain.
Questions to think about
Why might this be a bad idea?
fwrite(&hero, sizeof hero, 1, fp);
What happens if the compiler inserts padding into the struct?
What happens if the file is written on one architecture and read on another?
What changes when a value occupies more than one byte?
Why might a real application use a defined serialization format instead of simply dumping its memory directly into a file?
Rules
Use only concepts we’ve covered so far in Learn C With Me, up to and including Chapter 9 — File Input/Output.
You may use what we’ve already learned, including:
struct
FILE *
fopen()
fclose()
fread()
fwrite()
printf()
fprintf()
sizeof
along with arrays, strings, pointers, loops, conditions and functions from previous lessons.
Do not use concepts from later chapters.
In particular, you don’t need:
typedef
- dynamic memory allocation
- command-line arguments
- external serialization libraries
The point isn’t to build a production-quality save system.
The point is to understand the difference between:
“write whatever happens to be in memory”
and:
“define exactly which bytes our file contains.”
As always: predict first, experiment second.
Inspect the actual bytes. Change them. Break the file. See whether your prediction matches reality.
And if you get stuck, post your code, compiler output, and what you expected to happen in the thread.