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 Handles
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 File
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 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

Closing a File
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

Writing to a File
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

Reading from a File
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

Support Error Checking
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

Opening a file

Closing a file

Writing to a file

Reading from a file

Putting It All Together
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 File
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

Writing To A File
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

Copying a File
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

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.

Using FILE
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.

Working Example

 * main.c : http://pastebin.com/zXu5QkiW
 * test.txt : http://pastebin.com/H01X8iYJ