跳轉至內容

PSP 開發/Lua

100% developed
來自華夏公益教科書,開放世界開放書籍

Lua 分析

[編輯 | 編輯原始碼]

本文假定您對 Lua 有深入的瞭解。這不是一篇 Lua 語法文章。

用 Lua 替換 C

[編輯 | 編輯原始碼]

必須指出,將所有內容從 C 移植到 Lua 基本上是在用複雜性包裝所有內容。這意味著在 C 中建立一個函式,該函式連結到 Lua 函式識別符號(可能在某個有組織的表中)。雖然這確實很容易做到,但您必須從內部瞭解 Lua,而不僅僅是從外部瞭解。LuaC 在嘗試監控堆疊時可能會讓人望而生畏。

面向物件的 Lua

[編輯 | 編輯原始碼]

雖然 Lua 為 C 添加了體積,但使用它的好方法是 AI、GUI 和控制。Lua 是一種非常強大的語言,並且能夠進行 JIT 編譯,靈活性是一個主要優勢。與用 Lua 替換 C 不同,更傾向於有一個使用 Lua 的更面向物件的原因。

手動構建難度

[編輯 | 編輯原始碼]

Lua 無法開箱即用地與除普通 PC 之外的任何東西進行編譯。但是,您可以成功構建 Lua5.3.4 並執行它。它只需要一些手動工作。這可能很困難。

  • 應該使用 psp-gcc,它沒有被預先製作的 Makefile(只是標準 gcc)使用,以及 psp-gcc 包含檔案和庫
  • 您必須為每個檔案使用 -DLUA_USE_C89 和 -DLUA_C89_NUMBERS。psp-gcc 編譯器不使用 long long。C89 很適合使用,因為 PSP 的年代和我們目標的 PSP 韌體 1.5 版本
  • 標題需要匹配才能安裝它,或者將其用作靜態庫。lua.h、lauxlib.h、lualib.h 和 luaconf.h(包含在 lua.h 中)需要修改 luaconf.h。
  • 在將所有 C 檔案批次編譯為 O 檔案時,將有 lua.o 和 luac.o,它們包含主方法並將丟擲重新定義錯誤,並且需要保留在靜態庫之外
  • PSP 需要正確的 libm.a,它與 Windows 或 Linux 不同

獲取 Lua

[編輯 | 編輯原始碼]

Lua 是一個免費軟體和庫,根據 MIT 許可在 lua.org 上提供。為了構建和使用它,有必要了解許可證。Lua 有多年的歷史,因此它被構建為支援最基本的編譯器。進行一些調整,構建應該順利進行。

為 PSP 構建的 Lua5.1.5:(點選此處)

為 PSP 構建的 Lua5.2.4:(點選此處)

為 PSP 構建的 Lua5.3.4:(點選此處)

準備/編譯

[編輯 | 編輯原始碼]
  1. lua.org 下載 PSP 原始碼包
  2. 僅提取 src 資料夾
  3. 刪除內部的 Makefile,因為它不會被使用
  4. (如果適用)在 luaconf.h 中,取消註釋 #define LUA_32BITS 和 #define LUA_USE_C89
  5. 在該目錄中開啟一個終端或 cmd

執行此程式碼

psp-gcc -DLUA_USE_C89 -DLUA_32BITS -DLUA_C89_NUMBERS -O2 -G0 -Wall -c *.c

清理/組織

[編輯 | 編輯原始碼]
  1. 刪除 lua.o 和 luac.o 物件檔案
  2. 將所有 .o 檔案複製到一個新的空目錄
  3. 將標頭檔案 lua.h、lua.hpp(取決於版本)、luaconf.h 和 lauxlib.h 複製到一個新目錄
  4. 進入包含 .o 檔案的目錄
  5. 在該目錄中開啟一個終端或 cmd。

執行此程式碼

ar -cvq liblua.a *.o
  1. 將 liblua.a 複製到您在 #3 中臨時儲存標頭檔案的目錄
  2. 刪除在 #2 中建立的新 .o 檔案目錄
  1. 將 liblua.a 放置在 psp/sdk/lib 資料夾中
  2. 將所有 .h 檔案放置在 psp/sdk/include 資料夾中

設定 Makefile

[編輯 | 編輯原始碼]

正在使用的庫是 libm.a 和 liblua.a,它們已安裝到 psp/sdk/include 和 psp/sdk/lib 中。Makefile 中的 LIBS 變數包含要使用的庫。

  1. 在 LIBS 下新增 -llua
  2. 在 LIBS 下新增 -lm
TARGET = LUATEST
OBJS = main.o

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

LIBDIR =
LIBS = -llua -lm
LDFLAGS =

EXTRA_TARGETS = EBOOT.PBP
PSP_EBOOT_TITLE = Lua test program

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

在 C 中使用 Lua

[編輯 | 編輯原始碼]

這是您將要做的工作的核心。我們將在其中執行的很多程式碼都可以被認為是抽象的,因為您可以建立一個編譯指令,在一次快速呼叫中建立 LuaC 函式。但是,這超出了本文撰寫時的範圍。

包含檔案

[編輯 | 編輯原始碼]

首先,您將需要包含檔案。永遠不要忘記包含檔案。

main.c

// Standard libraries for PSP
#include <pspkernel.h>
#include <pspdebug.h>
#include <pspdisplay.h>

// Lua includes
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

// Macro keyword printf to point to pspDebugScreenPrintf for ease
#define printf pspDebugScreenPrintf

// Setup the module and main thread
#define VERS 1
#define REVS 0
PSP_MODULE_INFO("LUATEST", PSP_MODULE_USER, VERS, REVS);
PSP_MAIN_THREAD_ATTR(PSP_THREAD_ATTR_USER);

建立公共 Lua 空間

[編輯 | 編輯原始碼]

現在您在直接的頂級函式空間中擁有所有 Lua 函式。在提供的包中,還有 lua.hpp,它是一個 C++ 標頭檔案。如果您想以 C++ 風格使用它,您將不得不檢視它,它應該非常相似。

現在我們應該定義一個 print。列印是必不可少的。它是最主要的除錯工具。我們需要建立一個函式,它在 C 中,但被置於 Lua 的全域性範圍內,以便從 Lua 內部呼叫。最好不要讓它弄亂 main.c 編譯單元,並且由於您可以更改類以進行調整,因此您可以節省編譯時間。

lua_lib.c

#include "lua_lib.h"

// Lua includes
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

lua_lib.h

#ifndef LUA_LIB_H
#define LUA_LIB_H

// Lua includes
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

#endif

現在我們有了空的編譯單元,我們需要將其新增到我們的 Makefile OBJS 中。

OBJS = main.o lua_lib.o

現在我們已經設定好了一切,你可以構建並觀察在 lua_lib.c 旁邊建立的空目標檔案。但是,現在是時候填充它了!

建立 Lua 上下文

[edit | edit source]

所有內容都依賴於一個 lua_State 指標。可以將其視為全域性空間。當你建立新狀態時,你就建立了一個全新的“上下文”。也就是說,其中 **沒有函式**!__VERSION、os.* 等 **不在** 此狀態中 - 它很裸。我們需要賦予它生命。因此,讓我們建立一個這樣的函式,它將為我們提供這樣的狀態。但是,我們將呼叫 luaL_openlibs(L) 來為我們提供所有標準庫,而不是隻使用裸全域性變數。

lua_lib.h

lua_State* lua_create_newstate();

lua_lib.c

lua_State* lua_create_newstate()
{
	lua_State* L;
	L = luaL_newstate();
	luaL_openlibs(L);

	return L;
}

向 Lua 新增函式

[edit | edit source]

好的,我們的下一步將是萬能的 print 函式!為此,我們將使用一個 extern 函式,它返回一個 int。返回 int 很重要,因為它被 Lua 使用。Lua 具有非常動態的返回值系統。你應該將任何與 lua 直接相關的內容加上字首 lua_。extern 的原因可以追溯到我們正在做的事情。extern 與彙編之外的東西關係不大,一個很好的替代方案是 static,但為了正確性,它應該是 extern。你需要了解 extern 的作用才能理解它。返回的 int 是函式返回的引數數量。最後,當 Lua 呼叫 C 函式來使用它時,它會將你建立的 lua_State 指標傳遞給它。確保你包含了適當的標頭檔案。你基本上可以將你在 main 中包含的所有內容都塞入 lua_lib.c 中。

lua_lib.c

extern int lua_print(lua_State *L)
{
	int arg_len = lua_gettop(L);
	int n;
	for (n=1; n <= arg_len; n++) {
		printf("%s%s", lua_tostring(L, n), "\t");
	}
	printf("\n");
	return 0;
}

現在解釋函式的核心內容。Lua 是一種基於堆疊的指令碼語言。一個好的做法是在傳遞無效數量的引數時報錯。但是,由於 print 基本上是一個元組,我們可以忽略這一點。呼叫 lua_gettop(L) 將為你提供堆疊上推送的專案數量。lua_tostring(L, n) 返回來自 lua 字串的靜態 char*。一個謹慎的做法,但很固執,是使用 luaL_checkstring(L, n),它做的事情完全一樣,但如果你沒有給它字串,則會報錯。在 Lua 中,你不應該那麼固執,只允許字串,因為表格、函式、userdata 等都是可列印的,事實上,tostring 應該返回類似 function: pointer 的東西。

這個特定的函式在每個專案之間用製表符列印所有內容。當你需要除錯多個引數或解包(表格)時,它非常非常有用。不過別忘了結束行,因為你很容易會混淆。在 Lua 中的一次 print 呼叫是一行輸出,其中包含給定的引數,從右到左無限延伸 - 就像不會換行一樣。

接下來,我們需要註冊這個函式。print() 函式位於全域性空間中。這意味著我們可以直接註冊該函式。在 lua_create_newstate() 函式中執行此操作最容易,因為我們已經在此處構建了 lua_State。請記住,lua_State 是我們存放函式環境的地方。我們使用函式 lua_register() 來執行此操作。第一個引數是 lua_State,第二個是函式的名稱。(請記住,這將進入全域性空間)最後一個引數是我們建立的函式的引用。

lua_lib.c

lua_State* lua_create_newstate()
{
	lua_State* L;
	L = luaL_newstate();
	luaL_openlibs(L);
	
	lua_register(L, "print", lua_print);
	
	return L;
}

太棒了!現在 getfenv()["print"] 將呼叫函式 lua_print,讓 C 來管理堆疊。現在我們必須轉到主編譯單元並設定好一切。用於 PSP 的 Lua 非常挑剔,你必須知道自己在做什麼,並且理解為什麼要這樣做。

建立測試 Lua 指令碼

[edit | edit source]

我們應該建立要執行的指令碼。在根專案目錄中建立一個名為 script.lua 的指令碼。此檔案應與你的 EBOOT.PBP 位於同一個目錄中。我們將建立一個簡單的指令碼,該指令碼將在主迴圈中的每個迴圈中執行一次。這允許我們使用 pspDebugScreenPrintf 繪製列印內容,並且不會在下一個繪製迭代中被擦除。

script.lua

-- print a simple lua_string
print("Hello from Lua!")

將所有內容整合在一起

[edit | edit source]

現在讓我們看看 main。

我們應該執行基本的回撥設定並初始化除錯螢幕。這使我們可以繪製文字。請注意主函式中的引數。

main.c

int main(int argc, char** argv)
{
	setupExitCallback();
	pspDebugScreenInit();

讓我們談談 int main() 的引數和路徑。當你使用 PPSSPP 啟動一個典型的 EBOOT.PBP 時,你將得到 umd0:/EBOOT.PBP。我們位於 umd0:/。這是 EBOOT.PBP 資料夾中的所有內容,該資料夾位於你的 PSP 的 Memstick > /PSP/GAME/EBOOT_FOLDER 中。為了安全起見,你可以動態地從協議中剝離 EBOOT.PBP。但是,在你遇到問題之前,這個解決方案是有效的!這將需要一些字串操作。

接下來,我們需要載入檔案。為了開始執行此操作,我們首先需要獲取路徑。我們還需要初始化一個新狀態並載入庫。幸運的是,我們建立了 lua_lib_newstate() 來為我們執行此操作。

	
	// script file must be in the same directory as EBOOT.PBP
	const str* scriptPath = "umd0:/script.lua"; // wherever your script is located
	
	// init Lua and load all libraries
	lua_State *L = lua_lib_newstate();
	
	int status = 0;

**請勿使用以下程式碼。**雖然我們不應該退出程式,但我們將除錯螢幕恢復到位置。我們有幾種選擇。首先,我們可以呼叫 luaL_dofile 來載入並執行檔案。我們將非常快地從儲存器中讀取。這不是我們想要的行為。這也相當於呼叫 luaL_loadfile 然後呼叫 lua_pcall()。我們想要做的是載入一次,快取該載入,然後從那裡使用它。除此之外,如果你有 4 個在每次迭代中呼叫的指令碼,你將從儲存器中載入該資料 4 次。以 60fps 的速度,這相當於 240 次儲存器讀取訪問。**請勿使用以下程式碼。**

	// call script
	// DO NOT RUN THIS. IT ACCESSES FILES VERY, VERY FAST. PROTECT YOUR SSD/MEMSTICK LIFESPAN.
	while (isRunning()) {
		pspDebugScreenSetXY(0, 0);
		status = luaL_dofile(L, scriptPath);
		printf("test");
		//status = lua_pcall(L, 0, 2, 0);
		sceDisplayWaitVblankStart();
	}

為了繞過上述問題,我們需要執行以下操作。首先,你想要做的是載入檔案。之後,使用 luaL_ref 將載入的檔案檢查從堆疊中彈出(防止執行),並對它給出的索引進行硬引用。你的檔案塊被快取為 LUA_REGISTRY(INDEX) 中的該索引。之後,你應該從登錄檔中將其 rawget 回來以執行它。這樣你就可以從記憶體中讀取,而不是不斷地從 umd0:/ 讀取。它是被快取的。對其進行 pcall 操作,所有內容都應該正常工作!確保你執行了錯誤檢查,稍後你將在本頁面中發現如何執行錯誤檢查。如果你以這種方式載入了 4 個指令碼,你將從儲存器中讀取資料,並在記憶體中來回跳轉,而不是從儲存器中讀取資料。你只需要在最初訪問儲存介質 4 次。

	// init Lua, its libraries, and do a dry run for initialization
	lua_State *L = lua_lib_newstate();
	
	int status = 0;
	
	// cache the file in lua registry
	status = luaL_loadfile(L, scriptPath);
	
	// TODO: Error check the status for compilation error
	
	int lua_script = luaL_ref(L, LUA_REGISTRYINDEX);

現在你可以使用以下程式碼(與前面的示例不同)來連線所有這些。你必須檢查錯誤,請繼續閱讀以瞭解如何管理堆疊並保持堆疊清潔。

	// run main logic, call script, check for errors
	while (isRunning()) {
		pspDebugScreenSetXY(0, 0);
		lua_rawgeti(L, LUA_REGISTRYINDEX, lua_script); // get script into top of stack
		status = lua_pcall(L, 0, 0, 0); // run the script
		// TODO: check for runtime errors
		sceDisplayWaitVblankStart();
	}

現在剩下的就是清理程式。你可以在此處釋放 lua_State/堆疊等。如果你使程式崩潰,你不需要自己擦除堆疊,而是依賴於 lua_close。然後執行基本的退出遊戲並返回。你想要確保你清理了在 Lua 中使用的任何動態記憶體。我強烈建議**不要**在 Lua 端或 C 端動態分配你無法在返回常量之前立即清理的東西。

	// cleanup
	lua_close(L);
	
	sceKernelExitGame();	
	return 0;
}

恭喜!你現在應該能夠執行 lua 指令碼了!你只需要呼叫 make EBOOT.PBP 並解決你可能輸入錯誤的任何內容,例如 Makefile、main.c 或 lua_lib.c/.h 中的內容。

錯誤檢查/堆疊管理

[edit | edit source]

這就是你應該進行錯誤檢查的方式。如果你正在開發指令碼,尤其是在指令碼從執行到執行是動態的情況下,錯誤檢查是必不可少的。保持堆疊清潔也很重要。

瞭解堆疊上有什麼很重要。你將推送函式、字串、數字等(後者來自返回值)。如果 Lua 沒有處理它們,你**必須**將它們從堆疊中彈出。假設你只有一個函式,在載入指令碼後你在 lua 中呼叫它。它返回一個字串。你的堆疊現在不乾淨,因為函式的返回值沒有被處理。當指令碼死亡時,你需要清理它。這就是這個方便的 stackDump 函式派上用場的地方。使用它。

main.c

static void stackDump(lua_State *L) {
	int i = lua_gettop(L);
	printf("---------------- Stack Dump ----------------\n");
	while(i) {
		int t = lua_type(L, i);
		switch (t) {
		case LUA_TSTRING:
			printf("%d:`%s`\n", i, lua_tostring(L, i));
			break;
		case LUA_TBOOLEAN:
			printf("%d: %s\n", i, lua_toboolean(L, i) ? "true" : "false");
			break;
		case LUA_TNUMBER:
			printf("%d: %g\n", i, lua_tonumber(L, i));
			break;
		default:
			printf("%d: %s\n", i, lua_typename(L, t));
			break;
		}
		i--;
	}
	printf("--------------- Stack Dump Finished ---------------\n");
}

現在讓我們實現它!以下程式碼是上面幾節中看到的迴圈,但添加了 stackDump。如果你使用不會報錯的 Lua 指令碼執行它,你會發現堆疊在呼叫 lua_pcall 後始終是乾淨的,除非程式碼返回了值。這是不允許的,會生成錯誤,但可以透過 do return end 來解決。在這種情況下,堆疊可能是乾淨的。在下面的 lua_rawgeti 和 lua_pcall 之間,你將有專案被推送到你的堆疊上,lua_pcall 將消化這些專案(堆疊頂部將是一個函式,因為這就是你的 loadfile 放到堆疊上的內容 - 你的指令碼包裝在一個函式中)。你真的無法強調一個乾淨的堆疊是多麼重要。你可能會遇到未定義的行為和記憶體問題。PSP 不是一臺擁有 16GB WRAM、4GB VRAM 和 4+ GHz 速度的 CPU 的龐然大物計算機,它可以經受住一些打擊。

main.c

	// run main logic, call script, check for errors
	while (isRunning()) {
		pspDebugScreenSetXY(0, 0);
		lua_rawgeti(L, LUA_REGISTRYINDEX, lua_script); // get script into stack
		status = lua_pcall(L, 0, 0, 0); // run the script
		if(status != 0) {
			// alerting that we have a runtime error
			stackDump(L); // a dirty stack with error string
			printf(lua_tostring(L, -1)); // print last push, ie the error
			lua_pop(L, 1); // pop the error arg
			stackDump(L); // a clean stack
		}
		sceDisplayWaitVblankStart();
	}

工作示例

[edit | edit source]


華夏公益教科書