PSP Development/Filesystem/Reading Writing Files

Reading and writing files is an important thing to know how to do. Anything to saving configurations, loading configurations, including extra files, loading images, and other tasks become available.

File HandlesEdit

File handles are unique identifiers, which point to a certain file that is open. PSPDEV uses the term Unique Identifier (UID). A file handle can be generated by opening a file in the system. It is possible to open multiple files at once, however unneeded complexity builds on top of this. There is no known file open capacity. It would be bad practice to open all files for the whole lifespan of the program. Other than code complexity, when multi-threading, it is possible to close and open a file or while it is undergoing IO operations.

Opening a FileEdit

PSPDEV provides the function sceIoOpen() which takes in a const char* path, a series of additive bit-booleans (a single int), and a IEEE standard chmod octal integer. The path is a simple string of characters that point to where in the file systems the target file is. The previous article talks about the different file systems. The function returns the UID (file handle) of the file for use in the IO operations. The function sceIoOpenAsync() is the asynchronized version, which doesn't block the current thread on open.

How the file should be accessed is represented in a bit-boolean way. PSP_O_RDONLY will allow the file to be read from, however, if OR'd PSP_WRONLY, it will allow the file to be read and wrote to. Use PSP_O_RDWR instead, which is already computed as PSP_O_RDONLY | PSP_O_WRONLY. Opening a file that doesn't exist will return an error that is less than 0, unless PSP_O_CREAT is supplied. Supplying PSP_O_CREAT only ensures the file exists. All write operations will modify from the first byte, overlapping any data. To prevent this, use PSP_O_TRUNC, which clears the rest of the file to only the data that was wrote. If wanting to write to the end of a file, use PSP_O_APPEND. PSP_O_APPEND is good for multiple writes. For example, appending to a log in real time. PSP_O_EXCL should set an error when the file already exists, however, on PPSSPP it is broken.

File permissions are beyond the scope of this article. The last number is an octal number which represents file permissions. See w:Chmod for a better understanding. It is common to use 0777, which means creator can read write execute, user group can read write execute, and others can read write execute. It grants everyone full access. File permissions seem to be broken in PPSSPP.

Example

// open a file for reading only, if it doesn't exist, create it
// this is functionally equivalent to creating a new file
// to just close and do nothing with, for it is blank.
int uid = sceIoOpen("umd0:/example.file", PSP_O_RDONLY | PSP_O_CREAT, 0777);

Closing a FileEdit

PSPDEV provides the function sceIoClose() which takes in only a file handle. After closing a file, a new handle must be opened in order to continue using the file. This function should only be called at the end of the file operations. Do not do file operations in multiple threads, where two threads manage opening and closing files together as well as IO operations. Doing so will close the file while it is being read or wrote to. The asynchronized version is sceIoCloseAsync(). This is useful for closing many files to continue on with the program, instead of waiting for each file to close 1 by 1.

Example

// open a file for reading only
int uid = sceIoOpen("umd0:/example.file", PSP_O_RDONLY, 0777);

// close the file
if(uid >= 0) sceIoClose(uid);

Writing to a FileEdit

After opening a file handle, the ability to write to the file is gained. Writing to the file is handled with sceIoWrite() which takes in the file handle to write to, a const char* (const char[]) to write out, and the length of the output in bytes. The function returns the amount of bytes actually wrote. It is an error to write fewer or more bytes than expected. If outputting a char*, the sizeof() function will only give you 4. This is because sizeof() would be taking the size of the pointer, not the actual output. strlen() can be used, which is part of string.h. However, it is easy to use a char[] instead and sizeof(). When using a char[], you must subtract one or the string null terminator '\0' will be written to the file. The method sceIoWriteAsync() exists.

Example

// open a file for writing only, create file if not exist, truncate
int uid = sceIoOpen("umd0:/example.file", PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);

// write to the file
const char[] data = "Hello\nWorld\n!";
sceIoWrite(uid, data, sizeof(data)-1); // or strlen(data)

// close the file
if(uid >= 0) sceIoClose(uid);

Reading from a FileEdit

After opening a file handle, the ability to read from a file is gained. Reading from the file is handled with sceIoRead() which takes in the file handle to read from, a char* (char[]) to read into, and the number of bytes to read. The function returns the amount of bytes actually read. It is an error to read fewer or more bytes than expected. The method sceIoReadAsync() exists.

Reading can be done by preallocating space. This is only necessary when you can't determine the length of the file. If a file header doesn't exist in a binary file or the file is text and it will be entirely read, the struct SceIoStat can be used with the function sceIoGetstat() which takes in a const char* (char[]) path, and a reference to a SceIoStat struct. The SceIOStat struct holds information about the file. The member st_size tells the file size in bytes.

Memory needs to be allocated before loading the file. Do not forget to include stdlib. It is recommended to use a char* instead of a char[], as char* forces the need for a malloc or calloc call. This allows you to free data and discard the pointer's value to prevent bugs. It also stops from flooding the stack with a lot of bytes by putting the data in dynamic, heap memory instead of the automatic, stack memory.

Example

// generate stats about the file
SceIoStat info;
sceIoGetstat("umd0:/example.file", &info);

// open a file for writing only, create file if not exist, truncate
int uid = sceIoOpen("umd0:/example.file", PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);

// allocate memory
char* data = (char*)calloc(info.st_size+1, sizeof(char));
if(data == 0) crash(0, "Memory Allocation", "Memory allocation failed!");

// read from file into data, append null terminator
sceIoRead(uid, data, info.st_size);
data[info.st_size] = '\0'; // info.st_size - 1 would be index worthy

// close the file
if(uid >= 0) sceIoClose(uid);

// free memory, null the pointer
free(data);
data = 0;

Support Error CheckingEdit

File operations have many errors that can surface. It is important to check for all the errors so the program doesn't run into undefined behavior. File IO is an example where multiple expected errors can occur. Making functions which rap the operations into scopes, while also handling the errors makes a more productive system.

The major reason setting the pointer to 0, or namely null, is to prevent the string from being used successfully in the program by stopping the string being read. Freeing the data only marks the section allocated as allocatable - it doesn't set everything back to 0. Freeing a string then calling strlen() will return the length, even though it is freed. Some errors can occur when the program is using data that is allocatable. This enables debugging later when the program is finalized for such errors. It also enables immediate runtime debugging where the data expected isn't what was received, as per reading from memory location 0.

The methods sceIoGetstat(), sceIoOpen(), and sceIoClose() all return a state of error if the integer returned is less than 0. The methods sceIoRead() and sceIoWrite() return the bytes read and the bytes wrote. In the instance that the bytes read or wrote do not match up with what was expected, an error has occurred.

Getting SceIoStat file statistics

void check_file(SceIoStat* info, const char* path) {
	// open file description
	int status;
	if((status = sceIoGetstat(path, info)) < 0)
		crash(status, "Checking File", "File not found or no access!");
}

Opening a file

int open_file(const char* path, int params, int chmod) {
	// open file handle
	int uid = sceIoOpen(path, params, chmod);
	if(uid < 0)
		crash(uid, "Opening File", "File not found or no access!");
	return uid;
}

Closing a file

void close_file(int uid) {
	// close file handle
	int status;
	if((status = sceIoClose(uid)) < 0)
		crash(status, "Close File", "File could not be closed!");
}

Writing to a file

void write_file(int uid, const char* out, int size) {
	// write to file
	if(sceIoWrite(uid, out, size) != size)
		crash(uid, "Writing File", "File wrote incorrect number of bytes!");
}

Reading from a file

void read_file(int uid, char** out, int size) {
	// allocate, read into buffer, pad with \0
	char* buffer = calloc(size+1, sizeof(char));
	if(buffer == 0)
		crash(0, "Memory Allocation", "Memory allocation failed!");
	int read = sceIoRead(uid, buffer, size);
	if(read != size)
		crash(read, "Read File", "File size read doesn't match expected!");
	buffer[size] = '\0';
	*out = buffer;
}

Putting It All TogetherEdit

While the functions provided handle error checking, they act as wrappers. All data passed to all but read_file are the same as the sceIo*() functions. Using the functions will be similar to using the barebones functions provided in PSPDEV with error checking. This section aims at demonstrating the use. The functions below use a file called test.txt, which contains plain text format data. See the Working Example section for the file. This file belongs right next to the EBOOT.PBP generated.

Reading From A FileEdit

To read from a file, the path to the file is needed. Using the root of the EBOOT.PBP, one can concatenate two literals together to get a proper absolute path to the file. The struct SceIoStat will not persist after the scope ends.

  1. Get path to file
  2. Reserve data for the pointer and the struct SceIoStat
  3. Fill in the struct SceIoStat with check_file()
  4. Open the file using appropriate chmod and attribs
  5. Read the file into the reserved pointer using SceIoStat st_size
  6. Close the file
  7. Use the data
  8. Free the data
  9. Set the reserved pointer to 0
void do_example1() {
	const char* src = ROOT "file.txt";
	
	// Reading file (existent) src > (char*) file_data
	char* file_data;
	SceIoStat info;
	check_file(&info, src);
	
	int uid = open_file(src, PSP_O_RDONLY, 0777);
	read_file(uid, &file_data, (int)info.st_size);
	close_file(uid);
	
	printf("Read data:\n");
	printf("%s\n", file_data);
	free(file_data);
	file_data = 0;
}

Writing To A FileEdit

To write to a file, the path to the file is necessary. Using the root of the EBOOT.PBP, one can concatenate two literals together to get a proper absolute path to the file. Cleaning up a SceIoStat is not necessary when it falls out of a scope.

  1. Get path to file
  2. Get data to write
  3. Open the file using appropriate chmod and attribs
  4. Write the data to the file using the string length minus EOF string terminator '\0'
  5. Close the file
void do_example2() {
	const char* new = ROOT "test2.txt";
	const char output[] = "Hello World 123\nabc";
	
	// Writing new file output[] > (non-existent) test2.txt
	int uid = open_file(new, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
	write_file(uid, output, sizeof(output)-1);
	close_file(uid);
}

Copying a FileEdit

To copy a file, the path to the files are necessary. Using the root of the EBOOT.PBP, one can concatenate two literals together to get a proper absolute path to the file. Copying a file involves two opens, one write, one read, and two close operations. This section involves the exact same thing as the two previous examples.

  1. Get path to files
  2. Reserve data for the pointer and the struct SceIoStat
  3. Fill in the struct SceIoStat with check_file()
  4. Open the src file using appropriate chmod and attribs
  5. Read the file into the reserved pointer using SceIoStat st_size
  6. Close the file
  7. Open the dest file using appropriate chmod and attribs
  8. Write the data to the file using the string length minus EOF string terminator '\0'
  9. Close the file
  10. Free the data
  11. Set the reserved pointer to 0
void do_example3() {
	const char* src = ROOT "test.txt";
	const char* out = ROOT "diff.txt";
	
	// Copy file test.txt > diff.txt
	char* file_data;
	SceIoStat info;
	check_file(&info, src);
	
	// multiple files can be open at once, but since using one variable
	// closing the file would be wise before losing the data
	int uid = open_file(src, PSP_O_RDONLY, 0777);
	read_file(uid, &file_data, (int)info.st_size);
	close_file(uid);
	
	uid = open_file(out, PSP_O_WRONLY | PSP_O_CREAT | PSP_O_TRUNC, 0777);
	write_file(uid, file_data, (int)info.st_size);
	close_file(uid);
	
	free(file_data);
	file_data = 0;
}

To test, simply call the functions created. Understanding of the output will help if padded by prints dictating what the current action is, especially in a time of error.

int main(int argc, char** argv)
{
	// basic init
	setupExitCallback();
	pspDebugScreenInit();
	
	sceDisplayWaitVblankStart();
	pspDebugScreenSetXY(0, 0);

	printf("Reading file\n");
	do_example1();
	printf("Done.\n");
	printf("Writing file\n");
	do_example2();
	printf("Done.\n");
	printf("Copying file\n");
	do_example3();
	printf("Done.\n");
	
	// Sleep thread
	sceKernelSleepThread();
	
	sceKernelExitGame();
	return 0;
}

Using FILEEdit

You can also use stdio.h's FILE struct with fopen, ftell, fseek, etc. However, there are functions 'open', 'close', 'read', 'write', which inlines the functions introduced in the article. These use _O_* instead of PSP_O_*. Colloquially, O_*. There are also the windows variants of the functions '_open', '_close', '_read', and '_write'. These methods should only be used to support cross platform, as they just inline the functions and reduce extendability thereof.

The MakefileEdit

TARGET = file_io
OBJS = main.o ../common/callback.o

INCDIR = 
CFLAGS = -O2 -G0 -Wall
CXXFLAGS = $(CFLAGS) -fno-exceptions -fno-rtti
ASFLAGS = $(CFLAGS)

LIBDIR =
LIBS = 
LDFLAGS =

EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = FileIO

PSPSDK=$(shell psp-config --pspsdk-path)
include $(PSPSDK)/lib/build.mak

Working ExampleEdit