Read/Write Offsets and Seeking
Learn how to manipulate the read/write offset of an SDL_IOStream object to control stream interactions.
This lesson focuses on two essential functions for working with SDL_IOStream: SDL_TellIO() and SDL_SeekIO().
SDL_TellIO()provides the current read/write offsetSDL_SeekIO()lets you modify it
This gives us fine-grained control over stream interactions. We'll examine how these functions work together and apply them to a practical example of managing a game's high score.
Read/Write Offsets
Behind the scenes, SDL_IOStream objects maintain a simple integer value that gets updated as we perform read and write operations. This value is sometimes called the read/write position, cursor, or offset.
Its purpose is to keep track of the data we've already read, or already written. For example, the following program is reading a file containing 10 characters - HelloWorld.
When we read 5 characters using SDL_ReadIO(), the internal offset of the SDL_IOStream object we're using is updated, such that the next read begins where we left off:
Files
First Read: Hello
Second Read: WorldUsing SDL_TellIO()
We can retrieve the current offset of an SDL_IOStream object by passing it to the SDL_TellIO() function. When we first create an SDL_IOStream, we expect its offset to be 0, but this will change as we interact with the stream:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
int main(int, char**) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + "content.txt").c_str(),
"rb"
)};
char Content[6];
Content[5] = '\0';
std::cout << "Read/Write Offset: "
<< SDL_TellIO(File);
SDL_ReadIO(File, Content, 5);
std::cout << "\nFirst Read: " << Content;
std::cout << "\nRead/Write Offset: "
<< SDL_TellIO(File);
SDL_ReadIO(File, Content, 5);
std::cout << "\nSecond Read: " << Content;
std::cout << "\nRead/Write Offset: "
<< SDL_TellIO(File);
SDL_CloseIO(File);
return 0;
}Read/Write Offset: 0
First Read: Hello
Read/Write Offset: 5
Second Read: World
Read/Write Offset: 10Handling Errors from SDL_TellIO()
The SDL_TellIO() function can sometimes fail. When this happens, it will return a negative error code. We can check for this outcome and react to it as needed. We can also call SDL_GetError() for more information on what went wrong:
if (SDL_TellIO(File) < 0) {
std::cout << "Couldn't determine read/write "
"offset: " << SDL_GetError();
}Using SDL_SeekIO()
We can manipulate the read/write offset of an SDL_IOStream object using the SDL_SeekIO() function. This function requires three arguments:
- The stream: the pointer to the
SDL_IOStreamobject whose read/write offset we want to update - The seek value: an integer representing by how many bytes we want the offset to move, relative to the anchor. This can be a negative value if we want to move the offset backwards, or a positive value to move it forward
- The anchor: where we want to offset to move from
The second and third arguments work together to determine where the offset should be after our call to SDL_SeekIO(). The possible values for the third argument are:
SDL_IO_SEEK_SET: Our seek value sets the offset relative to the start of the stream. We should provide a positive seek value in this context.SDL_IO_SEEK_CUR: Our seek value moves the offset relative to the current position of the offset. Our seek value can be positive or negative in this context.SDL_IO_SEEK_END: Our seek value sets the offset relative to the end of the stream. Our seek value should be negative in this context.
Here's an example:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
int main(int, char**) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + "content.txt").c_str(),
"rb"
)};
char Content[6];
Content[5] = '\0';
std::cout << "Read/Write Offset: "
<< SDL_TellIO(File);
SDL_SeekIO(File, 5, SDL_IO_SEEK_SET);
std::cout << "\nRead/Write Offset: "
<< SDL_TellIO(File);
SDL_ReadIO(File, Content, 5);
std::cout << "\nFirst Read: " << Content;
SDL_CloseIO(File);
return 0;
}Read/Write Offset: 0
Read/Write Offset: 5
First Read: WorldReturn Value
The SDL_SeekIO() function returns the new offset of the SDL_IOStream object, relative to the beginning of the stream:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
int main(int, char**) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + "content.txt").c_str(),
"rb"
)};
std::cout << "Read/Write Offset: "
<< SDL_TellIO(File);
std::cout << "\nRead/Write Offset: "
<< SDL_SeekIO(File, -3, SDL_IO_SEEK_END);
SDL_CloseIO(File);
return 0;
}Read/Write Offset: 0
Read/Write Offset: 7Handling Errors from SDL_SeekIO()
The SDL_SeekIO() function can sometimes fail. When this happens, it will return -1. We can check for this outcome and react to it as needed. We can also call SDL_GetError() for more information on what went wrong:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
int main(int, char**) {
SDL_IOStream* File{SDL_IOFromFile(
"does-not-exist.txt",
"rb"
)};
if (SDL_SeekIO(File, 5, SDL_IO_SEEK_SET) < 0) {
std::cout << "Error seeking: "
<< SDL_GetError();
}
SDL_CloseIO(File);
return 0;
}Error seeking: Parameter 'context' is invalidExample: Tracking High Score
In this section, we'll create a simple example where we keep track of the player's highest score as a 32-bit integer serialized in a highscore.dat file on their hard drive.
Every time the player gets a new score, we'll compare it to the score in that file and, if it's higher, we'll update the file.
Let's handle the initial case first. The first time we call UpdateHighScore(), the highscore.dat file will not exist. So, we'll call CreateAndInitializeFile() to create it, and initialize it with the score the player received:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
void CreateAndInitializeFile(
const char* Path, int32_t InitialScore
) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + Path).c_str(),
"wb" // Write in binary mode
)};
if (!File) {
// Handle errors
return;
}
SDL_WriteIO(File, &InitialScore, sizeof(int32_t));
SDL_CloseIO(File);
}
void UpdateHighScore(
const char* Path, int32_t NewScore
) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + Path).c_str(),
"r+b" // Read and write in binary mode
)};
if (!File) {
CreateAndInitializeFile(Path, NewScore);
return;
}
// TODO
SDL_CloseIO(File);
}
int main(int, char**) {
UpdateHighScore("highscore.dat", 5000);
return 0;
}On subsequent calls to UpdateHighScore(), there will be a highscore.dat file available. We'll open that file for reading and writing, and retrieve the current high score.
If our new score is higher than what is currently in the file, we'll rewind our read/write offset to the beginning of the file using SDL_SeekIO(), and overwrite that integer with our higher score:
src/main.cpp
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_iostream.h>
#include <iostream>
void CreateAndInitializeFile(
const char* Path, int32_t InitialScore
) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + Path).c_str(),
"wb"
)};
if (!File) {
return;
}
SDL_WriteIO(File, &InitialScore, sizeof(int32_t));
SDL_CloseIO(File);
}
void UpdateHighScore(
const char* Path, int32_t NewScore
) {
std::string Base{SDL_GetBasePath()};
SDL_IOStream* File{SDL_IOFromFile(
(Base + Path).c_str(),
"r+b"
)};
if (!File) {
CreateAndInitializeFile(Path, NewScore);
return;
}
int32_t CurrentScore{0};
SDL_ReadIO(File, &CurrentScore, sizeof(int32_t));
std::cout << "Current High Score: "
<< CurrentScore << "\n";
if (NewScore > CurrentScore) {
std::cout << NewScore << " is a new high score!"
" Updating file\n";
SDL_SeekIO(File, 0, SDL_IO_SEEK_SET);
SDL_WriteIO(File, &NewScore, sizeof(int32_t));
} else {
std::cout << NewScore << " isn't better :(\n";
}
SDL_CloseIO(File);
}
int main(int, char**) {
// New high score
UpdateHighScore("highscore.dat", 5500);
return 0;
}If we run our program, we should see it read our previous score, and update it with our new record:
Current High Score: 5000
5500 is a new high Score! Updating fileBut if we run the program again and get the same score or lower:
Current High Score: 5500
5500 isn't better :(Advanced Serialization
The examples in this chapter have focused on slow, manual implementations of serialization and deserialization so we can build a deeper understanding of what is going on.
In larger projects, we typically want to make it as easy as possible to add serialization and deserialization capabilities to the classes and structs we define. Additionally, we have to deal with more complex situations that include:
- Inheritance: How do we handle objects that include inherited members?
- Reference Types: How do we handle objects that include references or pointers to other objects - for example, a
Characterwith aWeapon*member? - Polymorphic Types: If we have a pointer or reference to a polymorphic type, how do we serialize that object including data members from its more derived type?
- Versioning: When we update our game, how do we ensure saved files based on previous versions are still usable, even if we updated, added, or removed class variables?
Fortunately, these are largely solved problems. We have a wide range of open-source serialization libraries that we can add to our project for free.
Many of these libraries, like Cereal, also include support for serializing standard library types like std::string and std::vector, further reducing the amount of code we need to write. We cover Cereal in detail in our .
Summary
In this lesson, we explored the vital concept of the read/write offset in SDL_IOStream, learning how to use SDL_TellIO() to get the current position and SDL_SeekIO() to move to different locations within a stream. Here are the key points:
- The read/write offset tracks the current position within an
SDL_IOStreamstream. SDL_TellIO()returns the current offset.SDL_SeekIO()allows you to move the offset to a specific location.SDL_IO_SEEK_SET,SDL_IO_SEEK_CUR, andSDL_IO_SEEK_ENDdefine the anchor for seeking.- We can detect and react to errors when using
SDL_TellIO()andSDL_SeekIO().