PSP 开发/Lua
本文假定您对 Lua 有深入的了解。这不是一篇 Lua 语法文章。
必须指出,将所有内容从 C 移植到 Lua 基本上是在用复杂性包装所有内容。这意味着在 C 中创建一个函数,该函数链接到 Lua 函数标识符(可能在某个有组织的表中)。虽然这确实很容易做到,但您必须从内部了解 Lua,而不仅仅是从外部了解。LuaC 在尝试监控堆栈时可能会让人望而生畏。
虽然 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 是一个免费软件和库,根据 MIT 许可在 lua.org 上提供。为了构建和使用它,有必要了解许可证。Lua 有多年的历史,因此它被构建为支持最基本的编译器。进行一些调整,构建应该顺利进行。
为 PSP 构建的 Lua5.1.5:(点击此处)
为 PSP 构建的 Lua5.2.4:(点击此处)
为 PSP 构建的 Lua5.3.4:(点击此处)
- 从 lua.org 下载 PSP 源代码包
- 仅提取 src 文件夹
- 删除内部的 Makefile,因为它不会被使用
- (如果适用)在 luaconf.h 中,取消注释 #define LUA_32BITS 和 #define LUA_USE_C89
- 在该目录中打开一个终端或 cmd
运行此代码
psp-gcc -DLUA_USE_C89 -DLUA_32BITS -DLUA_C89_NUMBERS -O2 -G0 -Wall -c *.c
- 删除 lua.o 和 luac.o 对象文件
- 将所有 .o 文件复制到一个新的空目录
- 将头文件 lua.h、lua.hpp(取决于版本)、luaconf.h 和 lauxlib.h 复制到一个新目录
- 进入包含 .o 文件的目录
- 在该目录中打开一个终端或 cmd。
运行此代码
ar -cvq liblua.a *.o
- 将 liblua.a 复制到您在 #3 中临时存储头文件的目录
- 删除在 #2 中创建的新 .o 文件目录
- 将 liblua.a 放置在 psp/sdk/lib 文件夹中
- 将所有 .h 文件放置在 psp/sdk/include 文件夹中
正在使用的库是 libm.a 和 liblua.a,它们已安装到 psp/sdk/include 和 psp/sdk/lib 中。Makefile 中的 LIBS 变量包含要使用的库。
- 在 LIBS 下添加 -llua
- 在 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
这是您将要做的工作的核心。我们将在其中执行的很多代码都可以被认为是抽象的,因为您可以创建一个编译指令,在一次快速调用中创建 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.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]- main.c : http://pastebin.com/VxTJaAXp
- lua_lib.c : http://pastebin.com/iJRpC4cP
- lua_lib.h : http://pastebin.com/NUwVZwUT