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

Twitter icon류광, 2016-10-07 13:10
시리즈 네 번째(이자 아마도 마지막) 글인 이번 글에서는 객체지향적 표기법을 지원하는 루아 C/C++ 확장 모듈에서 발생할 수 있는 이름 충돌 문제의 좀 더 나은 해결책을 이야기합니다.

저번 글 마지막에서 “루아 레지스트리에 의존하지 않으면서도 메타테이블을 재활용할 수만 있다면”이라고 말했는데, 이번 글에서 구체적인 방법을 살펴보겠습니다.

문제를 좀 더 정확히 정의하자면 이렇습니다. “루아 확장 모듈 적재 함수(luaopen_XXX)에서 생성/설정한 메타테이블을 그 메타테이블을 필요로 하는 루아 확장 함수(특히, 루아 객체 생성 함수)에서 조회하되 그 어떤 루아 전역 정보에도 의존하지 않고 조회하려면?”

전통적인 luaL_newmetatable 기반 해법은 “그 어떤 루아 전역 정보에도 의존하지 않고”를 만족하지 않는다는(따라서 이름 충돌의 여지가 있다는) 것이 저번 글 마지막 부분의 요지였죠.

거두절미하고, 모듈 적재 함수에서 어떤 정보를 그 어떤 고유한 값에도 의존하지 않고 특정 루아 확장 함수에 전달하는 수단은 바로 윗값(upvalue)입니다. 아주 거칠게 말하면, 루아 함수는 실행 시점에서 생성, 전달, 삭제가 가능한 객체이고, 윗값은 함수 객체의 멤버 변수입니다. 다음 예를 봅시다.

local function make_adder(amount)
    return function(val)
        return val + amount
    end
end

local add_5 = make_adder(5)
local add_7 = make_adder(7)

print(add_5(6)) -- 11
print(add_7(6)) -- 13

여기서 amount가 바로 윗값입니다. add_5라는 함수 ‘인스턴스’의 amount는 5이고, add_7amount는 7입니다. 이 예에서 보듯이 윗값은 함수를 생성하는 시점에서 함수 내부의 정보를 설정하는 수단으로 유용합니다. 그리고 다행히 루아 C API도 루아 확장 함수를 루아 상태에 등록(생성)할 때 윗값을 지정하는 수단을 제공합니다.

그럼 저번 글의 동영상 재생 라이브러리 예제를 윗값을 이용해서 개선해 봅시다. 바뀐 것은 함수 두 개인데요. 우선 모듈을 적재하는 함수는 다음과 같습니다.

int luaopen_movie_lib (lua_State *L)
{
    lua_newtable(L); // t (모듈 자체)

    lua_newtable(L); // mt (메타테이블)
    luaL_setfuncs(L, movie_member_lib, 0);     // mt.play = play_movie
    lua_pushvalue(L, -1); 
    lua_setfield(L, -2, "__index");     // mt.__index = 메타테이블
    lua_pushcclosure(L, open_movie_file, 1); // mt를 open_movie_file 함수의 윗값으로 설정
    lua_setfield(L, -2, "open"); t.open = open_movie_file
    return 1; // 모듈을 반환
}

이전과는 달리 lua_newmetatable을 사용하지 않고 lua_newtable을 이용해서 메타테이블을 직접 생성하고, 메타테이블을 필요로 하는 open_movie_file 함수를 등록할 때 메타테이블을 그 함수의 한 윗값으로 설정합니다. lua_pushcclosure의 셋째 매개변수는 주어진 함수에 설정할 윗값들의 개수입니다. lua_pushcclosure는 루아 스택에서 그 개수만큼의 값들을 뽑아서 윗값들로 설정합니다. lua_pushcclosure 호출 시점에서 스택 최상위에는 메타테이블이 쌓여 있으므로, 그것이 open_movie_file의 윗값으로 설정됩니다.

다음으로, 함수에서 자신의 윗값을 가져오는 방법을 봅시다. 새로운 open_movie_file은 다음과 같습니다.

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);

    lua_pushvalue(L, lua_upvalueindex(1)); // 윗값(메타테이블)을 가져와서
    lua_setmetatable(L, -2); // 이 사용자자료의 메타테이블로 설정

    return 1;
}

저번 글의 예제에서는 luaL_getmetatable(L, "MT for Movie object");로 메타테이블을 가져왔지만 이번에는 lua_pushvalue(L, lua_upvalueindex(1));로 가져옵니다. 루아 C API 함수 lua_pushvalue는 스택의 특정 항목을 스택 최상위에 복제하는데, 둘째 매개변수가 바로 그 항목의 스택 색인입니다. 그리고 lua_upvalueindex(1)는 현재 함수의 첫 번째(1) 윗값의 색인을 돌려줍니다.

저번 글의 예제에서는 메타테이블을 설정하고 조회할 때 "MT for Movie object"라는 문자열을 사용했지만, 이번 예제에서는 그런 문자열이 없습니다. 즉, 이름 충돌의 여지가 사라진 것입니다.

안타깝게도, 이 역시 ‘궁극의’ 해결책은 아닙니다. 다음과 같은 코드는 오류를 발생합니다.

local movie_player = require"movie_lib"

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

local music = music.open(arg[2])

movie.play(music) 

movie.play는 저번 글 예제의 static int play_movie(lua_State *L)에 해당합니다. 다시 제시하자면:

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

이 함수는 첫 인수가 Movie 객체를 가리키는 포인터를 담은 사용자자료라고 기대하지만, movie.play(music) 호출의 첫 인수는 그런 사용자자료가 아니므로 movie->play();에서 뭔가 끔찍한 일이 발생할 것입니다. 이는 메타테이블과는(따라서 윗값이냐 루아 레지스트리냐와는) 무관하고, 그냥 play_movie 자체가 오류 점검을 소홀히 하기 때문에 생기는 문제입니다.

(제정신인 개발자라면 movie.play(music) 같은 엉뚱한 코드를 작성하지 않을 거라고 생각할 수도 있지만, 위의 예는 단순화된 것일 뿐입니다. 예를 들어 명령 대기열을 이용한 지연 실행 기능 같은 것을 구현한다고 하면, 사소한 실수 때문에도 movie.play(music) 같은 코드가 실행될 수 있습니다.)

이런 오류를 피하려면 주어진 인수가 우리가 원하는 종류의 사용자자료인지 확인해야 하고, 그러려면 애초에 그 사용자자료에 식별용 정보를 연관시켜야 합니다(사용자자료 자체는 결국 void*이므로 형식에 관한 정보를 얻을 수 없습니다). 문제의 근원은 Movie*가 아닌 void 포인터에 대해 static_cast<Movie*>를 적용하는 것이므로, Movie 객체 자체에 식별용 정보를 둘 수는 없습니다. 식별용 정보를 담을만한 곳으로 제가 떠올릴 수 있는 것은 사용자자료에 설정된 메타테이블 뿐입니다. 이해하기 쉽게 루아로 표현하면:

-- movie_lib.lua --

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

function meta:play()
    if getmetatalbe(self)
        and getmetatalbe(self).ID == "MT for Movie object"
    then
        return video_engine.play(self.handle)
    else
        -- .. 오류 처리 ..
    end
end

허무하게도, "MT for Movie object"이라는 식별용 문자열이 다시 등장했습니다. 문자열 대신 이를테면 정수 값을 사용한다고 해서 근본적으로 달라지지는 않습니다. 그리고 문자열이든 정수 값이든 충분히 길고 복잡한 값을 사용한다면 충돌 가능성을 아주 낮출 수 있다는 말은 별로 위안이 되지 않습니다(그럴 거면 애초에 그냥 luaL_newmetatable 기반 기법을 사용하면 그만이니까요...). 안타깝게도, 루아 C API 차원에서 뭔가 특별한 수단을 제공하지 않는 한 궁극의 해결책은 없는 것 같습니다. 혹시 이름충돌의 뭔가 돌파구를 발견하신 분이 있으면 꼭 알려 주세요!

이 시리즈는 이걸로 마무리합니다. 궁극의 해결책을 찾지 못해서 아쉽지만, 윗값을 루아 글루 함수 간 정보 전달 수단으로 사용하는 유용한 기법을 발견한 걸로 위안을 삼습니다.

태그: 프로그래밍 Lua

comments powered by Disqus