Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Пишем отладчик для Lua 5.1

Пишем отладчик для Lua 5.1

Автор:

В современных играх достаточно многое отдается на откуп скриптам. Самым распространенным и часто используемым для этого скриптовым языком является Lua . Удобный синтаксис, хорошая скорость (особенно при использовании LuaJIT), кроссплатформенность. Lua используется в проектах любых масштабов от небольших аркад, до монстров вроде World Of Warcraft.

Введение
Breakpoints
Call stack
Watch
Послесловие

Введение

При написании небольших скриптов не возникает проблемы отладки. Запись в лог, встроенная проверка синтаксиса — этого достаточно чтобы найти ошибку в коде из сотни строк. Но проекты разрастаются, все больше отдается на откуп скриптам. В больших проектах, с активным использованием скриптов, без нормального отладчика не обойтись.

Как это ни странно, но инструментов для отладки практически нет. Мной было найдено два более менее адекватных внешних отладчика: Decoda (создает впечатление удобного инструмента, однако платный) и LuaEdit (opensource, до сих пор не релиз и не факт что будет). RemDebug – консольный отладчик(последнее обновление — 2006 год). Из встраиваемых библиотек — ldb, не поддерживаемый с 2000 года...

На фоне такого раздолья не остается выбора, кроме как писать свой велосипед. Информации по написанию отладчика практически нет, пришлось копать документацию и собирать по крупинкам. Гугл стабильно предлагает для отладки использовать print...

В этой статье я рассмотрю реализацию трех необходимых для отладки задач: breakpoints, watch, callstack

Примеры в статье написаны на паскале, так как работа со строками в нем проще и нагляднее чем в C++. Никакого труда не составит перевести код на С++ или любой другой язык.

Breakpoints

Для реализации breakpoints в Lua есть свой инстумент Hooks. Однако я отказался от его использования по двум причинам:
1) Так и не удалось заставить его работать... Вроде бы все просто: вызов lua_sethook с MASKLINE, обработка hook функции... но сколько не пытался, hook ловит любые события, кроме HOOKLINE. *
2) Тысячи строк кода, отладка маленькой функции, 10 брейкпоинтов... и хук на каждую выполняему строчку... это явно не то что нам нужно.

Сделаем по аналогии с int 3. При загрузке скриптов в нужных нам местах вставляем вызов нашей функции debug_breakpoint(<id>); где id – номер брейкпоинта в нашем внутреннем списке.

breakpoints | Пишем отладчик для Lua 5.1

В отличии от «нормальных» дебагеров у нас получается два типа брейкпоинтов. Обычный и фоновые.

Обычный брейкпоинт ничем не отличается от всем привычных брейкпонитов в классических отладчиках. Фоновый — это для трассировки, так как у нас нет возможности сделать нормальную трассировку. Также фоновый мы можем в реальном времени заменить на обычный без перезагрузки скриптов.

debug_breakpoint в случае, если активен брейкпоинт с номер id или сейчас идет трассировка — выполняет нужные операции: заполнение call stack, обновление watch переменных, остановка всех процессов в движке, кроме обработки окна дебаггера.

Call stack

Логика получения списка вызовов достаточно простая: нужно просмотреть стек и вычленить из него функции. В библиотеке debug есть готовая функция debug.traceback(), но она доступна только в скриптах, по неизвестной причине в LuaAPI ее не вынесли. Конечно, можно вызвать Lua функцию и забрать результат, но это не похоже на хорошее решение.

Хотя traceback и не реализован на уровне LuaAPI, его реализацию можно сделать самостоятельно с помощью других функций LuaAPI.

С помощью функци lua_getstack в цикле получаем информацию об уровне выполнения начиная с 1 (вообще, стек индексируется с 0, но 0 — это debug_breakpoint, информацию о нем нам получать не нужно) и до тех пор, пока lua_getstack не вернет 0 (до тех пор, пока стек не кончился lua_getstack будет возвращать 1).

От lua_getstack нам нужно заполнение структуры lua_Debug, которую мы передадим в lua_getinfo. Параметр what должен содержать символы Sn. S – для заполнения информации по исходному коду, n – для заполнения информации об именах.

Код функции будет примерно таким:

procedure luaTraceback(LuaState:Pointer; List:TCallStackList);
var
  Level:integer;
  ar:lua_Debug;
  Item:TCallStackListItem;
begin
  Level := 1;
  while lua_getstack(LuaState,Level,@ar)=1 do begin
    lua_getinfo(LuaState,'Sn',@ar);

    if ar.name<>nil then
      Item.FunctionName:=ar.name
    else
      Item.FunctionName:='unknown';
    if ar.namewhat<>nil then
      Item.FunctionType:=ar.namewhat
    else
      Item.FunctionType:='unknown';
    Item.RunType:=ar.what;
    Item.ChunkName:=ar.source;

    List.Add(Item);

    inc(Level);
  end;
end;

lua_Debug.name – это имя функции. Если определить имя функции невозможно — nil
lua_Debug.namewhat – это где функция определена — метод таблицы, поле таблицы, в корне и т.п.
lua_Debug.what – это среда исполнения. Может быть 'Lua' или 'C'
lua_Debug.source – это место, где определен исходный код. Если код загружен из файла, то это имя файла, если из памяти — то имя чанка.

Watch

Реализация слежения за переменными достаточно простая, никаких костылей придумывать не приходится, весь нужный инструментарий предоставляет LuaAPI. Думаю, что с задачей распарсить на части строку вида self.Objects[0].Caption, вы справитесь сами.

В Watch мы хотим иметь доступ к глобальным переменным, аргументам функции и локальным переменным. Аргументы функции относятся к локальным переменным, так что обрабатываются они вместе.

Как и при обычной работе с переменными наша задача поместить переменную на вершину стека из нее получить новую переменную и поместить ее на вершину стека и так пока не дойдем до последней переменной в выражении.

С глобальными переменными все просто — работаем как и всегда. А вот с локальными сложнее. Дело в том, что мы зашли в брейкпоинт с помощью функции debug_breakpoint, а значит в стеке лежат переменные принадлежащие этой функции. А отлаживаемая функция находится на стеке на уровень выше и обычными средствами недоступна. К счастью LuaAPI предоставляет доступ к любому уровня на стеке с помощью уже известной функции lua_getstack.

Тогда функция для вынесения локальной переменной на вершину стека будет выглядеть так:

Function luaGetLocalVariable(LuaState:Pointer; const Name:string):boolean;
var
  ar:lua_Debug;
  VarName:PChar;
  current:integer;
begin
  Result:=false;
  if lua_getstack(LuaState,1,@ar)<>1 then begin
    //Вообще все сломалось почему-то...
    //если getstack не сработал, творится что-то очень плохое
    Exit;
  end;

  current:=1;
  VarName:=lua_getlocal(LuaState,@ar,current);
  while VarName<>nil do begin
    if VarName=Name then begin
      Result:=true; //Получили нужную переменную на вершине стека,
                    //возвращаем true и выходим
      Exit;
    end;
    lua_pop(LuaState,1); //Это не та переменная, которая нам нужна.
                         //Убираем ее с вершины стека
    VarName:=lua_getlocal(LuaState,@ar,current); 
    inc(current);
  end;
end;

Послесловие

Что получилось у меня после использования всего выше описанного — вы можете увидеть в видео расположенном ниже.
Любые вопросы, дополнения и исправления вы можете писать в комментариях к статье.

* - как выяснилось позднее хуки на строки не ставились из-за использования LuaJIT. Если использовать для отладки обычную Lua библиотеку - хуки работают в штатном режиме. Имеет смысл делать хуки стандартным способом, скорость работы существенно падает, но зато растет эффективность отладки.

27 апреля 2011

#Debugger, #Lua, #отладка, #скрипты


Обновление: 26 октября 2011

2001—2017 © GameDev.ru — Разработка игр