跳转到内容

PSP 开发/文件系统/读取写入文件

100% developed
来自维基教科书,开放的书籍,开放的世界

读取和写入文件是需要了解的重要内容。从保存配置,加载配置,包含额外的文件,加载图像等任务都变得可用。

文件句柄

[编辑 | 编辑源代码]

文件句柄是唯一的标识符,指向一个特定的打开的文件。PSPDEV 使用术语唯一标识符 (UID)。文件句柄可以通过在系统中打开文件生成。可以同时打开多个文件,但会增加不必要的复杂性。没有已知的文件打开容量限制。在程序的整个生命周期中打开所有文件是一种不好的做法。除了代码复杂性之外,在多线程情况下,可能会在文件进行 IO 操作时关闭并重新打开文件。

打开文件

[编辑 | 编辑源代码]

PSPDEV 提供了 sceIoOpen() 函数,它接收一个 const char* 路径,一系列附加的位布尔值(一个 int)和一个 IEEE 标准 chmod 八进制整数。路径是一个指向目标文件在文件系统中的位置的简单字符串。前一篇文章 讨论了不同的文件系统。该函数返回用于 IO 操作的文件的 UID(文件句柄)。sceIoOpenAsync() 函数是异步版本,它不会在打开时阻塞当前线程。

如何访问文件以位布尔值的方式表示。PSP_O_RDONLY 允许从文件读取,但如果按位或 PSP_WRONLY,它将允许读取和写入文件。使用 PSP_O_RDWR 代替,它已经被计算为 PSP_O_RDONLY | PSP_O_WRONLY。打开一个不存在的文件将返回小于 0 的错误,除非提供了 PSP_O_CREAT。提供 PSP_O_CREAT 仅确保文件存在。所有写入操作都将从第一个字节开始修改,覆盖任何数据。为了防止这种情况,使用 PSP_O_TRUNC,它会清除文件中的其余部分,只保留写入的数据。如果想写入文件的末尾,使用 PSP_O_APPEND。PSP_O_APPEND 适用于多次写入。例如,实时追加到日志。PSP_O_EXCL 应该在文件已存在时设置一个错误,但是,在 PPSSPP 中它被破坏了。

文件权限超出了本文的范围。最后一个数字是一个八进制数字,表示文件权限。查看 w:Chmod 了解更多信息。通常使用 0777,这意味着创建者可以读写执行,用户组可以读写执行,其他人可以读写执行。它授予所有人完全访问权限。文件权限在 PPSSPP 中似乎被破坏了。

示例

// 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);

关闭文件

[编辑 | 编辑源代码]

PSPDEV 提供了 sceIoClose() 函数,它只接收一个文件句柄。关闭文件后,必须打开一个新的句柄才能继续使用该文件。此函数应该只在文件操作结束时调用。不要在多个线程中进行文件操作,其中两个线程共同管理打开和关闭文件以及 IO 操作。这样做会在文件被读取或写入时关闭文件。异步版本是 sceIoCloseAsync()。这对于关闭许多文件以继续执行程序很有用,而不是等待每个文件逐个关闭。

示例

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

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

写入文件

[编辑 | 编辑源代码]

打开文件句柄后,就可以写入文件了。使用 sceIoWrite() 处理写入文件,它接收要写入的文件句柄、要写入的 const char* (const char[]) 以及输出的字节数。该函数返回实际写入的字节数。写入的字节数少于或多于预期都是错误。如果输出一个 char*,sizeof() 函数将只返回 4。这是因为 sizeof() 将获取指针的大小,而不是实际的输出。可以使用 strlen(),它是 string.h 的一部分。但是,使用 char[] 和 sizeof() 会更容易。当使用 char[] 时,必须减去 1,否则字符串空终止符 '\0' 将被写入文件。存在 sceIoWriteAsync() 方法。

示例

// 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);

从文件读取

[编辑 | 编辑源代码]

打开文件句柄后,就可以从文件读取了。使用 sceIoRead() 处理从文件读取,它接收要读取的文件句柄、要读取到的 char* (char[]) 以及要读取的字节数。该函数返回实际读取的字节数。读取的字节数少于或多于预期都是错误。存在 sceIoReadAsync() 方法。

读取可以通过预先分配空间来完成。只有在无法确定文件长度时才需要这样做。如果二进制文件不存在文件头或文件是文本并且将完全读取,则可以使用 sceIoStat 结构体以及 sceIoGetstat() 函数,该函数接收 const char* (char[]) 路径和对 SceIoStat 结构体的引用。SceIOStat 结构体包含有关文件的信息。成员 st_size 以字节为单位告诉文件大小。

加载文件之前需要分配内存。不要忘记包含 stdlib。建议使用 char* 而不是 char[],因为 char* 强制需要调用 malloc 或 calloc。这允许您释放数据并丢弃指针的值以防止错误。它还通过将数据放在动态的堆内存中而不是自动的堆栈内存中,来阻止堆栈被大量字节淹没。

示例

// 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;

支持错误检查

[编辑 | 编辑源代码]

文件操作有很多可能会出现的错误。检查所有错误非常重要,这样程序就不会遇到未定义的行为。文件 IO 是一个示例,其中可能会发生多个预期的错误。创建将操作封装到范围内的函数,同时处理错误,可以创建更高效的系统。

将指针设置为 0 的主要原因,或者更确切地说,是将指针设置为 null,是为了防止程序成功使用该字符串,方法是阻止读取该字符串。释放数据只将分配的部分标记为可分配 - 它不会将所有内容都设置为 0。释放一个字符串然后调用 strlen() 将返回长度,即使它已被释放。某些错误可能在程序使用可分配的数据时发生。这可以在程序完成之后启用调试,以检查此类错误。它还可以在运行时启用立即调试,其中接收到的数据与预期的数据不符,例如从内存位置 0 读取数据。

sceIoGetstat()、sceIoOpen() 和 sceIoClose() 方法如果返回的整数小于 0,则返回错误状态。sceIoRead() 和 sceIoWrite() 方法返回读取的字节数和写入的字节数。如果读取或写入的字节数与预期不符,则表示发生了错误。

获取 SceIoStat 文件统计信息

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!");
}

打开文件

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;
}

关闭文件

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

写入文件

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!");
}

从文件读取

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;
}

综合示例

[编辑 | 编辑源代码]

虽然提供的函数处理错误检查,但它们充当包装器。传递给所有函数(除了 read_file)的所有数据与 sceIo*() 函数相同。使用这些函数类似于使用 PSPDEV 中提供的基本函数,但具有错误检查功能。本节旨在演示其用法。以下函数使用一个名为 test.txt 的文件,该文件包含纯文本格式数据。请参阅 工作示例 部分以了解该文件。该文件应该放在生成的 EBOOT.PBP 旁边。

从文件读取

[编辑 | 编辑源代码]

要从文件读取,需要文件的路径。使用 EBOOT.PBP 的根目录,可以将两个字面量串联起来以获得指向文件的正确绝对路径。struct SceIoStat 在作用域结束后不会保留。

  1. 获取文件路径
  2. 为指针和 struct SceIoStat 保留数据
  3. 使用 check_file() 填充 struct SceIoStat
  4. 使用适当的 chmod 和 attribs 打开文件
  5. 使用 SceIoStat st_size 将文件读入保留的指针
  6. 关闭文件
  7. 使用数据
  8. 释放数据
  9. 将保留的指针设置为 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;
}

写入文件

[编辑 | 编辑源代码]

要写入文件,需要文件的路径。使用 EBOOT.PBP 的根目录,可以将两个字面量串联起来以获得指向文件的正确绝对路径。当 SceIoStat 离开作用域时,无需清理它。

  1. 获取文件路径
  2. 获取要写入的数据
  3. 使用适当的 chmod 和 attribs 打开文件
  4. 使用字符串长度减去 EOF 字符串终止符 '\0' 将数据写入文件
  5. 关闭文件
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);
}

复制文件

[编辑 | 编辑源代码]

要复制文件,需要文件的路径。使用 EBOOT.PBP 的根目录,可以将两个字面量串联起来以获得指向文件的正确绝对路径。复制文件涉及两次打开、一次写入、一次读取和两次关闭操作。本节涉及与前面两个示例完全相同的内容。

  1. 获取文件路径
  2. 为指针和 struct SceIoStat 保留数据
  3. 使用 check_file() 填充 struct SceIoStat
  4. 使用适当的 chmod 和 attribs 打开 src 文件
  5. 使用 SceIoStat st_size 将文件读入保留的指针
  6. 关闭文件
  7. 使用适当的 chmod 和 attribs 打开 dest 文件
  8. 使用字符串长度减去 EOF 字符串终止符 '\0' 将数据写入文件
  9. 关闭文件
  10. 释放数据
  11. 将保留的指针设置为 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;
}

要进行测试,只需调用创建的函数即可。如果通过指示当前操作的打印内容进行填充,则了解输出将有助于在出现错误时进行判断。

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;
}

使用 FILE

[编辑 | 编辑源代码]

您也可以将 stdio.h 中的 FILE 结构与 fopen、ftell、fseek 等一起使用。但是,存在“open”、“close”、“read”、“write”函数,这些函数内联了本文介绍的函数。它们使用 _O_* 而不是 PSP_O_*。口头上说,O_*。还存在这些函数的 Windows 版本:“_open”、“_close”、“_read”和“_write”。这些方法应该只用于支持跨平台,因为它们只是内联函数,并降低了其可扩展性。

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

工作示例

[编辑 | 编辑源代码]


华夏公益教科书