将 Lua 集成到 C++
一篇关于将 Lua 脚本语言嵌入 C++ 对象中的文章。
引言
是否曾想过在不重新编译整个代码段的情况下调整一两个值?那么嵌入式脚本可能是你的答案。这时 Lua 就派上用场了。Lua 是一个可嵌入的脚本解释器,它轻量级且快速。你可以在 Lua 站点上了解更多信息。
背景
在编程中,我经常遇到这样的情况:需要调整和修改才能排列项目或设置其值。这总是需要反复重建(或编辑/继续)代码,浪费大量时间。因此,就有了在程序中加入脚本语言的想法。这样我就可以只更改脚本,保存它,然后一次又一次地重新运行程序,从而节省大量构建时间。
那么,为什么选择 Lua?
Lua 是一种功能强大、轻量级、占内存小的编程语言,专为扩展应用程序而设计。此外,Lua 通过垃圾回收提供自己的内存管理(可以针对你的应用程序进行专门优化)。Lua 也是完全可重入的代码,没有全局变量。
为什么不使用 LuaBind?
对于那些不知道的人,请查看 LuaBind。LuaBind 是一个很棒的产品,但对我来说它看起来太复杂了。首先,代码不容易跟踪类和对象的位置。另外,考虑到我想要将 Lua 集成到 wxWidgets 应用程序中,使用模板有点不行(你可以在 wxWidgets 站点上阅读有关跨平台问题的文章)。
开始使用 Lua
首先,你可以从官方网站 Lua.org 下载 Lua。在撰写本文时,最新版本是 5.02。Lua 可与任何 ANSI C 编译器一起构建。整个库都符合 ANSI C 标准。下载代码后,构建一个库(我个人倾向于静态库)非常容易。考虑到我们要使用 C++ 环境,我们需要将头文件转换为 C++。我创建了一个新的头文件
#ifndef __LUA_INC_H__ #define __LUA_INC_H__ extern "C" { #include "lualib/lua.h" #include "lualib/lauxlib.h" #include "lualib/lualib.h" } #endif // __LUA_INC_H__
此文件包含了 Lua 所需的所有 Lua 组件。现在,你可以在 C++ 文件中像往常一样包含此头文件(luainc.h)。最简单的开始方法是创建一个控制台项目,这样你就可以看到 Lua 的打印输出了。
Lua 与 C++ 的问题
Lua 是用 C 编写的,整个 Lua API 都是基于 C 的。因此,将 Lua 转换为 C++ 环境似乎相当困难,但 Lua 提供了实现这一点的能力。我读过许多报告说 Lua 不能与类一起使用,但通过一些技巧可以实现。
嵌入 Lua
嵌入 Lua 非常简单。有大量文档列出了 Lua 的 C API,所以我不打算详细介绍,只想给你一些要点。为了控制 Lua 的运行,我们创建了一个 CLuaVirtualMachine
类。在我看来,虚拟机只是用于与 Lua 在加载和运行文件以及控制状态方面进行交互。许多人可能不同意,但这是我的 5 美分。因此,我们的 VM 可以加载文件,外部类可以获取其状态以便在 Lua 堆栈上存取数据。
使用 Lua 进行脚本编写
在我的应用程序中,脚本用于为正在运行的程序提供“答案”。因此,脚本是一组函数。Lua 提供了许多关于如何编写脚本的示例,但一个简单的“hello world”脚本如下所示:
-- Lua Hello World (test.lua)
function helloWorld ()
io.write ("hello World")
end
io.write
是一个加载到 Lua 中的辅助库。要让这个脚本在程序中嵌入运行,我们有以下代码:
int iErr = 0; lua_State *lua = lua_open (); // Open Lua luaopen_io (lua); // Load io library if ((iErr = luaL_loadfile (lua, "test.lua")) == 0) { // Call main... if ((iErr = lua_pcall (lua, 0, LUA_MULTRET, 0)) == 0) { // Push the function name onto the stack lua_pushstring (lua, "helloWorld"); // Function is located in the Global Table lua_gettable (lua, LUA_GLOBALSINDEX); lua_pcall (lua, 0, 0, 0); } } lua_close (lua);
现在你应该在控制台上看到“hello World”打印出来。好的,现在我们知道如何调用 Lua 函数了。现在我们将让 Lua 调用 C 函数。我们必须将函数注册到 Lua。为简单起见,我们创建另一个打印函数:
static int printMessage (lua_State *lua) { assert (lua_isstring (lua,1)); const char *msg = lua_tostring (lua, 1); // debug output cout << "script: " << msg << endl; return 0; } ... luaopen_io (lua); // Load io library // setup global printing (trace) lua_pushcclosure (lua, printMessage, 0); lua_setglobal (lua, "trace"); ...
我们的脚本如下所示:
-- Lua Hello World (test.lua) function helloWorld () io.write ("hello World") trace ("trace working now :)") end
你现在将看到“hello Worldscript: trace working now :)”打印出来。那么,这有什么用呢?
进入 C++
我们现在已经看到了如何让 Lua 调用 C 函数,但 C 离 C++ 还有很长的路要走。调用 C++ 函数的主要问题是你必须知道你引用的对象(即 this
指针)。在上面的代码中,你一定注意到 Lua 将所有函数存储在 LUA_GLOBALSINDEX
表中。事实上,如果你阅读文档,你会发现 Lua 中的大多数东西都是基于表的。这使我们想到将我们的对象存储在 Lua 表中。为了与 C++ 协同工作,我们将表命名为“this”,从而指示 C++。
首先,我们的脚本会进行更改以显示“this”表的存在。脚本会变得稍微复杂一些,但仍然足够容易编写。主要区别在于函数现在存在于“this”表中,而不是全局表中。C++ 代码负责处理当前处于活动状态的“this”表。此外,每个脚本函数都必须传递“this”表,如果它们想访问非全局函数的话。我们的 Hello World 现在变成:
function this.helloWorld (this) io.write ("hello World") trace ("trace working now :)") end
对于这一点,你将看到脚本仍然可以调用全局 C 函数。
为了开始 C++ 部分,我们需要一些助手来帮助我们。第一个是 CLuaScript
类。当用户类想要运行脚本函数时,他们需要继承该类。该类允许用户将 C++ 函数(方法)注册到 Lua。CLuaScript
还包含对“this”表的引用。这个引用用于在需要时获取“this”表。下一个助手类是 CLuaThis
。这个类有助于控制 Lua 当前使用的“this”表。
每次创建新的 CLuaScript
时,它都会在 Lua 中创建一个新表。然后将此表存储在一个名为 registry 的全局表中。Registry 是一个跨函数调用的持久表。通过引用来保存“this”表。使用引用,我们可以在需要时将表推到堆栈上。编译脚本时,Lua 将 helloWorld 函数放在“this”表中。Lua 知道“this”表,因为 CLuaThis
在编译之前已经告诉了 Lua “this”表的位置。“this”表的作用是将所有函数聚集在一起,供 CLuaScript
对象使用。每个 CLuaScript
现在都有自己的“this”表,从而实现了面向对象的 Lua。
现在怎么办?
现在我们有了理论,麻烦在于如何将其付诸实践。考虑到 Lua 只能调用 C 函数(或类的静态成员),我们需要一种将其转换为 C++ 的方法。Lua 为我们提供了一种注册变量给调用函数的能力。这使我们能够非常巧妙地将函数注册到 Lua。事实上,我们实际上并不注册函数,而是注册它们的索引查找。
动手实践
当我们创建一个 CLuaScript
对象时,我们需要创建一个表并将当前的 this
指针存储在索引 0 的表中。这样,当脚本调用我们的 C 函数时,我们将能够获得与调用相关的对象指针。因此,构造函数:
lua_newtable (state); m_iThisRef = luaL_ref (state, LUA_REGISTRYINDEX); // Save the "this" table to index 0 of the "this" table CLuaRestoreStack rs (vm); lua_rawgeti (state, LUA_REGISTRYINDEX, m_iThisRef); lua_pushlightuserdata (state, (void *) this); lua_rawseti (state, -2, 0);
你会注意到那里有一个 CLuaRestoreStack
类。这只是一个类,用于确保 Lua 堆栈在退出时是平衡的。它很有用,因为这样你就可以偷懒,不用担心确保将堆栈恢复到原来的样子了 :)
现在是编译文件的时候了。在编译脚本之前,我们需要将正确的“this”表加载到堆栈上。在 CLuaThis
类的构造函数中,我们有:
// Save the old "this" table lua_getglobal (state, "this"); m_iOldRef = luaL_ref (state, LUA_REGISTRYINDEX); // replace it with our new one lua_rawgeti(state, LUA_REGISTRYINDEX, iRef); lua_setglobal (state, "this");
这会保存对“this”表的先前引用,并加载我们新表的引用。然后,在析构函数中,我们返回堆栈。这样做是为了确保所有函数都存储在正确的表中。我们的编译代码因此变成:
CLuaThis luaThis (m_vm, m_iThisRef); // Make available to // correct "this" table m_vm.RunFile (strFilename); // Compile the file
在这一切之后,我们来注册那些 C++ 函数。我们总是为每个 C++ 函数向 Lua 注册同一个 C 函数。区别在于每次我们都使用唯一的索引进行注册。这个索引被发送到 CLuaScript
回调函数。因此,向 Lua 注册的代码如下所示:
iMethodIdx = ++m_nMethods; // Register a function with the lua script. // Added it to the "this" table lua_rawgeti (state, LUA_REGISTRYINDEX, m_iThisRef); // Push the function and parameters lua_pushstring (state, strFuncName); lua_pushnumber (state, (lua_Number) iMethodIdx); lua_pushcclosure (state, LuaCallback, 1); lua_settable (state, -3);
你会注意到该函数存储在“this”表中,而不是全局表中。因此,现在每次你想从脚本中调用函数时,都需要引用该表。现在假设我们在 Lua 中注册了一个名为“hello”的函数,我们的脚本将通过以下方式访问它:
function this.helloWorld (this) io.write ("hello World") trace ("trace working now :)") this:hello () end
使用代码
CLuaScript
首先要考虑的类是 CLuaScript
。你需要让你的脚本类继承它,并重载 ScriptCalling
和 HandleReturns
方法。当脚本调用类的方法时,基类代码会调用 ScriptCalling
。方法的一个索引作为参数传入。如果脚本有任何需要处理的返回值,则会调用 HandleReturns
方法。并将返回值的函数名传递给它。CLuaScript
基类能够将参数(int
、float
和 string)发送给脚本函数。
CLuaVirtualMachine
下一个类是 CLuaVirtualMachine
。你只需要一个这样的实例(除非你有其他需求)。测试程序将具有以下结构:
class CMyScript : public CLuaScript { public: CMyScript (CLuaVirtualMachine& vm) : CLuaScript (vm) { m_iMethodBase = RegisterFunction ("hello1"); RegisterFunction ("hello2"); RegisterFunction ("hello3"); } ... int ScriptCalling (CLuaVirtualMachine& vm, int iFunctionNumber) { switch (iFunctionNumber - m_iMethodBase) { case 0: return Hello1 (vm); case 1: return Hello2 (vm); case 2: return Hello3 (vm); } return 0; } ... int Hello2 (CLuaVirtualMachine& vm) { lua_State *state = (lua_State *) vm; int iNumber = (int) lua_tonumber (state, -1); printf ("Hellow (2) -> %d\n", iNumber); return 0; } ... void HandleReturns (CLuaVirtualMachine& vm, const char *strFunc); ... }; void main (void) { CLuaVirtualMachine vm; vm.InitialiseVM (); ... CMyScript ms (vm); ms.CompileFile ("test.lua"); ... ms.SelectScriptFunction ("CountAndCall"); ms.AddParam (2); ms.Go (); ... }
其他
如讨论中所述,还有其他类。还有一个调试类,但我不怎么使用它,但在出现问题时,查看脚本的去向很有用。示例显示了它的用法。
历史
- v1.0 - 啊……,第一个版本。永远不要购买版本 1.0 的软件。会有 Bug 的 :)
- v1.01 - 修正了
CLuaThis
类,以便在设置新表之前获取“this”表。