LuaCppBindingTemplate

http://www.lua.org/images/logo.gif

Lua: technical note 5 원본파일 : http://www.lua.org/notes/ltn005.html

Lua 에 C++을 바인딩하기 위한 템플릿

(A template class for binding C++ to Lua)

by [email protected] 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(http://sophia.eyep.net/code/luna.tar.gz).

이 문서는 C++ 클래스를 Lua(앞으로 '루아'로 표기)에 바인딩하는 방법을 설명한다. 루아는 C++클래스로 바인딩하는 기능을 직접적으로 지원하지는 않지만, 저수준의 C API 및 익스텐션 메카니즘을 통해 이를 가능케 한다. 필자가 설명하는 방법은 C++ 템플릿(template), 루아의 C API와 익스텐션(extension) 메카니즘을 사용한다. 이것들을 사용하여 클래스 등록 서비스를 제공하는 작고 간단하지만 효과적인 static 템플릿을 작성하는 것이 이 문서의 목적이다. 이 방법은 여러분의 클래스에 몇 가지 제약을 가하는데, 더 구체적으로 설명하자면, 오직 이와 같은 형태 int(T::*)(lua_State*) 를 갖는 클래스 멤버 함수들만이 등록될 수 있다. 하지만, 이러한 제약을 해결하는 방법도 뒤에 설명하겠다. 최종 결과는 클래스를 등록하기 위한 깔끔한 인터페이스이며, 이것은 루아가 사용하는 table 의 의미와도 유사하다. 여기에서 설명하는 해결책은 필자가 Luna(http://sophia.eyep.net/code/luna.tar.gz) 에서 작성했던 템플릿 클래스에 기반한다.

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.

루아의 API 는 루아에 C++ 클래스를 등록하게끔 설계되어 있지 않다. 오직 int()(lua_State*) 의 형식을 갖춘 C 함수만이 가능하다. 즉, lua_State 포인터를 인수로 취하고 정수형을 리턴하는 함수만이 가능하다는 것이다. 실제로, 이것은 등록작업(registering) 시에 루아가 유일하게 지원하는 C 데이터 타입이다. 다른 타입을 등록하려면 여러분은 루아가 제공하는 확장 메카니즘인 태그메소드(tag method), 클로져(closure) 등등을 사용해야 한다. C++ 클래스를 루아에 등록하기 위한 해결책을 제공하기 위하여, 우리는 이러한 확장 메카니즘을 사용해야만 한다.

The solution 해결책

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

해결책을 위해서는 네 가지 구성요소가 필요하다. 그것은 클래스 등록, 객체 인스턴스화, 멤버 함수 호출, 가베지 콜렉션(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.

클래스 등록(class registration)은 테이블(table) 생성자(constructor) 함수를 클래스의 이름과 함께 등록시킴으로써 수행된다. 테이블 생성자 함수는 테이블 객체를 리턴하는 작업을 하는 템플릿 클래스의 static 메소드이다.

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.

주의: static 클래스 멤버 함수는 C 함수와 호환된다. 형태(signature)가 동일하기 때문에, 루아에 등록시키는 것이 가능하다. 아래의 짧은 코드는 템플릿 클래스의 멤버 함수에 대한 코드이다. 'T' 는 바운드(bound)되는 클래스를 의미한다.

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

    if (otag == 0) {
      otag = lua_newtag(L);
      lua_pushcfunction(L, &amp;Luna<T>::gc_obj);
      lua_settagmethod(L, otag, 'gc'); /* 객체를 해제하기 위한 tm(태그 메쏘드) */
    }
  }

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.

객체 인스턴스화(object instantiation)는 사용자가 테이블 생성자 함수에 전달한 모든 인수들을 C++ 객체의 생성자에 전달하고, 객체를 대표할 테이블 을 생성하고, 클래스의 멤버 함수들을 테이블에 등록하고, 마지막으로 테이블을 루아에 돌려주는 방식으로 수행된다. 객체 포인터는 테이블 안에서 인덱스 0에 위치하는 사용자데이터(userdata)로 저장된다. 멤버 함수 map 에 들어가는 인덱스는 각 함수에 대한 클로져 값으로 저장된다. 멤버 함수 map 은 나중에 더 자세히 설명하겠다.

  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); /* 새로운 테이블 객체 */
    lua_pushnumber(L, 0); /* 인덱스 0 에 사용자데이터 객체를 입력 */
    lua_pushusertag(L, obj, otag); /* gc(가베지 콜렉터)가 tm(태그 메소드)를 호출하도록 */
    lua_settable(L, -3);

    /* 멤버 함수 등록 */
    for (int i=0; T::Register[i].name; i++) {
      lua_pushstring(L, T::Register[i].name);
      lua_pushnumber(L, i);
      lua_pushcclosure(L, &amp;Luna<T>::thunk, 1);
      lua_settable(L, -3);
    }
    return 1; /* 테이블 객체 리턴 */
  }

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'한 다음, 실제 함수를 호출하는 함수에 의해 수행된다. 멤버 함수 포인터는 클로져 값을 통해 멤버 함수 map 에 인덱싱되며, 객체 포인터는 테이블 상의 인덱스 0 에 위치한다. 루아 상의 모든 클래스 함수들이 이 함수를 통해 등록된다는 사실을 유념하자.

  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); /* 인덱스 0에는 사용자데이터 객체 */
    lua_gettable(L, 1);
    T* obj = static_cast<T*>(lua_touserdata(L,-1));
    lua_pop(L, 2); /* 클로져 값과 obj 를 팝(pop) */
    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.

가베지 콜렉션은 테이블 내의 사용자데이터에 대한 가베지 콜렉션 태그 메소드(tag method)를 세팅함으로써 수행된다. 가베지 콜렉터가 실행되면, 'gc' 태그 메소드가 호출되어, 객체는 간단히 삭제된다. 'gc' 태그 메소드는 클래스를 등록하는 과정 중에 새로운 태그로 등록된다. 위의 객체 인스턴스화 과정에서, 사용자데이터는 태그 값을 사용하여 태그화된다.

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

이러한 것들을 염두할 때에, 여러분이 이미 알고 있는 사항들일 지도 모르지만, 반드시 지켜야 할 몇가지 요구사항이 존재한다:

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::RegType is a function map. name is the name of the function that the member function mfunc will be registered as. Luna::RegType 은 함수 map 이다. 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::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++ 클래스를 루아에 등록시키는 예제를 보였다. Luna::Register() 를 호출하면 클래스를 등록하게 된다. 이 함수는 템플릿 클래스의 단 하나뿐인 public 인터페이스이다. 루아에서 클래스를 사용하기 위해 여러분은 table 생성자 함수를 호출하여 클래스의 인스턴스를 생성한다.

  class Account {
    double m_balance;
   public:
    Account(lua_State* L) {
      /* constructor table at top of stack */
      /* 스택의 최상단에 생성자 table 이 위치함 */
      lua_pushstring(L, 'balance');
      lua_gettable(L, -2);
      m_balance = lua_tonumber(L, -1);
      lua_pop(L, 2); /* pop constructor table and balance *//* 생성자 table과 balance 를 꺼냄(pop) */
    }

    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',  &amp;Account::deposit },
    { 'withdraw', &amp;Account::withdraw },
    { 'balance',  &amp;Account::balance },
    { 0 }
  };

  [...]

  /* Register the class Account with state L */
  /* state L 을 사용하여 Account 클래스 등록 */
  Luna<Account>::Register(L);

  -- 루아에서
  -- Account 객체를 생성
  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++ 템플릿을 별로 좋아하지 않을 지 모르지만, 여기에서는 템플릿을 사용하는 것이 적절하다. 템플릿을 사용하면 처음에는 복잡해 보였던 문제도 빠르고 정확히 해결할 수 있다. 템플릿을 사용하기 때문에, 클래스는 상당한 타입 안전성을 갖는다. 예를 들면, 멤버 함수 map 안에서 다른 클래스의 멤버 함수들을 섞어 사용하는 것은 불가능하다. 컴파일러가 에러를 발생시키기 때문이다. 뿐만 아니라, 템플릿 클래스의 정적인(static) 설계는 클래스의 사용을 쉽게 만들며, 여러분이 작업을 끝마쳤을 때에 템플릿 인스턴화된 객체를 정리할 필요가 없도록 해준다.

-- 여기까지

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.

썽크 메카니즘은 호출을 '썽크(thunks)'하는 것처럼 클래스의 핵심이다. 함수 콜이 연관된 테이블로부터 객체 포인터를 취하고, 멤버 함수 map을 멤버 함수 포인터에 인덱싱하는 식으로 수행된다. (루아 테이블 함수 호출 table:function()table.function(table) 의 더 편한 용법이다. 호출이 이루어질 때 루아는 먼저 테이블을 스택에 추가하고, 나머지 인자들을 추가한다). 멤버 함수 인덱스는 closure 값이다. 스택에 가장 늦게 추가된다(다른 인자들 추가한 다음에). 초기에 필자는 객체 포인터를 2 개의 closure 값을 갖는 closure 로 간주했는데, 그 2개의 closure 는 객체(void* 타입)와 멤버 함수 인덱스(int 타입)을 모든 함수에 대하여 인스턴스화된 모든 클래스에 대하여 사용된다; 이것은 오히려 더 비용이 많이 들지만, 객체 포인터에 대해 빨리 접근할 수 있다. 뿐만 아니라, 가베지 콜렉션용으로 테이블 내에 userdata 객체가 필요하다. 끝으로, 필자는 결과적으로 함수 호출 오버헤드를 가중시키기는 했지만, 객체 포인터에 대해 테이블을 인덱스하여 리소스들 상에 저장하기로 선택했다; 객체 포인터에 대한 테이블 룩업.

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.

모든 것을 고려해 볼 때, 구현은 루아에서 사용할 수 있는 확장 메카니즘의 일부만을 이용한다. 멤버 함수에 대한 인덱스를 담기 위해 closure 를 사용하고, 가베지 콜렉션을 위해 'gc' 태그 메소드를 사용하고, 테이블 생성자 및 멤버 함수 호출을 위해 함수 registration 을 사용한다.

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*) signature 를 갖는 멤버 함수만 등록을 허용하는가? 이는 여러분의 멤버 함수들이 루아와 직접적으로 상호작용할 수 있도록 하기 위함이다; 인수를 취하고, 루아에 값을 리턴해주고, 루아 API 함수를 호출하는 등등. 뿐만 아니라, 루아에 등록될 때 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.

이 템플릿 클래스 솔루션은 앞서 설명한 대로 특정 signature 를 갖는 멤버 함수들만을 바인딩시킨다. 따라서, 만약 여러분이 이미 작성된 클래스를 가지고 있거나, 루아와 C++ 환경 모두에서 사용할 수 있는 클래스를 작성하려고 한다면, 이 방법은 최선의 방법은 아니다. 개요부분에서 필자는 이것이 사실 별 문제가 아니라는 것을 밝힌다라고 언급했다. 프락시(proxy) 패턴을 사용하면, 우리는 실제 클래스를 캡슐화하고 클래스에 만들어진 호출들을 목표 객체에 위임한다. 프락시 클래스의 멤버 함수들은 루아로부터 오고 루아로 보내지는 인수들 및 리턴값들을 강제로 조작하여 호출들을 목표 객체에 위임한다. 여러분은 루아에 실제 클래스가 아닌, 프락시 클래스를 등록할 수 있다. 추가적으로, 여러분은 프락시 클래스가 베이스 클래스로부터 상속하고자 할 때, 또한, 베이스 클래스에 함수 호출을 위임하고자 할 때 상속을 사용할 수도 있지만, 한가지 주의할 점은, 기반 클래스는 디폴트 생성자를 가져야 한다는 것이다; 여러분은 루아로부터 프락시의 생성자 초기화 목록(constructor initializer list) 안에 있는 베이스 클래스로 넘겨줄 생성자 인수들을 얻을 수는 없다. 프락시 패턴으로 이러한 문제를 해결할 수 있다. 따라서, 우리는 이제 C++ 과 루아 모두에서 클래스를 사용할 수 있게 되었다. 하지만, 그렇게 하려면 프락시 클래스들을 작성해야 하며 또한 그것들을 관리해야 한다.

Objects are simply new'ed when created, more control as to how an object is created should be given to the user. 객체들은 생성될 때 단순히 new 되어지며, 객체를 어떻게 생성하는 가에 대한 더 상세한 컨트롤은 사용자에게 맡겨진다.

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. 한가지 해결책은 유저가 create()라는 정적 멤버 함수로 객체의 포인터를 리턴받도록 구현 하게끔 하는것이다.

This way the user may implement a singleton class, simply allocate the object via new, or anything else. 이 방법은 유저가 싱글톤 클래스를 구현하는데 있어 new, 혹은 그 외의 방법을 통해 간단하게 객체를 할당할수 있게 한다.

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. contructor 함수는 객체의 포인터를 얻을때에 new보다 create()를 사용하게 한정할 수도 있다. 이것은 클래스에 대해 다소 한정적일수도 있지만, 상당한 유연성을 얻을수 있다. 쓰레기를 모으기 위한 'hook'도 유용할수 있는 것이다.

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 'http://www.lua.org/addons.html' add-ons page. 템플릿 클래스의 풀소스는 약 70 라인 정도되는데, 루아 애드온(add-on) 페이지 'http://www.lua.org/addons.html'에서 다운받을 수 있다.

References참조

1] R. Hickey, 'http://www.tutok.sk/fastgl/callback.html' Callbacks in C++ using template functors, C++ Report February 95

Last update최근 갱신: Thu Feb 22 14:25:52 EST 2001 by lhf(http://www.tecgraf.puc-rio.br/~lhf/).


분류:스크립팅