Скриптовый язык LUA LUA (
www.lua.org) — ОДИН ИЗ СКРИПТОВЫХ ЯЗЫКОВ, ИСПОЛЬЗУЕМЫХ В ИГРАХ ЧАЩЕ ВСЕГО. ЭТОТ ЯЗЫК ПОПУЛЯРЕН НАСТОЛЬКО В ОСНОВНОМ БЛАГОДАРЯ ПРОДУМАННОМУ ДИЗАЙНУ И РЕАЛИЗАЦИИ ЯЗЫКА.
Первая версия языка Lua была создана в 1993 году сотрудниками научно-исследовательской группы по технологиям компьютерной графики (Computer Graphics Technology Group, TeCGraf) Бразильского папского католического университета (Pontifical Catholic University, PUC-Rio) в Рио-де-Жанейро. Над языком работали три человека: Роберто Иерусалимский (Roberto Ierusalimschy), Луиш Энрике де Фигуиредо (Luiz Henrique de Figueiredo) и Вальдемар Челеш (Waldemar Celes). Название языка на португальском означает «луна» и произносится как «луа» (LOO-ah). Истории развития языка посвящена специальная страница его официального сайта —
www.lua.org/versions.html. В 2003 году была выпущена версия языка 5.0.2, последняя на момент написания статьи (следующая версия 5.1 находится на стадии бета-тестирования) — Lua обрел зрелость и стабильность. Его интерпретатор, реализованный на основе регистровой (register-based) виртуальной машины, очень эффективен и по производительности, и по объему занимаемой памяти. В сборке со всеми стандартными библиотеками языка он добавляет к исполняемому файлу программы всего порядка 150 Кб (100 Кб без библиотек).
Интерпретатор Lua написан на «чистом» (clean) C (подмножестве языков ANSI C и C++), его можно собрать на любой платформе, для которой существует компилятор, поддерживающий стандарт ANSI C. Высокая портируемость (portability) языка обеспечена!
Изначально Lua был задуман как язык расширения функциональности приложений (extension language), и поэтому он обладает достаточно удобным в этом плане интерфейсом (так называемый Lua C API).
Гибкий синтаксис
Lua обладает несложным синтаксисом, близким к традиционному и несколько напоминающим Pascal. Часто элементы синтаксиса носят необязательный характер, что позволяет писать на Lua и в C-подобном стиле, и в стиле, который при определенном старании можно приблизить к некоему подобию формализованного человеческого языка благодаря отсутствию избыточных синтаксических конструкций.
Традиционный пример Hello world на Lua состоит из одной строчки:
print(“Hello, world!”);
Синтаксис языка позволяет при желании опустить скобки и точку с запятой. Таким образом, вот эта конструкция эквивалентна первой:
print “Hello, world”
Обычно сердцу программиста милее конструкция со скобками: ее легче воспринимает взгляд, натренированный работой с C-подобными языками. Однако язык Lua удобно использовать в качестве языка, специализированного под конкретную задачу (Domain-Specific Languages, DSL), например как язык описательного характера, такой как «язык» конфигурационного файла программы. Свобода в синтаксисе Lua бывает очень полезна для повышения выразительности подобных языков.
В Lua также удобно хранить табличные данные, причем их можно экспортировать непосредственно из Excel, написав маленький скрипт-кодогенератор на Visual Basic. Значит, больше не нужен лишний парсер данных (например, в формате CSV) — парсер и интерпретатор Lua сделают все за тебя.
На полную мощность
Часто при разработке архитектуры скриптовой системы и при непосредственной разработке на Lua программисты допускают ошибку — пытаются писать на этом языке так, как будто он некое бледное подобие C++. Lua — мультипарадигменный язык, гибкий в той степени, чтобы допускать разработку в стиле C++. Однако удобнее и эффективнее рассматривать его как самостоятельную сущность и вести разработку исходя из всего богатства возможностей, в том числе работать с возможностями, которых или нет в C++, или использование которых затруднительно.
Функции в Lua — значения первого класса. В Lua есть замыкания (closures). Функция может возвращать список значений... Если ты помнишь обо всем этом, то значительно облегчишь свой труд по разработке проекта. Подход с использованием корутин (coroutines) дает хорошее преимущество в объеме и прозрачности кода (если сравнивать с классическими системами взаимодействия с движком, основанными исключительно на обратных вызовах (callbacks) и/или событиях (events). Благодаря встроенным в язык возможностям рефлексии (например, функции type и функциям работы с таблицами), на Lua очень удобно реализуются связанные с ней задачи, скажем сериализация (пример такой реализации есть в книге Programming In Lua). Когда в движке требуются рефлективные возможности (для сериализации или автоматической генерации интерфейса редактирования данных), не пытайся прикрутить рефлексию к C++, а используй возможности Lua.
Консоль игры
В поставку Lua, помимо исходников библиотеки для программной работы с интерпретатором (Lua C API), входят исходники интерактивного интерпретатора (lua.exe), очень удобного для изучения языка, быстрого тестирования и отладки небольших кусков кода. Пользуясь им, ты видишь результат выполнения написанного кода не отходя от кассы. Очень удобно реализовать функциональность, аналогичную той, которой обладает интерактивный интерпретатор Lua, в консоли игры. Тогда ты и твои дизайнеры смогут эффективно писать код в контексте твоего движка и немедленно просматривать то, как изменения в коде влияют на игровой мир.
Перенаправление вывода
Посмотрим такой пример. В Lua нужно переопределить функцию print, чтобы она выводила текст не в стандартный поток вывода, а в игровую консоль. Функция print в Lua является частью базовой библиотеки языка и принимает список аргументов переменной длины. Она преобразует каждый из аргументов в строку при помощи функции tostring() и выводит ее разделяя символами табуляции.
Функции в Lua — значения первого класса (first-class values), их можно присваивать переменным, передавать в качестве аргументов и возвращать из других функций. Благодаря этому переопределение любой функции в Lua — не больше чем присвоение переменной с именем этой функции нового значения. Если у тебя есть функция __console_print, которая принимает строку и выводит ее в консоль, то переопределение системной функции print в Lua будет выглядеть, например, вот так:
print = function(...)
for i = 0, arg.n -1 do
__console_print(tostring(arg[i]) .. "\t")
end
__console_print(arg[arg.n] .. "\n")
end
Если нужна высокая гибкость или производительность, ты можешь написать свою функцию __console_print на C/С++, которая работает непосредственно с виртуальным стеком интерпретатора, по образцу print из базовой библиотеки функций (luaB_print() в файле lbaselib.c исходников Lua). Фактически ты только продублируешь ее исходный код, заменив в нем вызов fputs() на вызов нужной функции. Соответственно, для замены стандартного print в Lua достаточно одной строчки:
print = __console_print
При необходимости аналогичным образом переопределяешь и другие стандартные функции, работающие с вводом-выводом.
Подключение игры к движку
Вместе с языком для подключения языка к твоей программе поставляется библиотека на C (Lua C API). Для взаимодействия с языком C интерпретатор Lua использует виртуальный стек. Когда из скрипта вызывается функция на C, она получает чистый экземпляр стека, который независим от стеков других функций, вызванных на данный момент, и содержит аргументы, переданные в данную функцию. Значения, возвращаемые функцией, тоже передаются через стек. Большинство функций Lua C API работают и с виртуальным стеком. Для удобства эти функции не следуют строгой стековой дисциплине, когда можно использовать только операции push и pop, но позволяют обращаться к значениям, содержащимся в стеке, по их абсолютной или относительной позиции. Подробности о работе с виртуальным стеком интерпретатора Lua расписаны в Lua Reference Manual.
Чаще всего больше удобств дает работа на более высоком уровне, который предоставляют различные библиотеки-обертки. Тем не менее полезно уметь работать с интерпретатором непосредственно на уровне Lua C API, хотя бы чтобы понять, что происходит на самом деле. К тому же для некоторых специфических задач бывает просто недостаточно функциональности, предоставляемой библиотеками-обертками.
Язык Lua специально создавался как язык расширения функциональности (extension programming language), поэтому достаточно легко организовать взаимодействие кода на Lua с кодом на C (и, следовательно, C++).
Помимо Lua C API, существует некоторое количество оберток для этой библиотеки, призванных повысить удобство работы с Lua из C и C++. Такие обертки делятся на две основные группы: 1) генерируют прослойку межъязыкового взаимодействия автоматически на основе данных, подготовленных специальным образом; 2) служат для облегчения ручного создания такой прослойки.
Несмотря на обилие оберток для подключения Lua, стоит изучить Lua C API — получишь самое полное представление о том, что происходит в программе. К тому же при всей своей мощи сам API достаточно небольшой. Необходимую информацию ищи в Lua Programming Manual и Programming With Lua.
На сегодня из библиотек для обеспечения межъязыкового взаимодействия C++ с Lua наиболее развиты:
- toLua, toLua++ (
www.tecgraf.puc-rio.br/~celes/tolua, www.codenix.com/~tolua); - SWIG (
www.swig.org); - Luabind (luabind.sourceforge.net).
И toLua, и SWIG генерируют код регистрации по специально подготовленным входным файлам (SWIG умеет генерировать такой код не только для Lua, но и для множества скриптовых языков).
Luabind использует шаблоны C++ для генерации кода регистрации твоих типов в Lua, поэтому он избавляет от промежуточного этапа генерации кода сторонними средствами за счет некоторого оверхеда в скорости компиляции и объемах конечного исполняемого файла. По моему опыту, удобнее всего пользоваться библиотекой Luabind в сочетании с прямым использованием Lua C API (чтобы реализовать отдельные тонкие моменты).
Вопросы производительности
При разработке программ на Lua нужно иметь в виду, что компилятор этого языка не обладает широкими способностями по оптимизации. С другой стороны, интерпретатор Lua реализован очень эффективно и чаще всего обеспечивает весьма приличную скорость работы. Если же тебе нужна сверхвысокая производительность, ты должен сам следить за тем, чтобы в коде не было множества лишних конструкций. Добиться этого будет легче, если будешь придерживаться нескольких простых правил при написании кода на Lua (подробнее о подходах к оптимизации кода на Lua — в материалах, опубликованных в Lua Us-ers Wiki).
Доступ к локальным переменным в Lua несколько быстрее, чем к глобальным. Если требуется интенсивный доступ к глобальной таблице, функции или корутине, лучше завести локальную, присвоить ей значение глобальной и только потом начинать использовать. Эта техника не несет накладных расходов на копирование, так как в Lua данные таких типов копируются как ссылки, а не как значения.
По возможности в коде, критичном по производительности, не создавай множество объектов Lua, управляемых сборщиком мусора. Такие объекты создаются при склеивании строк, вызове конструкторов таблиц (например при вызовах функций с переменным числом аргументов), при объявлении любых функций, при выполнении команд dofile/dostring.
Компилятор Lua способен выполнять элементарную оптимизацию. Например, он оптимизирует простые выражения с участием переменных и констант. Не обязательно заменять, скажем, выражение
a = 2 * 34567 + b
на
a = 69134 + b.
Перевод скриптового кода в код на C++ — один из действенных методов оптимизации. Если тебе вдруг показалось, что какой-то участок кода на Lua нужно подвергнуть интенсивной оптимизации, может быть, ты обнаружил первый сигнал к тому, что этот кусок кода нужно переписать на C++.
Заботясь о производительности кода, остерегайся преждевременной оптимизации. По возможности соблюдай рекомендации по написанию оптимального по производительности кода — писать гарантированно медленный код не стоит. Однако, как известно, затраты, скажем, на оптимизацию куска кода, который выполняется не более 1% времени, не уменьшат временные затраты на выполнение программы больше чем на 1%.
Сначала реализуй код самым удобным (по твоим ощущениям) способом. Если понадобится, собирай статистику с помощью таких инструментов, как Lua Profiler (
www.keplerproject.org/luaprofiler). И оптимизируй только то, что действительно должно быть оптимизировано.
Обработка ошибок
Важно помнить, что при небрежном отношении к обработке ошибок в Lua, после возврата ошибки из lua_pcall(), скриптовая система может оказаться в невалидном состоянии.
Безопаснее всего (по крайней мере, в стадии активной разработки) прекращать выполнение программы при возникновении ошибок в скрипте. Естественно, это не относится к консольному интерпретатору, в котором для комфортной работы нужно обеспечивать толерантность к возможным ошибкам и опечаткам. Тем не менее желательно принимать меры к тому, чтобы ошибки в коде, выполняемом с консоли, не приводили к неявной «порче» игрового мира.
- ПРИ ВОЗНИКНОВЕНИИ ОШИБКИ В ФУНКЦИЯХ НА LUA ВМЕСТО ВЫЗОВА БИБЛИОТЕЧНОЙ ФУНКЦИИ ERROR() ВОЗВРАЩАЙ NIL И ТЕКСТ СООБЩЕНИЯ ОБ ОШИБКЕ.
- ФУНКЦИИ, КОТОРЫЕ МОГУТ ВЫЗВАТЬ ERROR(), ВЫЗЫВАЙ (ГДЕ ВОЗМОЖНО) ЧЕРЕЗ БИБЛИОТЕЧНУЮ ФУНКЦИЮ PCALL(), ДЛЯ ЧЕГО ЛУЧШЕ ИСПОЛЬЗОВАТЬ ИДИОМУ PROTECT, ОПИСАННУЮ В СТАТЬЕ ДИЕГО НЕХАБА (DIEGO NEHAB) FINALIZED EXCEPTIONS (
www.lua-users.org/wiki/FinalizedExceptions). ТАКЖЕ ПОЛЕЗНО ПОЛЬЗОВАТЬСЯ ИДИОМОЙ NEWTRY (СМ. ТУ ЖЕ СТАТЬЮ).
- ПРОВЕРЯЙ ПЕРЕДАННЫЕ ФУНКЦИИ ПАРАМЕТРЫ НА NIL. ЕСЛИ, НАПРИМЕР, ФУНКЦИЯ, ОБЪЯВЛЕННАЯ С ТРЕМЯ ПАРАМЕТРАМИ, ВЫЗЫВАЕТСЯ С ДВУМЯ, ТО ТРЕТЬЕМУ ПАРАМЕТРУ БУДЕТ ПРИСВОЕН NIL.
- ЕСЛИ ТВОЯ ФУНКЦИЯ РАБОТАЕТ С ГЛОБАЛЬНЫМИ ДАННЫМИ, ОСОБЕННО С ОПИСАНИЯМИ УРОВНЕЙ, ТЕКСТАМИ ДИАЛОГОВ И Т.П., ПО ВОЗМОЖНОСТИ ПРОВЕРЯЙ ЭТИ ДАННЫЕ НА NIL.
- ОЧЕНЬ ЧАСТО ОШИБКИ ВОЗНИКАЮТ ИЗ-ЗА ТОГО, ЧТО ПРОГРАММИСТ ЗАБЫЛ НАПИСАТЬ КЛЮЧЕВОЕ СЛОВО LOCAL ПЕРЕД ПЕРВЫМ ПРИСВАИВАНИЕМ ЛОКАЛЬНОЙ ПЕРЕМЕННОЙ И ЗАТЕР ТАКИМ ОБРАЗОМ ГЛОБАЛЬНУЮ. СЛЕДИ ЗА ОБЛАСТЬЮ ВИДИМОСТИ ТВОИХ ПЕРЕМЕННЫХ. В НАЗВАНИЯХ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ ИСПОЛЬЗУЙ УНИКАЛЬНЫЙ ПРЕФИКС, НАПРИМЕР G_. ПРИМЕНЯЙ ИНСТРУМЕНТЫ ТИПА LUA LINT ДЛЯ ПРОВЕРКИ ИСПОЛЬЗОВАНИЯ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ. В ДИСТРИБУТИВ LUA 5.1 ВХОДИТ ПРИМЕР НА LUA, КОТОРЫЙ НАСТРАИВАЕТ МЕТАТАБЛИЦУ ТАБЛИЦЫ, СОДЕРЖАЩЕЙ ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ (“_G”), ТАКИМ ОБРАЗОМ, ЧТО ПРИ ДОБАВЛЕНИИ ИЛИ ЧТЕНИИ НЕЗАРЕГИСТРИРОВАННЫХ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ ВО ВРЕМЯ ВЫПОЛНЕНИЯ СКРИПТА ВЫДАЕТСЯ СООБЩЕНИЕ ОБ ОШИБКЕ.
- СТАРАЙСЯ МИНИМИЗИРОВАТЬ ИСПОЛЬЗОВАНИЕ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ. СОБЛЮДАЙ МОДУЛЬНОСТЬ КОДА. СЧИТАЕТСЯ ХОРОШИМ ТОНОМ, ЕСЛИ ОДИН МОДУЛЬ СОЗДАЕТ ТОЛЬКО ОДНУ ГЛОБАЛЬНУЮ ПЕРЕМЕННУЮ. ПОДРОБНЕЕ О ПОДХОДАХ К ОРГАНИЗАЦИИ МОДУЛЕЙ В LUA МОЖНО ПРОЧИТАТЬ В LUA TECHNICAL NOTE 7: MODULES & PACKAGES РОБЕРТО ИЕРУСАЛИМСКОГО (
www.lua.org/notes/ltn007.html). - ИЗБЕГАЙ СОЗДАВАТЬ НА СКРИПТОВОМ ЯЗЫКЕ СЛИШКОМ СЛОЖНЫЕ СИСТЕМЫ. ДИНАМИЧЕСКАЯ ТИПИЗАЦИЯ И ПРОЧИЕ ОСОБЕННОСТИ, ПРИ ВСЕМ СВОЕМ УДОБСТВЕ, ПЛОХО ВЛИЯЮТ НА СТАБИЛЬНОСТЬ, НАДЕЖНОСТЬ И ПРОИЗВОДИТЕЛЬНОСТЬ КОДА. СКРИПТОВЫЕ ЯЗЫКИ ВООБЩЕ И LUA В ЧАСТНОСТИ ХОРОШИ ДЛЯ «СКЛЕЙКИ» И НАСТРОЙКИ ФУНКЦИОНАЛЬНОСТИ ИГРОВОГО ДВИЖКА. ЕСЛИ ТЫ ВИДИШЬ, ЧТО КАКОЙ-ТО МОДУЛЬ НА LUA СТАНОВИТСЯ СЛИШКОМ СЛОЖНЫМ, ПОСТАРАЙСЯ ПЕРЕНЕСТИ ЕГО ФУНКЦИОНАЛЬНОСТЬ НА C++, ОСТАВИВ LUA ТОЛЬКО ВЫСОКОУРОВНЕВОЕ УПРАВЛЕНИЕ ЭТИМ МОДУЛЕМ.
Управление памятью
Язык Lua автоматически управляет памятью при помощи сборщика мусора (garbage collector): интерпретатор периодически вызывает сборщик мусора, удаляющий объекты, для которых была выделена память (таблицы, userdata, функции, потоки и строки) и которые стали недоступными из Lua («мертвые» объекты).
Интерпретатор Lua задает предел объема памяти (threshold), занимаемого данными. Как только занятый объем достигнет этого предела, запускается алгоритм, освобождающий память, занятую накопившимися «мертвыми» объектами. Затем устанавливается новый предел, равный двухкратному объему памяти, занимаемой после очистки.
Чтобы запустить сборку мусора немедленно, нужно программно установить предел занимаемой памяти в 0 (вызвав lua_setgcthreshold() из C или collectgar-bage() из Lua). Чтобы остановить сборку мусора, устанавливаем этот предел в достаточно большое значение.
Замечено, что в некоторых случаях память, занимаемая данными Lua, проявляет тенденцию к разрастанию, что может негативно сказаться на производительности программы. Чтобы избежать этого, лучше периодически вызывать сборку мусора принудительно.
При написании программ на Lua обязательно учитывай то, каким образом интерпретатор Lua управляет распределением памяти. Сборка мусора в версии 5.0.2 — относительно затратная по производительности операция (используется алгоритм non-incremental mark and sweep). Она инициируется автоматически, когда объем выделенной памяти превышает двукратный объем памяти, оставшейся выделенной после предыдущей сборки мусора (объем выделенной памяти, при превышении которого произойдет следующая сборка мусора, также можно задавать программно). Значит, фактически, в достаточно большой динамической системе сборка мусора может быть запущена в произвольный момент времени, вызвав просадку по производительности. Чем больше памяти выделено под объекты Lua, тем дольше происходит сбор мусора.
Итак, если ты, например, читаешь в Lua данные из файла построчно и склеиваешь их в одну строку линейно, скорее всего, процесс будет продвигаться страшно медленно: после каждой следующей операции склейки объем занятой памяти, как минимум, удваивается и, соответственно, запускается процесс сборки мусора.
Код создает новую строку (и присваивает ее переменной some_string), а строка, хранившаяся в some_string до склейки, остается в памяти до следующей сборки мусора:
some_string = some_string .. "a"
Такая проблема и способы ее решения описаны в статье Роберто Иеруса-лимского Lua Technical Note 9: Creating Strings Piece by Piece (
www.lua.org/notes/ltn009.html). В Lua 5.1 реализована инкрементальная сборка мусора, позволяющая распределить нагрузку по производительности от процесса сборки мусора во времени.