Lua 技术说明 5

用于将 C++ 绑定到 Lua 的模板类

作者:Lenny Palozzi

摘要

本说明解释了将 C++ 类绑定到 Lua 的方法。Lua 并不直接支持此操作,但它提供了低级 C API 和扩展机制,使其成为可能。我描述的方法利用 Lua 的 C API、C++ 模板和 Lua 的扩展机制构建了一个小巧简单但有效的静态模板类,该类提供类注册服务。该方法对你的类施加了一些限制,即只有签名为 int(T::*)(lua_State*) 的类成员函数才能注册。但正如我将展示的那样,可以克服此限制。最终结果是用于注册类的简洁界面,以及 Lua 中类的熟悉 Lua 表语义。此处解释的解决方案基于我编写的模板类,名为 Luna

问题

Lua 的 API 并非设计为将 C++ 类注册到 Lua,只注册具有签名 int()(lua_State*) 的 C 函数,即,将 lua_State 指针作为参数并返回整数的函数。实际上,这是 Lua 在注册中支持的唯一 C 数据类型。要注册任何其他类型,你必须使用 Lua 提供的扩展机制、标记方法、闭包等。在构建允许我们将 C++ 类注册到 Lua 的解决方案时,我们必须利用这些扩展机制。

解决方案

解决方案由四个部分组成:类注册、对象实例化、成员函数调用和垃圾回收。

通过使用类的名称注册表构造函数来完成类注册。表构造函数是模板类的静态方法,它返回一个表对象。

注意:静态类成员函数与 C 函数兼容,假设它们的签名相同,因此我们可以将它们注册到 Lua 中。以下代码片段是模板类的成员函数,“T”是要绑定的类。

  static void Register(lua_State* L) {
    lua_pushcfunction(L, &Luna<T>::constructor);
    lua_setglobal(L, T::className);

    if (otag == 0) {
      otag = lua_newtag(L);
      lua_pushcfunction(L, &Luna<T>::gc_obj);
      lua_settagmethod(L, otag, "gc"); /* tm to release objects */
    }
  }
对象实例化通过将用户传递给表构造函数的任何参数传递给 C++ 对象的构造函数来完成,创建一个表示对象的表,将类的任何成员函数注册到该表,最后将表返回给 Lua。对象指针存储在表中索引为 0 的用户数据中。成员函数映射中的索引存储为每个函数的闭包值。稍后会详细介绍成员函数映射。
  static int constructor(lua_State* L) {
    T* obj= new T(L); /* new T */
    /* user is expected to remove any values from stack */

    lua_newtable(L); /* new table object */
    lua_pushnumber(L, 0); /* userdata obj at index 0 */
    lua_pushusertag(L, obj, otag); /* have gc call tm */
    lua_settable(L, -3);

    /* register the member functions */
    for (int i=0; T::Register[i].name; i++) {
      lua_pushstring(L, T::Register[i].name);
      lua_pushnumber(L, i);
      lua_pushcclosure(L, &Luna<T>::thunk, 1);
      lua_settable(L, -3);
    }
    return 1; /* return the table object */
  }
与 C 函数不同,C++ 成员函数需要类的对象才能调用函数。成员函数调用由一个函数完成,该函数通过获取对象指针和成员函数指针并进行实际调用来“thunk”调用。成员函数指针通过闭包值从成员函数映射中索引,对象指针从索引为 0 的表中索引。请注意,Lua 中的所有类函数都使用此函数注册。
  static int thunk(lua_State* L) {
    /* stack = closure(-1), [args...], 'self' table(1) */
    int i = static_cast<int>(lua_tonumber(L,-1));
    lua_pushnumber(L, 0); /* userdata object at index 0 */
    lua_gettable(L, 1);
    T* obj = static_cast<T*>(lua_touserdata(L,-1));
    lua_pop(L, 2); /* pop closure value and obj */
    return (obj->*(T::Register[i].mfunc))(L);
  }
垃圾回收通过为表中的用户数据设置垃圾回收标记方法来完成。当垃圾回收器运行时,将调用“gc”标记方法,该方法只是删除对象。“gc”标记方法在类注册期间使用新标记注册。在上面的对象实例化中,用户数据被标记为标记值。
  static int gc_obj(lua_State* L) {
    T* obj = static_cast<T*>(lua_touserdata(L, -1));
    delete obj;
    return 0;
  }
考虑到这一点,您可能已经注意到一个类必须符合一些要求 注意:这些要求是我的设计选择,您可以决定使用不同的接口;只需对代码进行一些调整即可。

Luna<T>::RegType 是一个函数映射。name 是成员函数 mfunc 将注册为的函数的名称。

  struct RegType {
    const char* name;
    const int(T::*mfunc)(lua_State*);
  };
以下是将 C++ 类注册到 Lua 的示例。调用 Luna<T>::Register() 将注册该类;这是模板类的唯一公共接口。要使用 Lua 中的类,可以通过调用其表构造函数来创建它的实例。
  class Account {
    double m_balance;
   public:
    Account(lua_State* L) {
      /* constructor table at top of stack */
      lua_pushstring(L, "balance");
      lua_gettable(L, -2);
      m_balance = lua_tonumber(L, -1);
      lua_pop(L, 2); /* pop constructor table and balance */
    }

    int deposit(lua_State* L) {
      m_balance += lua_tonumber(L, -1);
      lua_pop(L, 1);
      return 0;
    }
    int withdraw(lua_State* L) {
      m_balance -= lua_tonumber(L, -1);
      lua_pop(L, 1);
      return 0;
    }
    int balance(lua_State* L) {
      lua_pushnumber(L, m_balance);
      return 1;
    }
    static const char[] className;
    static const Luna<Account>::RegType Register
  };

  const char[] Account::className = "Account";
  const Luna<Account>::RegType Account::Register[] = {
    { "deposit",  &Account::deposit },
    { "withdraw", &Account::withdraw },
    { "balance",  &Account::balance },
    { 0 }
  };

  [...]

  /* Register the class Account with state L */
  Luna<Account>::Register(L);

  -- In Lua
  -- create an Account object
  local account = Account{ balance = 100 }
  account:deposit(50)
  account:withdraw(25)
  local b = account:balance()
Account 实例的表如下所示
  0 = userdata(6): 0x804df80
  balance = function: 0x804ec10
  withdraw = function: 0x804ebf0
  deposit = function: 0x804f9c8

说明

有些人可能不喜欢使用 C++ 模板,但它们在此处的使用非常合适。它们为最初看起来很复杂的问题提供了快速、紧凑的解决方案。使用模板的结果是该类非常类型安全;例如,不可能在成员函数映射中混合不同类的成员函数,编译器会发出警告。此外,模板类的静态设计使其易于使用,在完成后无需清理模板实例化对象。

Thunk 机制是该类的核心,因为它“Thunk”调用。它通过从函数调用关联的表中获取对象指针,并为成员函数指针索引成员函数映射来实现这一点。(Lua 表函数调用 table:function()table.function(table) 的语法糖。当进行调用时,Lua 首先将表压入堆栈,然后压入任何参数)。成员函数索引是一个闭包值,最后压入堆栈(在任何参数之后)。最初,我将对象指针作为闭包,这意味着对于每个实例化的类,每个函数都有 2 个闭包值,一个指向对象(void*)的指针和一个成员函数索引(int);这似乎相当昂贵,但可以快速访问对象指针。此外,表中需要一个用于垃圾回收目的的用户数据对象。最后,我选择为对象指针索引表并节省资源,从而增加了函数调用开销;对象指针的表查找。

综合考虑所有事实,该实现仅使用 Lua 的一些可用扩展机制,用于保存成员函数索引的闭包,“gc”标记方法用于垃圾回收,以及用于表构造函数和成员函数调用的函数注册。

为什么只允许注册具有签名 int(T::*)(lua_State*) 的成员函数?这允许您的成员函数直接与 Lua 交互;检索参数并将值返回 Lua,调用任何 Lua API 函数等。此外,它提供了 C 函数注册到 Lua 时具有的相同接口,从而使希望使用 C++ 的人更容易上手。

缺点

此模板类解决方案仅绑定具有特定签名的成员函数,如前所述。因此,如果您已经编写了类,或打算在 Lua 和 C++ 环境中使用该类,这可能不是您的最佳解决方案。在摘要中,我提到我会解释这实际上不是一个问题。使用代理模式,我们封装了真实类并将对其进行的任何调用委托给目标对象。代理类的成员函数将参数和返回值强制转换为 Lua,并将调用委托给目标对象。您将向 Lua 注册代理类,而不是真实类。此外,您还可以使用继承,其中代理类继承自基类并将函数调用委托给基类,但有一个警告,基类必须具有默认构造函数;您无法在代理的构造函数初始化程序列表中从 Lua 获取构造函数参数到基类。代理模式解决了我们的问题,我们现在可以在 C++ 和 Lua 中使用该类,但在这样做时需要我们编写代理类并维护它们。

创建对象时只需新建,用户应该对如何创建对象有更多控制权。例如,用户可能希望注册一个单例类。一种解决方案是让用户实现一个静态的 create() 成员函数,该函数返回指向对象的指针。这样,用户就可以实现一个单例类,只需通过 new 分配对象或其他任何方式。可以修改 constructor 函数以调用 create() 而不是 new 来获取对象指针。这将更多策略推送到类中,但灵活性更高。垃圾回收的“挂钩”也可能对某些人有用。

结论

本说明解释了一种将 C++ 类绑定到 Lua 的简单方法。该实现相当简单,让您有机会根据自己的目的对其进行修改,同时满足任何一般用途。还有许多其他用于将 C++ 绑定到 Lua 的工具,例如 tolua、SWIGLua 以及像这样的其他小型实现。每个都有自己的优点、缺点和对您特定问题的适用性。希望本说明能对一些更微妙的问题有所启发。

模板类的完整源代码(约 70 行源代码)可从 Lua 加载项页面获得。

参考

[1] R. Hickey,使用模板仿函数在 C++ 中进行回调,C++ 报告 95 年 2 月


上次更新:2003 年 3 月 12 日星期三上午 11:51:13 EST,更新人:lhf