技术说明 5

原文:https://www.lua.org/notes/ltn005.html

Technical Note 5 技术说明 5

Last update: Wed Mar 12 11:51:13 EST 2003 by lhf.

A template class for binding C++ to Lua 一个用于将 C++ 绑定到 Lua 的模板类

by Lenny Palozzi 作者:Lenny Palozzi

Abstract 摘要

This note explains a method of binding C++ classes to Lua. Lua does not support this directly, but it does provide a low level C API and extension mechanisms that makes it possible. The method I describe makes use of Lua’s C API, C++ templates, and Lua’s extension mechanisms to build a small and simple, yet effective static template class that provides a class registration service. The method imposes a few restriction on your classes, namely, only class member functions with the signature int(T::*)(lua_State*) can be registered. But as I’ll show, this restriction can be overcome. The end result is a clean interface to register classes, and familiar Lua table semantics of classes in Lua. The solution explained here is based on a template class that I wrote called Luna.

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

The problem 问题

Lua’s API is not designed to register C++ classes to Lua, only C functions that have the signature int()(lua_State*), that is, a function that takes an lua_State pointer as an argument and returns an integer. Actually, that is the only C data type that Lua supports in registering. To register any other type you have to use the extension mechanisms that Lua provides, tag methods, closures, etc. In building a solution that allows us to register C++ classes to Lua, we must make use of those extension mechanisms.

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

The solution 解决方案

There are four components that make the solution, class registration, object instantiation, member function calling, and garbage collection.

​ 该解决方案由四个组件组成,即类注册、对象实例化、成员函数调用和垃圾回收。

Class registration is accomplished by registering a table constructor function with the name of the class. A table constructor function is a static method of the template class that returns a table object.

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

Note: Static class member functions are compatible with C functions, assuming their signatures are the same, thus we can register them in Lua. The code snippets below are member functions of a template class, ‘T’ is the class being bound.

​ 注意:静态类成员函数与 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 */
    }
  }

Object instantiation is accomplished by passing any arguments the user passed to the table constructor function to the constructor of the C++ object, creating a table that represents the object, registering any member functions of the class to that table, and finally returning the table to Lua. The object pointer is stored as a userdata in the table at index 0. The index into the member function map is stored as a closure value for each function. More on the member function map later.

​ 对象实例化通过将用户传递给表构造函数的任何参数传递给 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 */
  }

Unlike C functions, C++ member functions require an object of the class for the function to be called. Member function calling is accomplished by a function that “thunks” the calls by acquiring the object pointer and member function pointer and making the actual call. The member function pointer is indexed from the member function map by the closure value, the object pointer from the table at index 0. Note that all class functions in Lua are registered with this function.

​ 与 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);
  }

Garbage collection is accomplished by setting a garbage collection tag method for the userdata in the table. When the garbage collector is run the ‘gc’ tag method will be called which simply deletes the object. The ‘gc’ tag method is registered during class registration with a new tag. At object instantiation above, the userdata is tagged with the tag value.

​ 通过为表中的用户数据设置垃圾回收标记方法来完成垃圾回收。当运行垃圾回收器时,将调用“gc”标记方法,该方法只是删除对象。“gc”标记方法在类注册期间使用新标记进行注册。在上面的对象实例化中,用户数据被标记为标记值。

  static int gc_obj(lua_State* L) {
    T* obj = static_cast<T*>(lua_touserdata(L, -1));
    delete obj;
    return 0;
  }

With that in mind, there are a few requirements that a class must comply with as you may have noticed:

​ 考虑到这一点,您可能已经注意到,类必须遵守一些要求:

  • it must have a public constructor that takes a lua_State* 它必须有一个采用 lua_State* 的公共构造函数
  • registered member functions must have the signature int(T::*)(lua_State*) 已注册的成员函数必须具有签名 int(T::*)(lua_State*)
  • it must have a public static const char[] member called className 它必须有一个名为 public static const char[]className 成员
  • it must have a public static const Luna<T>::RegType[] member called Register

Note: These requirements are of the design choice I made, you may decide on a different interface; with only a few adjustments to the code. 注意:这些要求是我做出的设计选择,您可以决定使用不同的接口;只需对代码进行一些调整。

Luna<T>::RegType is a function map. name is the name of the function that the member function mfunc will be registered as.

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

  struct RegType {
    const char* name;
    const int(T::*mfunc)(lua_State*);
  };

Here’s an example of how to register a C++ class to Lua. A call to Luna<T>::Register() will register the class; the only public interface of the template class. To use the class in Lua you create an instance of it by calling its table constructor function.

​ 以下是如何将 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()

The table of an Account instance looks like this:

​ Account 实例的表如下所示:

  0 = userdata(6): 0x804df80
  balance = function: 0x804ec10
  withdraw = function: 0x804ebf0
  deposit = function: 0x804f9c8 

Explanation 解释

Some may not like the use of C++ templates, but their use here fits in well. They offer a quick tight solution to what initially seemed a complex problem. As a result of using templates the class is very type safe; for example its impossible to mix member functions of different classes in the member function map, the compiler will complain. Additionally, the static design of the template class makes it easy to use, there are no template instantiated objects to cleanup when you’re done.

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

The thunk mechanism is the core of the class, as it “thunks” the call. It does so by taking the object pointer from the table the function call is associated to, and indexing the member function map for the member function pointer. (Lua table function calls of table:function() is syntactic sugar for table.function(table). When the call is made Lua first pushes the table on the stack, then any arguments). The member function index is a closure value, pushed onto the stack last (after any arguments). Initially I had the object pointer as a closure which meant having 2 closure values, a pointer to object(a void*) and the member function index(an int) for every class instantiated for every function; which seemed rather costly but provided quick access to the object pointer. As well, a userdata object in the table for garbage collection purposes was required. In the end I opted to index the table for the object pointer and save on resources, as a result increasing the function call overhead; a table lookup for the object pointer.

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

All facts considered, the implementation makes use of only a few of Lua’s available extension mechanisms, closures for holding the index to the member function, the ‘gc’ tag method for garbage collection, and function registration for table constructor and member function calls.

​ 总体而言,该实现仅使用 Lua 的少数可用扩展机制,包括用于保存成员函数索引的闭包、“gc”标记方法用于垃圾回收,以及用于表构造函数和成员函数调用的函数注册。

Why allow only member functions with the signature int(T::*)(lua_State*) to be registered? This allows your member functions to interact directly with Lua; retrieving arguments and returning values to Lua, calling any Lua API function, etc. Moreover, it provides an identical interface that C functions have when registered to Lua, making it easier for those wishing to use C++.

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

Weaknesses 缺点

This template class solution only binds member functions with a specific signature as described earlier. Thus if you already have classes written, or are intending to use the class in both Lua and C++ environments, this may not be the best solution for you. In the abstract I mentioned that I’d explain that this isn’t really a problem. Using the proxy pattern, we encapsulate the real class and delegate any calls made to it to the target object. The member functions of the proxy class coerce the arguments and return values to and from Lua, and delegate the calls to the target object. You would register the proxy class with Lua, not the real class. Additionally, you may use inheritance as well where the proxy class inherits from the base class and delegates the function calls up to the base class, but with one caveat, the base class must have a default constructor; you cannot get the constructor arguments from Lua to the base class in the proxy’s constructor initializer list. The proxy pattern solves our problem, we now can use the class in both C++ and Lua, but in doing so requires us to write proxy classes and maintain them.

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

Objects are simply new’ed when created, more control as to how an object is created should be given to the user. For example, the user may wish to register a singleton class. One solution is to have the user implement a static create() member function that returns a pointer to the object. This way the user may implement a singleton class, simply allocate the object via new, or anything else. The constructor function could be modified to call create() rather than new to get an object pointer. This pushes more policy unto the class but is much more flexible. A “hook” for garbage collection may be of use to some as well.

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

Conclusion 结论

This note explained a simple method of binding C++ classes to Lua. The implementation is rather simple giving you an opportunity to modify it for your own purposes, at the same time satisfying any general use. There are many other tools for binding C++ to Lua, such as tolua, SWIGLua, and other small implementations like this one. Each with their own strengths, weaknesses, and suitability to your specific problem. Hopefully this note has shed some light on the subtler issues.

​ 本文介绍了一种将 C++ 类绑定到 Lua 的简单方法。该实现相当简单,使您有机会根据自己的目的对其进行修改,同时满足任何一般用途。还有许多其他将 C++ 绑定到 Lua 的工具,例如 tolua、SWIGLua 以及像本文这样的其他小型实现。每种工具都有自己的优势、劣势以及对您特定问题的适用性。希望本文对一些较微妙的问题有所阐述。

The full source of the template class, around 70 lines of source, is available from the Lua add-ons page.

​ 模板类的完整源代码,约 70 行源代码,可从 Lua 附加组件页面获得。

References 参考

[1] R. Hickey, Callbacks in C++ using template functors, C++ Report February 95 [1] R. Hickey,C++ 中使用模板函数对象的回调,C++ 报告 95 年 2 月

最后修改 January 31, 2024: 更新 (e0896df)