LuaTinker Manual

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

LuaTinker Manual

진행하기에 앞서서..

LuaTinker 는 Boost.Python 이나 LuaBind 와 흡사한 형태를 갖도록 작성되어 졌습니다. 따라서 많은 부분은 Boost 라이브러리와 흡사한 모양을 띄고 있지만 Boost를 사용하지 않기 때문에 문법적으로나 기타 여러가지 부족한 부분들이 있으리라 생각합니다. 개선 사항이나 코드에 대한 의견은 언제든지 환영이고 코드를 뜯어서 고치는 것도 편하게 작업해 주시면 좋겠습니다.

현재 이 메뉴얼은 2005년 5월 27일자 기준의 코드로 작성되었습니다.

현재 지원 기능

추후 추가할 사항들

예제 참고하기

일단 가장 좋은 방법은 자료실에 등록된 예제를 참고하는 것입니다. 간단한 예제와 함께 몇가지 사용법이 들어 있으므로 이 부분들을 활용하면 큰 어려움은 없으리라 생각됩니다. 다음은 차례대로 함수를 다루는 법, 클래스를 다루는 법, 루아에서 사용하는 방법등을 언급해 보겠습니다. 아래의 T 는 template 의 typename 에 해당하고 일반적으로 클래스를 넣어주게 되어 있습니다. 자료실에 등록된 예제를 참고하시고... (자료 올린후 뒷부분 업데이트)

LuaTinker를 초기화 작업

lua_state::open(L)

LuaTinker 에게 Lua 로 부터 생성된 lua_state* 를 알려주는 작업입니다. 이 함수를 호출할 경우 LuaTinker 내부에 전역으로 선언된 값이 셋팅되게 되어 있습니다. 현재 여러개의 lua_state 가 선언될 경우는 가정하지 않고 작업되어 있습니다.

luabind::open(L)

LuaTinker는 현재 문법적으로 LuaBind 와 호환되게 인터페이스가 만들어져 있습니다. 몇몇 기능상 호환되지 않는 부분이 있고 LuaTinker 만의 고유한 기능이 계속적으로 추가되고 있기 때문에 앞으로 문법적 호환성이 큰 도움이 안될 수도 있으나 LuaTinker 만의 고유 기능이 필요 없을 경우 간단하게 namespace 를 바꿔가면서 두가지 라이브러리를 변경해서 쓸 수 있습니다. 이에 대한 주의 사항은 LuaTinker 소개 페이지에 간단히 언급 하였고 저도 제가 사용하는 범위를 넘어서서 테스트 해보지 않았기 앞으로 호환성에 관계하여 더 확장되리라 상각되지 않지만 관련 인터페이스는 계속 남겨둘 계획입니다.

함수를 다루는 법

def(name, func)

어떠한 함수를 Lua 쪽으로 노출 시켜줍니다. 함수 이름은 name 에 해당하는 것으로 보이게 되고 func 은 일반적인 함수를 넣어주면 됩니다. 가령 int test() 라는 C 함수가 있다면 이는

def('test', test);

라고 정의해 주면 루아측에서

test()

와 같이 일반적인 전역으로 바로 호출 가능하게 됩니다.

call(name, ...)

이 함수는 Lua 쪽에 선언되어 있는 어떠한 함수를 호출하고 반환값을 받아오는 함수입니다. 만약 루아 함수쪽에서 반환값을 돌려주지 않을 경우 LuaTinker 가 그에 적합한 값을 읽어오도록 노력하고 0 또는 NULL 값을 리턴하게 되어 있습니다. 만약 Lua 쪽에 다음과 같은 함수가 있다면

function test(name, value)
   print(name, value)
   return 'end'
end

이것을 C++ 쪽에서 호출할때는

const char* rval = call<const char*>('test', 'zupet', 10);

와 같이 호출할 수 있습니다. Lua 쪽에서 올바른 값을 리턴할 경우 그에 적합한 포인터가 반환되고 그렇지 않을 경우 NULL 이 리턴됩니다. 이는 LuaTinker 내에서 적합한 형변환을 찾도록 노력하기 때문에 우리가 명시적으로 const char* 타잎을 요구하였기 때문에 lua_tostring() 을 호출하여 받은 결과입니다. 현재 실행중 올바른 형을 체크하지 않기 때문에 Lua 쪽에서 자료형을 받아올 경우 조심스러운 접근이 필요합니다.

클래스를 다루는 법

LuaTinker는 userdata 타잎과 그에 붙은 metatable 을 이용해서 하나의 인스턴스를 표현합니다. metatable 은 index, newindex 를 직접 구현해서 유저가 어떠한 클래스 멤버 함수를 호출했을때 그에 적당한 function 을 리턴해주게 됩니다. 서로 다른 인스턴스는 각각 별도의 userdata를 할당받지만 metatable 은 모두가 공유되기 때문에 LuaTinker 로 전달된 포인터들은 큰 부담 없이 metatable 만 붙여서 사용되게 되어 있습니다.

class_<[T]>(name)

C++ 에서 T로 정의된 클래스를 Lua 쪽에 선언하는 객체입니다. class_ 템플릿 인스턴스로 생성자에서 새로운 table을 생성하고 name 이라는 전역으로 이를 선언해 줍니다. 이 table은 새로운 인스턴스들에 대한 metatable로 쓰일 수 있도록 기본적인 __index, __newindex, __gc 등이 정의되게 되어 있습니다.

.def(name, func)

class_ 의 멤버 함수중 하나로 Lua 쪽으로 노출된 클래스에 대해서 함수들을 노출시키게 됩니다. 기본 정의나 사용법은 위의 일반 함수를 정의하는 경우와 완벽하게 같지만 만약 오버로딩된 함수가 있을 경우 명시적으로 함수 자료형을 정의해 주어야 합니다.

.def_readwrite(name, variable)

class_ 의 멤버 함수중 하나로 Lua 쪽으로 노출된 클래스에 대해서 멤버 변수들을 노출 시키게 됩니다. LuaTinker 는 항상 읽기/쓰기에 대해서 열려있는 자료형만 노출하게 되고 설사 const 로 정의되어서 변경이 불가능한 객체일 지라도 강제로 값을 읽고 쓸 수 있게 만들어줍니다. 만약 Lua 가 실행되는중 원래 선언된 자료형과 다른 것을 대입하려고 할 경우 0 또는 NULL 로 값을 채우게 됩니다.

'''.inh

()'''

class_ 의 멤버 함수중 하나로 Lua 쪽으로 노출된 클래스를 P 타잎의 부모 객체로 부터 상속받았다는 것을 정의해 주게 됩니다. 만약 P 타잎이 정의되어 있지 않다면 이 함수는 상속작업을 진행하지 않습니다. 상속은 현재 1개의 부모에 대해서만 가능하고 여러단계 상속을 받더라도 정상적으로 Lua 에서 인식하게 됩니다.

객체 전달의 편의성 향상

decl(name, value)

이는 단순하게 value 의 값을 Lua 의 전역으로 선언해줍니다. 선언할 수 있는 것은 다양한데 객체의 경우 포인터를 넣어주는 것이 적합하며 복사가 일어나도 된다면 그냥 인스턴스를 통채로 전달해도 무방합니다. 저는 많은 관리자들과 프로그램상에 단일체로 존재할 필요 없는 단일 객체들을 전부 전역으로 사용하고 있는데 이런 것들을 선언해주면 여러모로 편하게 사용할 수 있습니다.

C++ 코드

class Renderer {
   void SetBgColor(int r, int g, int b);
};
Renderer g_renderer;
class_('Renderer')
   .def('SetBgColor', Renderer::SetBgColor)
   ;
decl('g_renderer', &g_renderer);

Lua 코드

g_renderer:SetBgColor(0, 0, 255)

struct lua_value {};

LuaTinker 의 강력한 기능은 바로 LuaTinker 내에 정의된 struct push_{} 와 struct pop_{} 객체입니다. 이 두개의 객체는 각각 C++ -> Lua 또는 Lua -> C++ 로 값을 전달하는 작업을 해주는 템플릿들이고 이를 통해서 아주 간단하게 Lua와 C++코드가 서로 통신을 해주고 있습니다. (가장 좋은 예는 call 함수의 구현부를 살펴보는 것입니다.)

그런데 push_ 객체에는 lua_value 라는 특별한 값을 받게 되어 있습니다. 이는 상속받은 객체가 부모의 형으로 자료형이 선언된 상태에서도 자신을 Lua로 보여주기 위한 자료형입니다. 문장보다는 간단한 예제로 설명하는 쪽이 편리하리라 생각됩니다.

C++ 코드

class A {
};
class B : public A {
   bool DoB() { printf('this is B !!!'); }
};
A* FindObject(const char* name) { /* 뭔가 A 형에서 상속받은 녀석을 리턴한다 */ }
def('FindObject', FindObject);

Lua 코드

local temp = FindObject('B')
temp:DoB() -- error

위와 같이 호출을 하게 되면 C++ 에서 Lua로 자료형이 넘어갈때 B의 객체가 부모인 A의 객쳐 형태로 전달되게 됩니다. 따라서 스크립트 상에서 넘겨받은 인스턴스는 B가 아닌 A로 인식이되고 B에서 확장된 인터페이스나 기능을 전혀 활용할 수 없게 됩니다. 따라서 push_ 객체는 lua_value 라는 자료형을 받을 경우 직접 자료형을 Lua 에 전달하지 않고 lua_value에게 자료형을 전달하는 작업을 위임하게 됩니다.

struct lua_value
{
   virtual void to_lua(lua_State *L) = 0;
};

이것의 사용법은 다음과 같습니다.

C++ 코드

class A : public lua_value {
   void to_lua(lua_State *L) { push_::invoke(this);
};
class B : public A {
   bool DoB() { printf('this is B !!!'); }
   void to_lua(lua_State *L) { push_::invoke(this);
};
A* FindObject(const char* name) { /* 뭔가 A 형에서 상속받은 녀석을 리턴한다 */ }
def('FindObject', (lua_value*)(*(const char*))FindObject);

위와 같이 코드를 약간만 확장할 경우 push_ 객체는 전달받은 포인터를 바로 사용하지 않고 lua_value::to_lua() 함수를 역으로 호출해 줌으로써 자료 전달을 위임하게 됩니다. 위의 코드를 실행할 경우 test 의 자료형은 자신의 자료형에 맞춰서 입력되게 됩니다. 위의 코드를 Lua 에서 보게 되면 아래와 같습니다.

Lua 코드

local temp = FindObject('A')
temp:DoB() -- error (A 는 B 타잎이 아니므로 인터페이스가 없다)
temp = FindObject('B')
temp:DoB() -- ok (B 타잎을 요구해서 B 타잎이 리턴했으므로 OK 이다)

위와 같은 구현이 필요한 이유는 클래스의 가상 함수만으로는 상속받는 클래스들의 모든 구현을 노출시킬 수 없기 때문입니다. 물론 모든 인터페이스를 가상 함수로 만들어서 지원할 수 있지만 이는 코드를 복잡하게 하고 Lua 를 위해서 C++ 코드가 너무 많은 희생을 하는 것으로 생각되기 때문에 이를 피하기 위한 것이 바로 lua_value 의 선언인 것입니다. 위와 같은 구현은 MFC 가 모든 함수들을 virtual로 선언하지 않은 이유와 흡사하고 Lua 에서 C++의 제한적인 상속 구조를 그대로 이어받지 않고 좀더 편리하게 모든 객체에 접근할 수 있도록 구현한 것입니다.

p.s.메뉴얼 작성중입니다.. 시간 나는대로 갱신하고 자료실에 0.2 버젼도 올리도록 하겠습니다.