루아 모듈의 이름 충돌 방지(3/n)

류광, 2016/07/26 19:40
시리즈 세 번째 글인 이번 글에서는 객체지향적 표기법을 지원하는 루아 C/C++ 확장 모듈에서 발생할 수 있는 이름 충돌 문제를 살펴봅니다.

이 시리즈 첫 글에서 이런 예를 제시했었습니다.

local movie_player = require"movie_lib"

local movie = movie_player.open(arg[1])

if not movie:play() then
	-- 오류 처리
end

...

movie_player.play(movie)보다 movie:play()가 더 짧은 것은 확실합니다. 그리고 이런 객체-기호-멤버 표기가 C ‘핸들’ 스타일의 함수 호출 표기보다 직관적이고 가독성이 좋다는 주장에 공감하는 분들도 많을 것입니다.

루아 C/C++ 확장 모듈에서 이런 표기를 지원하는 방법은 여러 가지인데, 시리즈 첫 글에서는 다음과 같이 루아 파일을 거치는 예를 제시했습니다.

-- movie_lib.lua --

local video_engine = require"my_video_engine" -- 가상의 C/C++ 확장 모듈
local meta = {}
meta.__index = meta

function meta:play()
	return video_engine.play(self.handle)
end

-- ... stop, close, is_playing, seek 등등 ...

return {
	open = function(filename)
		local inst = {}
		inst.handle = video_engine.open(filename)
		setmetatable(inst, meta)
		return inst
	end
}

이 접근방식은 C/C++ 확장 모듈의 구현이 간단하다는 장점이 있지만, 대신 .dll(또는 so) 파일과 함께 .lua 파일도 배포, 설치해야 하므로 좀 번거롭습니다. .dll 하나만 배포하려면 메타테이블 생성 및 설정 작업(위의 예에서 meta.__index = ...setmetatable(...)에 해당하는)을 C/C++ 확장 모듈 안에서 처리해 주어야 합니다.

추가적인 lua 파일 없이 C/C++ 확장 모듈 자체에서 movie:play() 같은 표기법을 지원하려면 사용자 자료(userdata)에 메타테이블을 설정해야 합니다. 큰 틀만 제시하자면, 대략 다음과 같은 형태입니다(Programming in Lua 제3판 제29장의 “array” 예제를 수정한 것입니다). 우선, 모듈 적재(require"movie_lib"에 의한) 시 메타테이블을 하나 만들고 ‘멤버 함수’를 등록해 둡니다.

static const struct luaL_Reg movie_lib [] = {
	{"open", open_movie_file},
	{NULL, NULL}
};

static const struct luaL_Reg movie_member_lib [] = {
	{"play", play_movie},
	{NULL, NULL}
};

int luaopen_movie_lib (lua_State *L)
{
	luaL_newmetatable(L, "MT for Movie object"); // (1)
	lua_pushvalue(L, -1);
	lua_setfield(L, -2, "__index"); 	// meta.__index = meta
	luaL_setfuncs(L, movie_member_lib, 0);     // meta.play = play_movie
	luaL_newlib(L, movie_lib);
	return 1;
}

다음으로, 루아 쪽의 movie_player.open(arg[1])에 의해 open_movie_file이 호출됩니다. 이 함수는 루아 사용자자료(userdata)를 생성하고, 동영상을 대표하는 객체를 생성해서[1] 그 사용자자료와 연결하고, 앞에서 생성한 메타테이블을 그 사용자자료의 메타테이블로 설정합니다(이에 의해 Movie 객체에 ‘멤버 함수’ play(실제 구현은 play_movie)가 등록됩니다).

static int open_movie_file(lua_State *L)
{
	const char * filename = luaL_checkstring(L, 1);
	auto userdata = static_cast<Movie**>(lua_newuserdata(L, sizeof(Movie*)));
	*userdata = new Movie(filename);

	luaL_getmetatable(L, "MT for Movie object"); // (2)
	lua_setmetatable(L, -2);

	return 1;
}

루아 쪽의 movie:play() 호출은 movie_의_메타테이블.play(movie)(좀 더 환원하자면 movie_의_메타테이블['play'](movie))가 되며, 결과적으로 확장 모듈의 play_movie 함수가 호출됩니다. 이 함수는 인수로 넘어온 사용자자료에 연결된 Movie 객체를 이용해서 동영상을 재생합니다.

static int play_movie(lua_State *L)
{
	auto movie = static_cast<Movie*>(lua_touserdata(L, 1));
	movie->play();
	return 0;
}

이상의 코드를 보면 메타테이블 이름으로 "MT for Movie object"라는 문자열이 두 번 쓰였습니다. 이 메타테이블 이름에 충돌의 여지가 있습니다. 같은 루아 상태(코드의 L)에 대해 다른 어떤 모듈이 이미 "MT for Movie object"라는 이름으로 luaL_newmetatable을 호출했었다면, (1)의 luaL_newmetatable은 새 메타테이블을 만드는 것이 아니라 기존 메타테이블을 가져와서 루아 스택에 쌓습니다. luaopen_movie_lib의 이후 코드가 그 메타테이블을 수정하므로, 기존 모듈이 제대로 작동하지 않을 수 있습니다.

루아 5.1 레퍼런스의 해당 항목에서 보듯이, 같은 이름의 메타테이블이 있으면 luaL_newmetatable은 0을 돌려줍니다. 이름 충돌을 피하려면 luaL_newmetatable의 반환값을 점검해서 0일 때에는 다른 이름으로 다시 시도해야 합니다. 또한, 반환값이 1인(즉, 메타테이블이 새로 생성된 경우의) 이름을 기억해 두었다가 play_movieluaL_getmetatable 호출 시 사용해야 하고요.

안타깝게도, luaL_newmetatable의 반환값이 1이 될 때까지 이름을 바꾸어 시도하는 것은 절반의 해결책일 뿐입니다. 내 모듈에서 이름 충돌을 방지한다고 한다고 해도, 내 모듈이 적재된 후에 다른 어떤 모듈이 같은 이름으로 luaL_newmetatable을 호출하지 않으리라는 보장이 없기 때문이죠. 이름 충돌 시 luaL_newmetatable 호출이 오류를 발생하는 등의 좀 더 깊숙한 수준에서의 해결책이 필요한데요. luaL_newmetatable에 의존하는 한(좀 더 일반화한다면 루아 레지스트리에 메타테이블을 등록해 두는 방법에 의존하는 한), 현재로서는 위와 같이 조처하는 것과 함께 메타테이블 이름 자체를 고유하게 지어서 충돌 가능성을 낮추는 것이 최선인 것 같습니다.

luaL_newmetatable과 루아 레지스트리에 의존하지 않는다면 메타테이블 이름 충돌 문제에서 벗어날 수 있습니다. 앞의 예라면 luaopen_movie_lib에서 메타테이블을 생성하지 말고, open_movie_file에서 매번 같은 메타테이블을 생성해서 사용자자료에 설정하는 것이지요. 메타테이블을 한 번만 생성해서 여러 번 재활용하는 것에 비해 비효율적일 것은 확실합니다(어느 정도인지는 측정이 필요하겠지만요).

그렇다면, 루아 레지스트리에 의존하지 않으면서도 메타테이블을 재활용할 수만 있다면 궁극의 해결책이 될 텐데요. 다음 글에서는 그러한 방법을 살펴보겠습니다.


[1] 제대로 하려면 메모리 누수를 방지하는 장치(해당 포인터에 대해 delete를 호출하는 루아 C 함수와 메타테이블 __gc 설정)도 있어야 하지만, 지금 주제와는 무관하므로 생략합니다.

top
트랙백 0 : 의견 # + 0

Trackback Address :: http://occamsrazr.net/tt/trackback/309

comments powered by Disqus

(2013년 11월 10일자로 블로그에도 DISQUS 시스템을 도입했습니다. 기존 의견의 수정, 삭제, 댓글 추가는 여전히 가능합니다.)


◀ PREV : [1] : ... [5] : [6] : [7] : [8] : [9] : [10] : [11] : [12] : [13] : ... [289] : NEXT ▶