65.9K
CodeProject 正在变化。 阅读更多。
Home

将 Lua 集成到 C++

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (41投票s)

2005 年 9 月 2 日

CPOL

8分钟阅读

viewsIcon

343102

downloadIcon

8604

一篇关于将 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。你需要让你的脚本类继承它,并重载 ScriptCallingHandleReturns 方法。当脚本调用类的方法时,基类代码会调用 ScriptCalling。方法的一个索引作为参数传入。如果脚本有任何需要处理的返回值,则会调用 HandleReturns 方法。并将返回值的函数名传递给它。CLuaScript 基类能够将参数(intfloat 和 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”表。
© . All rights reserved.