번역서 정보 | 번역서 질문&의견 | 번역 이야기 | 문서 창고 | 방명록

루아 모듈의 이름 충돌 방지(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

근황 - 2016-06-30

류광, 2016/06/30 21:54
C# 6.0 in a Nutshell 탈고, Introduction to 3D Game Programming with Direct3D 12.0 번역 예정.

5월에 글이 없었는데 6월 넘기기 전에 근황 글이라도 올립니다;;;

3월에 번역 시작한 C# 6.0 in a Nutshell을 오늘 탈고했습니다. 1000페이지 넘는 책이라 조판과 교정에 시간이 좀 걸릴 것입니다.

C# 6.0 in a Nutshell은 꽤 유명한 시리즈의 최신판이고, 저자 중 한 명은 인기있는 C# 학습 도구인 LINQPad의 개발자입니다. LINQPad는 이를테면 파이썬의 IDLE 같은 것인데, 아기자기한 기능들이 더 많고요. 루아에도 이런 도구가 있으면 좋겠습니다.

7월부터는 Frank D. Luna의 최신작 Introduction to 3D Game Programming with Direct3D 12.0을 번역할 계획입니다. 2014년 GEG 2 이후에 게임 개발 관련 서적을 할 기회가 없어서 아쉬웠는데요. 11월 탈고 예정이라 올해는 힘들겠고, 내년에는 게임 개발서가 나오겠습니다. 기대해 주세요!

top
트랙백 0 : 의견 # + 0

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

류광, 2016/04/29 23:02
이번 글에서는 루아 C API를 이용한 루아 C/C++ 확장 모듈의 이름 충돌 방지를 살펴봅니다.

이전 글에서는 순수 루아 모듈의 이름 충돌 방지를 이야기했습니다. 이번 글에서는 루아 C API를 이용한 루아 C/C++ 확장 모듈의 이름 충돌 방지를 살펴봅니다.

C/C++ 확장 모듈의 이름 충돌 문제도 순수 루아 모듈의 것과 본질적으로 동일합니다. 결국은 모듈이 전역 변수(함수도 그냥 함수 형식의 값을 가진 변수임을 기억하세요)를 만드느냐, 다른 말로 하면 전역 테이블을 건드리느냐의 문제입니다.

이전 글의 처음에 등장하는 “원시적인” 함수 라이브러리의 모듈에 해당하는 C/C++ 확장 모듈은 루아 '접착제' 함수들 각각을 lua_register(또는 lua_pushcfunction과 lua_setglobal의 조합)을 이용해서 직접 전역 테이블에 추가하는 형태일 것입니다. 이전 글에서도 말했듯이 이런 접근방식으로는 이름 충돌을 피할 수 없습니다.

개별 함수들의 이름 충돌을 피하기 위해서는 함수들을 하나의 테이블로 묶을 필요가 있는데, 그런 경우를을 위해 루아 5.0과 5.1은 luaL_register라는 보조 함수를 제공합니다. 아마 5.0과 5.1용 C 확장 모듈은 대부분 이 함수를 사용할 것입니다.

void luaL_register (lua_State *L,
                    const char *libname,
                    const luaL_Reg *l);

libname 매개변수에 이름(C 문자열)을 지정해서 이 함수를 호출하면 그 이름으로 전역 변수가 만들어집니다. 예를 들어 PIL 제1판(루아 5.0 대응)과 제2판(루아 5.1 대응) §28.1에는 다음과 같은 예제가 있습니다.

static const struct luaL_Reg arraylib [] = {
  {"new", newarray},
  {"set", setarray},
  {"get", getarray},
  {"size", getsize},
  {NULL, NULL}
};

int luaopen_array (lua_State *L) {
  luaL_register(L, "array", arraylib);
  return 1;
}

이 코드는 new, set, get, size라는 필드(값은 C 함수)를 가진 테이블을 생성해서 array라는 이름의 전역 변수에 배정합니다. 결과적으로, 전역 이름공간 오염의 관점에서는 이전 글의 “require"mymp3lib"” 예제와 같습니다.

다행히 luaL_register의 libname 매개변수에 널(0)을 지정할 수 있습니다. 그러면 luaL_register는 그냥 테이블에 생성하기만 하고 전역 변수에는 배정하지 않습니다. 그 테이블을 그대로 돌려주기만 하면 이전 글의 “local my_movie_lib = require"movie_lib"” 패턴과 같은 효과가 납니다. 지금 예의 경우 luaopen_array를 다음처럼 바꾸면 됩니다.

int luaopen_array (lua_State *L) {
  luaL_register(L, 0, arraylib); // "array" 대신 널 포인터
  return 1;
}

(현대적인 C++에서는 0 대신 nullptr를 사용해야겠죠.)

루아 5.2에서는 luaL_register가 사라지고 luaL_newlib가 생겼습니다. 다음에서 보듯이 모듈 이름을 지정하는 매개변수가 아예 없습니다.

void luaL_newlib (lua_State *L, const luaL_Reg *l);

따라서 일부러 luaL_register를 만들어서 쓰지 않는 한, 5.2부터는 전역 이름공간을 오염시키지 않는 모듈 패턴이 '저절로' 적용됩니다.

이것으로 테이블 안에 담긴 함수 라이브러리 형태의 확장 모듈의 이름공간 오염 문제는 확실히 해결됩니다. 그러나 루아의 이름 충돌 문제가 모두 해결된 것은 아닙니다. 이전 글의 “local video_engine = require"my_video_engine"” 예제처럼 마치 클래스 비슷하게 사용하는 모듈을 C API로 구현하는 경우에는 겉으로 드러나는 전역 이름공간 말고도 오염될 수 있는 이름공간이 더 있기 때문입니다. 이에 대해서는 다음 글에서 살펴보겠습니다.

top
트랙백 0 : 의견 # + 0

"클라우드를 관리하는 기술" 증정 이벤트 결과 발표

류광, 2016/03/10 18:01

*** 아직 제 편지 못 받으신 당첨자 분은 스팸함을 한 번 확인해 보세요. 보낸 사람 이름은 'GpgStudy'입니다('류광'이 아니라).***

세 문제의 답은

  1. Lua(루아)
  2. PHP
  3. Python(파이썬, 파이선)

입니다. 누구나 맞출 수 있게 쉽게 낸다고 냈는데, 2번에서 좀 삐끗했습니다.

세 문제 모두 맞춘 분들의 수가 3을 초과해서, Random.org의 목록 무작위화 서비스(https://www.random.org/lists/)를 이용해서 세 분을 추첨했습니다. 당첨자는 다음과 같습니다.

  1. Geunho Lee
  2. Se-Yeon Kim
  3. 김영호

세 분 축하합니다. 조만간 메일이 갈 것이니 답장해 주시고요.

참여해주신 모든 분께 감사드립니다!

top
트랙백 0 : 의견 # + 0

독자 증정 이벤트 추가 사항입니다.

류광, 2016/03/05 21:26

문제 2가 애매해서 문구를 바꾸었다는 이야기를 원 글의 끝에 추가했는데 혹시 못 보신 분들이 있을까봐 다시 알려 드립니다.

어제(3월 4일) 오후 2시 50분 경에, 문제 2의 마지막 문장에 "(같은 책의 제1판과 제2판)"이라는 문구를 추가했습니다. 원래 문제에서는 제가 생각한 것과는 다른 답이 가능했는데, 이 문구 때문에 이제는 제가 생각한 답만 정답이 됩니다.

어제 오후 3시 30분까지 제출된 답들에 대해서는 다른 답도 정답으로 처리해 드린다고는 했지만, 원래 문제를 보고 답을 찾았다가 나중에야 방명록에 쓰신 분들도 있을 수 있고, 또 실제로 글을 올린 시간과 Disqus(이 곳에서 사용하는 클라우드 댓글 시스템) 쪽에 등록된 시간에 차이가 있을 수도 있어서, 이렇게 하기로 했습니다:

혹시 제출한 답이 아닌 것 같다 싶으신 분들은, 원하시면 언제라도(물론 마감 전까지) 비공개 방명록에 다시 답을 제출하셔도 좋습니다.

마지막으로 제출한 답만 보겠습니다. 기존 글을 수정하면 제가 못 보고 지나칠 수 있으니 새 글로 올려 주세요. 문제 2뿐만 아니라 문제 1이나 3의 답을 새로 제출하는 것도 가능합니다.

진행이 매끄럽지 못한 점 죄송합니다!

top
TAG 공지
트랙백 0 : 의견 # + 0

◀ PREV : [1] : [2] : [3] : [4] : [5] : ... [57] : NEXT ▶