에러 처리는 중요하다. 아무리 강조해도 지나치지 않다. 인하우스 툴 만들기의 첫번째 항목 "실수에 대해 적절한 피드백을 주어라."
이 글은 http://www.lua.org/manual/5.1/manual.html 에 있는 내용 일부를 이해하기 좋게 정리한 것이다. 실제 C++ 코드는 zupet님이 만드신 LuaTinker 소스를 참고하면 좋다. Programming in Lua 책도 사도록 하자.
로드 시간 에러 처리
루아는 로드 시간과 실행 시간이 명확히 구분된다. 문법 실수는 마치 컴파일이 실패하듯이 로드 시간에 전부 잡힌다.
lua_load와 LuaL_load{.*}가 로드 시간을 담당하는데, 이 C API들은 루아 런타임 에러를 발생시키지 않는다. 리턴값을 통해 문법 에러가 있었는지 알 수 있고, 에러가 있었을 경우 루아 스택에서 에러 메세지를 읽으면 된다.
로드가 성공적으로 끝나면 루아 파일이 통째로 하나의 함수가 되어 스택에 푸시된다. 이것을 pcall로 실행하면 되는데, 여기부터는 실행 시간이다.
luaL_do{file|string}은 간편해 보이지만 에러 핸들러를 설정할 수 없으니 쓰지 말자.
http://www.lua.org/manual/5.1/manual.html#lua_load 참고.
실행 시간 에러 처리
try-catch 와 비슷하다. try 블럭에 해당하는 것이 lua_pcall 함수 호출이고, catch 블럭에 해당하는 것이 lua_pcall에 등록한 에러 핸들러다. 루아 코드 수행 중에 에러가 발생하면 마치 C++에서 예외를 던지듯이 콜스택에서 가장 가까운 에러 핸들러를 호출한다. 핸들러 호출이 끝나면 에러가 발생한 이후의 루아 코드를 더이상 수행하지 않고 lua_pcall이 즉시 리턴한다.
lua_pcall의 마지막 인자로 에러 핸들러를 지정하지 않아도 에러 메세지는 얻을 수 있지만 콜스택 등의 추가 정보는 얻을 수 없다. lua_pcall이 리턴하는 시점에는 콜스택이 다 정리되어 있기 때문이다. 반드시 에러 핸들러를 만들어 넣어야 한다.
http://www.lua.org/manual/5.1/manual.html#lua_pcall 참고.
루아 코드 안에서 발생한 에러를 C++에서 설정한 기본 에러 핸들러로 전파시키지 않고 직접 처리하고 싶을 때가 있다. 이런 경우 루아 함수 pcall을 쓰면 된다. 단, 에러 핸들러를 설정할 수는 없다.
http://www.lua.org/manual/5.1/manual.html#pdf-pcall 참고.
코루틴 실행은 coroutine.resume 을 통해 일어나는데, pcall처럼 에러 핸들러를 설정할 수가 없고 리턴값을 통해 에러 메세지를 얻어야 한다. 다만 에러가 일어난 뒤에도 코루틴의 스택은 되감기지 않는다. coroutine.resume이 에러와 함께 리턴된 뒤에도 debug.traceback의 인자로 코루틴 핸들을 넣으면 콜스택도 얻을 수 있고 콜스택 속의 지역변수들에도 접근할 수 있다.
http://www.lua.org/manual/5.1/manual.html#pdf-coroutine.resume 참고.
에러 핸들러에서 할 일
debug.traceback으로 콜스택을 얻어서 개발자에게 알려주고, 필요한 경우 콜스택 내용을 덤프 서버로 전송하거나 인터랙티브 디버거를 띄울 수 있다.
에러 핸들러는 평범한 루아 함수라고 생각해도 된다. lua_pcall 에 집어넣기에 더 편하고 빠르기 때문에 에러 핸들러를 루아 함수보다는 C++ 함수로 만들게 되겠지만, 에러 핸들러 안에서 다른 루아 함수를 호출하는 것도 가능하다. 게다가 루아 디버깅 관련 기능에는 (다른 몇몇 용도처럼) C API로 직접 만드는 것보다는 루아 코드로 짜는 것이 훨씬 편한 것들이 있기 때문에, 에러 처리 과정으로 진입하는 것만 C++ 함수로 만들고 실제 복잡한 일들은 루아 코드를 통해 해도 된다.
인터랙티브 디버거는..... 사실 루아에 기본적으로 들어 있다. 에러 핸들러에서 debug.debug() 를 호출하면 lua.exe의 프롬프트 비스무리한 것이 뜬다. 여기서 루아 코드를 직접 쳐서 실행할 수 있다. 즉, 전역변수 내용을 들여다보거나 함수를 직접 실행할 수 있다. 다만 지역변수 내용에 접근하는 기능이 없는데, 이것은 debug 패키지에 들어있는 함수들을 이용하면 하루면 만들 수 있다. 나는 '콜스택을 찍는' 함수, '임의의 루아 값을 사람이 읽을 수 있는 포맷으로 표시해 주는' 함수, 그리고 '콜스택 레벨 k에 있는 지역변수들을 모두 모아 하나의 테이블로 만들어주는' 함수 세 개를 만들어서 인터랙티브 디버거에서 호출할 수 있도록 했다. 이 정도면 사용하기에 좀 귀찮기는 하지만 watch 기능으로는 충분히 쓸만하다.
게임클라이언트는 콘솔을 가지지 않으므로 인터랙티브 디버거를 쓸 수가 없다. 대신에 에러가 발생했을 때 AllocConsole로 새 콘솔을 만들고, SetStdHandle을 호출해서 stdin/stdout/stderr를 새로 만든 콘솔로 연결한 뒤 인터랙티브 디버거로 진입하면 잘 된다.
debug.sethook을 쓰면 breakpoint/step도 만들 수 있는데, 아직 못했다. 소스코드 위치를 표시해주는 GUI 디버거가 있어야 의미있을 것 같다. 커스텀 디버거를 통합할 수 있는 에디터가 있으면 좋겠다. 가벼운 걸로.
http://www.lua.org/manual/5.1/manual.html#5.9 참고.
게임클라이언트는 보통 매 틱 업데이트 함수를 호출하기 때문에, 업데이트 과정에 루아 함수 호출이 포함되어 있다면 한번 에러가 난 뒤에도 매 틱 계속 에러가 발생하는 경향이 있다. 현재 VM에서 에러가 났는지를 기억하는 플래그를 두고, 에러가 한번 발생했으면 그 이후부터는 VM이 리셋되기 전까지는 에러를 무시하게 해주면 편하다.