Встраивание Lua для поддержки скриптов в приложениях

Реализация поддержки скриптов в приложениях при помощи небольшого специализированного языка

Lua — небольшой скриптовой язык программирования, специально разработанный для встраивания в другие программы. Прикладной программный интерфейс Lua для C позволяет создавать очень простой и понятный код как для вызова Lua из C, так и для вызова C из Lua. Это позволяет разработчикам, которым нужен удобный скриптовой язык, действующий во время выполнения, просто реализовывать необходимые для языка основные элементы программного интерфейса, после чего вызывать из своих приложений код на Lua. В этой статье Lua рассматривается как средство упрощения распространенных задач разработки, а также обсуждаются некоторые причины для встраивания скриптового языка на начальном этапе работы.

Питер Сибах, автор, plethora.net

Питер Сибах (Peter Seebach) работает с компьютерами много лет и постепенно приспособился. Хотя он до сих пор не понимает, почему мышку надо чистить так часто.



23.04.2009

Lua — маленький скриптовой язык. Насколько маленький? Например, в нем вместо регулярных выражений POSIX используются особые механизмы поиска по шаблону, так как полная реализация регулярных выражений значительно превышает все его вместе взятые стандартные библиотеки. Более простой поиск в строках, предоставляемый Lua, хотя и не такой мощный, но занимает значительно меньше места.

Переменные в Lua не являются строго типизированными — вы можете узнать тип значения, но ничего не препятствует изменению типа у переменной. Оба этих принципа хорошо подходят для скриптового языка. Система типов Lua довольно простая, но гибкая. Обычные и ассоциативные массивы составляют один тип, называемый таблицами. Базовыми типами являются строки, числа (только с плавающей точкой), булевы значения, а также специальный тип nil. Пожалуй, более интересно то, что функции также являются базовым типом. Вы можете присваивать функции переменным так же, как и любые другие типы, для этого не требуется специальный синтаксис. Имеется дополнительная поддержка специальных объектов userdata, которые разработчики могут определять для работы с типами, выходящими за рамки базовой системы.

Один из самых неожиданных моментов для программистов, ранее работавших с другими языками — в Lua ложными значениями считаются только false и nil; любое значение небулевого типа в условных выражениях всегда считается истинным. Хотя этот подход может удивить людей, привыкших к соглашениям C, таким как использование 1 и 0 вместо true и false, к нему легко привыкнуть.

Lua написана на переносимом C; также его можно использовать с C++. Основа языка очень хорошо переносима; хотя некоторые возможности и требуют поддержки со стороны платформы, Lua хорошо работает без платформенных зависимостей. У этого языка нет огромного набора тестов Autoconf, так как он следует стандарту. Lua распространяется на условиях лицензии MIT и может бесплатно использоваться для любых целей, в том числе коммерческих. (Вероятно, это послужило причиной того, что многие программисты встраивают этот язык в свои приложения.)

Зачем встраивать язык?

Встраивание скриптового языка дает ряд преимуществ. Я приведу пример, с которого начал работать с Lua: это многопользовательская ролевая онлайн-игра World of Warcraft (WoW) от компании Blizzard. Пользовательский интерфейс WoW полностью реализован на Lua; разработчики предоставили несколько базовых вызовов прикладного программного интерфейса для непосредственного взаимодействия с движком отрисовки и запроса данных о мире, а затем использовали Lua для основной части кода пользовательского интерфейса.

Это значительно упрощает отделение кода пользовательского интерфейса от внутренностей игры, повышает безопасность и читаемость. В свою очередь, Blizzard в результате получила возможность открыть пользовательский интерфейс для игроков и позволить им писать свой код для изменения процесса взаимодействия с игрой.

В целом во многих задачах со скриптовыми языками работать проще, нежели с низкоуровневыми языками. Язык со сборкой мусора, неявным выделением памяти и ассоциативными массивами часто дает более простой код, который разрабатывается быстрее. Он может и не работать быстрее, но во многих случаях это не проблема; так, пользовательские интерфейсы лишь должны работать быстрее, чем пользователь вводит информацию с помощью клавиатуры или мыши.

Есть несколько способов использования скриптовых языков. Первый и самый простой — использовать язык для управляющей логики, считая код на C деталями реализации программы, которая на самом деле написана на Lua. Второй способ — писать программу в основном на C, после чего встроить Lua как механизм хранения и передачи данных и настроек. Третий и самый гибкий способ — совмещать предыдущие два, используя для скриптования определенных действий Lua, а для работы с другими — код на C. Простой интерфейс между Lua и C способствует использованию такого подхода.


Написание движка

Создание движка для программы, которая будет в основном писаться на Lua, имеет смысл в том случае, когда основное использование процессорного времени сосредоточено в отдельных операциях, а высокоуровневое управление относительно легковесно. Это помогает отделить детали реализации от высокоуровневой конструкции. Реализация основной логики программы на Lua, а не на C, может очень значительно уменьшить время разработки.

При таком подходе к проектированию основная часть интерфейса между Lua и C состоит из определения функций C для вызова из Lua, и ожидается, что после начала выполнения всё последующее использование кода на C будет сводиться к вызовам из скрипта.


Скриптуемые файлы настроек

Каждый знакомый мне программист написал хотя бы один блок кода, который занимается только тем, что пытается сохранить настройки в файле и позднее их восстановить. (Пожалуй, из того, что я использовал, мне больше всего нравятся списки свойств, разработанные Apple.) Однако в качестве формата таких файлов может использоваться встроенный скриптовой язык, что даст пользователям прекрасный набор возможностей для настройки. Так, оконный менеджер Ion использует Lua для своих файлов, позволяя пользователям создавать мощные и гибкие настройки.

У пользователей появляется превосходная возможность не ограничиваться простыми присваиваниями значений: файлы настроек на Lua могут включать комментарии, условные выражения и другие конструкции. Можно предоставить ограниченный прикладной программный интерфейс для получения данных, которые могли бы повлиять на выбор настроек.


Сочетание подходов

Вполне возможно переходить от Lua к C и обратно, так как интерпретатор Lua является реентерабельным. Ваша программа на C может вызывать для скрипта интерпретатор Lua, а скрипт в свою очередь вызывать функцию C, также использующую интерпретатор.

Для пользовательского интерфейса World of Warcraft в основном используется именно такая модель: вызовы Lua в пользовательском интерфейсе могут совершать обратные вызовы движка, а движок доставляет события коду пользовательского интерфейса, написанному на Lua. В результате получается гибкий интерфейс с хорошим разделением и безопасностью, что дает пользователям широкие возможности при небольшом риске вызвать выход за границы буфера в приложении или аварийное завершение работы программы. В прикладных программных интерфейсах, основанных на C, встроенный код практически всегда будет иметь возможность создания аварийной ситуации; если же пользовательский код может вызвать сбой при использовании интерфейса на Lua, это означает, что в программе имеется ошибка, которую нужно исправить.


Сборка и использование Lua

Сборка Lua происходит просто, достаточно выполнить make <имя_платформы>; если нет желания использовать специфические возможности платформы, то можно указать posix или даже ansi После сборки получаем библиотеку liblua.a, которую можно связывать со своими программами. Поздравляю, Lua встроен! Конечно, практическое использование языка требует немного больше работы.

Реентерабельность Lua обеспечивается тем, что состояние интерпретатора сохраняется в объекте; можно иметь несколько интерпретаторов, которые не используют переменные совместно, и у них не будет общих глобальных сущностей. Для взаимодействия с интерпретатором нужно сначала создать состояние Lua:

lua_State *l;
l = lua_open();

Если вызов lua_open() происходит неудачно, возвращается нулевой указатель. В остальных случаях мы получаем пригодное для использования состояние интерпретатора Lua. Конечно, без библиотек от него мало проку. Добавить стандартные библиотеки можно при помощи функции luaL_openlibs():

luaL_openlibs(l);

Теперь состояние Lua готово к исполнению кода. Вот пример цикла программы, просто выполняющей свои аргументы как код на Lua:

Листинг 1. Выполнение аргументов в качестве кода на Lua
  for (i = 1; i < argc; ++i) {
    if (luaL_loadbuffer(l, argv[i], strlen(argv[i]), "argument")) {
      fprintf(stderr, "lua couldn't parse '%s': %s.\n",
		 		 argv[i], lua_tostring(l, -1));
      lua_pop(l, 1);
    } else {
      if (lua_pcall(l, 0, 1, 0)) {
        fprintf(stderr, "lua couldn't execute '%s': %s.\n",
		 		 argv[i], lua_tostring(l, -1));
        lua_pop(l, 1);
      } else {
        lua_pop(l, lua_gettop(l));
      } 
    }
  }

Функция luaL_loadbuffer() компилирует скрипт в код Lua; если имеется синтаксическая ошибка, то сообщение о ней возвращается в стек. Иначе скомпилированный код может исполняться при помощи функции lua_pcall(). Опять же, если возникают ошибки, то они возвращаются в стек. Заметьте, что каждый вызов интерпретатора Lua на языке C, либо относящийся к интерпретатору, принимает в качестве аргумента состояние Lua; состояния по умолчанию нет.

Как работает стек Lua

Интерпретатор Lua использует для взаимодействия с вызывающим кодом стековый интерфейс. Передаваемые коду на Lua данные помещаются в стек кодом на C; ответы интерпретатора Lua также помещаются в стек. Если функции luaL_loadbuffer() передан неправильный код, то в стек помещается сообщение об ошибке.

Элементы стека имеют типы и значения. Функция lua_type() запрашивает тип объекта, а функции lua_to<type>() (например, lua_tostring()) дают значения, приведенные к определенному типу C. Код, написанный на Lua, всегда строго подчиняется модели стека. Однако код на C может просматривать остальную часть стека и даже вставлять туда значения.

Это простой, но удивительно мощный интерфейс. Работа с кодом для исполнения ведется таким же образом: он просто помещается в стек для выполнения функцией lua_pcall().

Использование lua_pcall()

Функция lua_pcall() принимает три аргумента помимо состояния Lua, с которым работает. Код для выполнения не входит в эти аргументы, он помещается в стек при помощи luaL_loadbuffer() либо другой функции, получающей код. Вместо этого lua_pcall() принимает количество аргументов в стеке для передачи выполняемому коду, количество ожидаемых результатов и необязательный обработчик ошибок. Чтобы вызвать функцию, вы помещаете в стек сначала ее, а затем принимаемые аргументы (по порядку). Возвращенные значения помещаются в том же порядке; первое значение лежит в нижней части стека, а последнее — наверху.

И при отправке аргументов, и при получении возвращенных значений Lua без предупреждений корректирует количество значений таким образом, чтобы оно соответствовало числам, переданным lua_pcall(): если полученных значений недостаточно, то остальные заполняются значениями nil; если получены лишние значения, то они просто отбрасываются. (Точно так же в Lua работает множественное присваивание.)

Обработчиком ошибок, если таковой указан, служит индекс в стеке с кодом Lua для обработки любых возникающих ошибок. В этой обзорной статье мы опустим подробное обсуждение обработки ошибок; для начала важно знать, что обработка ошибок есть и что она реализуется на Lua. Это очень удобно.


Встраивание C в Lua

Писать функции на C для последующего использования в Lua очень просто. Вы будете этим удивлены, если когда-либо писали код для встраивания другого скриптового языка. Вот функция на C, которая принимает число x и возвращает x + 1:

Листинг 2. Функция на C для использования в Lua
int
l_ink(lua_State *L) {
        int x;
        if (lua_gettop(L) >= 0) {
                x = (int) lua_tonumber(L, -1);
                lua_pushnumber(L, x + 1);
        }
        return 1;
}

Функция вызывается с состоянием Lua в качестве аргумента; всё взаимодействие между C и Lua опять производится через стек состояния Lua. Возвращаемое значение — количество объектов, помещенных функцией в стек. Чтобы сделать эту функцию доступной в Lua, вы должны создать представляющий ее объект Lua и присвоить ей имя:

lua_pushcfunction(L, l_ink);
lua_setglobal(L, "ink");

Функция lua_pushcfunction() используется для преобразования указателя на функцию C во внутренний объект Lua. Этот объект, разумеется, помещается в стек. Далее функция lua_setglobal() присваивает верхнее значение стека именованной глобальной переменной. Так как функции в Lua являются просто значениями, при этом создается доступная для вызова функция.

Стековый интерфейс значительно всё упрощает: нет необходимости определять или объявлять аргументы, которые принимает функция. Это не дало бы ничего, так как Lua очень гибко подходит к вызову кода. Однако при желании вы можете более аккуратно проверить некоторые моменты. Функция luaL_checknumber() также может использоваться для проверки аргументов, вывода информативного сообщения об ошибке и прекращения выполнения функции.


Правильное использование встроенного скриптования

Простота встраивания Lua в код на других языках (в особенности на C) делает его простым средством для значительного улучшения функциональности программ на других языках. Это доступная альтернатива созданию собственного языка настроек или написанию своего анализатора выражений.

Некоторые вещи могут удивить людей, привыкших работать с более крупными скриптовыми языками. Во-первых, затраты на установку интерпретатора Lua очень малы; если вы хотите запустить что-то в песочнице, то это делается без проблем. У Lua есть много возможностей для обеспечения безопасности, которых мы не касались, но стоит отметить, что эффективное исполнение в изоляции от среды возможно даже в рамках одного состояния Lua.

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

Ресурсы

Научиться

Получить продукты и технологии

  • Ознакомительные версии ПО IBM: используйте в вашем следующем проекте программное обеспечение, которое можно загрузить непосредственно с сайта developerWorks. (EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=385159
ArticleTitle=Встраивание Lua для поддержки скриптов в приложениях
publish-date=04232009