跳轉到內容

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

工作示例

[編輯 | 編輯原始碼]


華夏公益教科書