Журнал LinuxFormat - перейти на главную

LXF125:Lua

Материал из Linuxformat
Перейти к: навигация, поиск
Lua Язык программирования сценариев, встраиваемый в ваши приложения

Содержание

Lua: Встроим его в код

LUA
Часть 4: Вот уже четвертый номер мы пропагандируем Lua как хороший встраиваемый язык, и теперь пришла пора подтвердить слова делом. Андрей Боровский скрестит его с C и C++

Говорят, что когда Билл Гейтс и Пол Аллен писали первый интерпретатор Basic для «Альтаира», они не пользовались генераторами лексических и синтаксических анализаторов и формальными грамматиками, потому что не знали об их существовании. Тут можно усмотреть как проявление гениальности основателей Microsoft, так и отсутствие надлежащей квалификации. Как бы там ни было, современные программисты также могут наделять свои приложения встроенными интерпретаторами скриптовых языков, ничего не зная о принципах написания последних. Умные люди упростили решение этой задачи до такой степени, что теперь результатами их труда могут пользоваться все желающие. Остается только надеяться, что с годами избалованные программисты не станут глупее.

Все синтаксические красоты Lua вряд ли привлекли бы к нему внимание, если бы не главное: возможность встраивать интерпретатор в программы, написанные на C и других компилируемых языках. О том, что это дает разработчику, мы говорили в LXF122. С точки зрения программного интерфейса, объединение Lua и C позволяет решить две (как правило, связанные) задачи: управление программой Lua из приложения на C и вызов из программ Lua функций, написанных на C (в этом случае можно сказать, что программа Lua управляет C-кодом).

LXF125 72 1.jpg Игра Unknown Horizon — пример программы, использующей интерпретатор Lua.

Состояние и стек

Ядро интерпретатора Lua находится в библиотеке liblua. Программа-интерпретатор, которой мы пользовались до сих пор (файл lua5.1), представляет собой лишь тоненькую обертку вокруг нее. Чтобы наделить программу на C возможностью использовать язык Lua, нужно связать ее с liblua и задействовать экспортируемый библиотекой API. Прежде чем выполнять примеры из этой статьи, убедитесь, что файл liblua.so в вашей системе является ссылкой на библиотеку liblua.so.5.1. API C довольно заметно изменился при переходе от версии Lua 5.0 к 5.1, а в наших примерах мы будем использовать API последней версии.

Два наиболее важных понятия в Lua C API – это «состояние Lua» (Lua state) и стек. Первое есть структура, содержащая сведения об интерпретаторе, к которым можно получить доступ из функции и макросов Lua API. Адрес данной структуры является обязательным параметром для всех функций и макросов Lua C API (и всегда передается в качестве первого аргумента, так что дальше мы не будем каждый раз о нем упоминать). Для тех, кто регулярно имеет дело с различными программными интерфейсами в стиле C (скажем, GTK), структура, описывающая состояние Lua, не представляет собой ничего необычного – ее аналоги, содержащие информацию о состоянии программируемого элемента (окна, кодека, драйвера устройства и тому подобного), есть практически везде. Указатель на структуру, описывающую состояние Lua, можно рассматривать как дескриптор экземпляра интерпретатора Lua (только нужно помнить, что экземпляра как такового не существует).

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

Запутались? Все это не так сложно, как кажется. Рассмотрим пример программы на C, загружающий скрипт Lua (файл runlua.c на LXFDVD):

#include <lua.h>
#include <lauxlib.h>
#include <stdlib.h>
#include <stdio.h>
int main(void)
{
  int result;
  lua_State * L;
  L = luaL_newstate();
  luaL_openlibs(L);
  result = luaL_loadfile(L, “script.lua”);
  if (result) { 
  fprintf(stderr, “Ошибка загрузки: %s\n”, lua_tostring(L, -1));
  exit(1);
}
lua_pushnumber(L, 2);
lua_setglobal(L, “var”);
result = lua_pcall(L, 0, LUA_MULTRET, 0);
if (result) {
  fprintf(stderr, “Ошибка выполнения: %s\n”, lua_tostring(L,-1));
  exit(1);
}
printf(“Успешное завершение\n”);
lua_close(L);
return EXIT_SUCCESS;
}

Здесь считывается файл script.lua, создается глобальная переменная Lua var со значением 2 и выполняется Lua-программа. Элементы Lua C API описаны в заголовочных файлах lua.h и lauxlib.h. Переменная L – это указатель на структуру, описывающую состояние Lua. Саму структуру мы создаем с помощью функции luaL_newstate(). Функция luaL_openlibs() открывает стандартные библиотеки Lua для заданного состояния. Если ее не вызывать, загруженная программа Lua не сможет обращаться к элементам стандартных библиотек Lua, например, функции print(). Вызов luaL_loadfile() загружает текст программы Lua из файла. В случае успеха функция возвращает 0; ненулевое значение свидетельствует об ошибке.

Библиотека Lua возвращает подробные текстовые описания всех обнаруженных ошибок, причем использует для этого стандартный механизм передачи данных между Lua и программой-хозяином, то есть стек. В случае возникновения ошибки мы читаем ее описание посредством функции lua_tostring(). Обратите внимание на ее второй аргумент. Как и в большинстве функций Lua API, имеющих дело с данными Lua, в качестве источника сведений для lua_tostring() мы указываем ячейку стека. Они обозначаются индексами, причем начиная с единицы, а не с нуля. Вызов функции

lua_tostring(L, -1)

преобразует в строку C значение, находящееся в верхней ячейке стека (т.е. попавшее в стек последним). Аналогично, конструкция

lua_tostring(L, 1)

считывает значение, которое было помещено в стек первым. Ячейку, где находится самое последнее из помещенных в стек значений (и на которую ссылается индекс -1), мы называем вершиной стека. Ячейку, где находится самое первое помещенное в стек значение (самое старое, которому соответствует индекс 1), мы называем дном. Документация Lua придерживается противоположных обозначений (то, что мы называем дном стека, там считается вершиной, и наоборот). В данном примере будет считано описание ошибки, которое помещается на вершину стека.

Важно отметить, что функция lua_tostring() (а также lua_tonumber() и ей подобные) не удаляет значение из стека. При работе с функциями Lua API следует всегда помнить, как они влияют на стек. Библиотеки Lua не следят за переполнением стека – вы сами должны заботиться о том, чтобы в нем не было ничего лишнего. Для удаления из стека заданного значения предназначена функция lua_pop(). Мы не вызываем ее после lua_tostring() только потому, что программа в этом случае все равно завершится и очищать стек специально нет необходимости.

Если текст программы загружен успешно, функция luaL_loadfile() создает выполняемый интерпретатором Lua фрагмент и помещает его на вершину стека. Далее мы создаем глобальную переменную var. Функция lua_pushnumber() помещает в стек числовое значение. Макрос lua_setglobal() создает глобальную переменную и присваивает ей значение из ячейки, находящейся на вершине стека. Если мы посмотрим определение макроса lua_setglobal(), то увидим, что он сводится к вызову lua_setfield(). Первым аргументом этой функции является указатель на структуру, описывающую состояние Lua, вторым – индекс таблицы, в которую добавляется переменная, а третьим – строка C с ее именем. Позволю себе напомнить, что все переменные Lua являются элементами каких-либо таблиц. Так, глобальные переменные содержатся в _G (LXF124) – именно с этой таблицей и работает макрос lua_setglobal(). Разумеется, с помощью функции lua_setfield() (и макроса lua_setglobal()) можно не только создавать новые переменные, но и модифицировать значения уже существующих. С помощью этих же функций можно уничтожать переменные Lua. Делается это так же, как и внутри Lua-кода, то есть путем присвоения переменной значения nil. Для записи его в стек предназначена специальная функция lua_pushnil(). Функция lua_setfield() удаляет значение с вершины стека, так что вызов lua_pop() после нее не нужен.

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

Помимо функции lua_pcall(), API предоставляет нам lua_call(). Те, кто читал предыдущую статью, наверняка уже догадались, в чем разница между ними. Функция lua_pcall() выполняет загруженный код в «защищенном режиме», то есть подавляет все возникшие во время выполнения кода Lua ошибки, а не передает их на более высокий уровень. Но откуда функция lua_pcall() знает, какой фрагмент кода Lua она должна выполнить? В процессе ее вызова из стека извлекаются аргументы, переданные фрагменту кода Lua (если они есть), а после них lua_pcall() ожидает увидеть ссылку на сам фрагмент. Она также извлекается из стека, и на ее месте оказываются значения, возвращенные выполненным фрагментом. Последний аргумент lua_pcall() – индекс в стеке функции Lua, используемой для обработки ошибок. Если он равен 0, то в случае возникновения ошибки на вершине стека оказывается текстовое описание, которое мы выводим так же, как и в случае с luaL_loadfile(). Теперь нетрудно понять, как интерпретатор Lua выполняет несколько фрагментов программы, содержащихся в разных файлах, в едином контексте. Все, что для этого нужно – вызвать несколько функций lua_[p]call() с одной и той же переменной, описывающей состояние интерпретатора Lua. Наконец, мы уничтожаем структуру, описывающую состояние Lua, функцией lua_close().

Файл script.lua, который мы загружаем на выполнение в программе-примере, может содержать любой корректный фрагмент программы Lua. Нашу среду выполнения Lua отличает то, что фрагменту доступна глобальная переменная var. Это можно проверить с помощью простейшей конструкции:

print(“переменная var”, var)

Сохранив эту строку в файле script.lua, скомпилируйте нашу C-программу:

gcc runlua.c -o runlua -llua

Наберите ./runlua и убедитесь, что приложение работает. Попробуйте отредактировать текст script.lua и посмотрите, как наша программа реагирует на различные ошибки в коде Lua.

И снова калькулятор

В прошлый раз мы использовали возможности Lua для создания программы-калькулятора. Теперь давайте попробуем применить Lua для добавления функций калькулятора в приложение C++ (файл calc.cpp):

 #include <iostream>
 #include <string>
 extern “C” {
 #include “lualib.h”
 #include “lauxlib.h”
 }
 using namespace std;
 size_t l;
 const char * reader (lua_State *L, void *data, size_t *size) {
   char * result;
   * size = l;
   if (!l)
     result = 0;
   else
     result = (char *) data;
   l = 0;
   return result;
 }
 int main()
 {
   int result;
   lua_State * L;
   L = luaL_newstate();
   luaL_openlibs(L);
   while (true) {
     cout << “Введи те строку или на жми те Ctrl-C” << endl;
     string s;
     cin >> s;
     s =do return+ s + “ end \0”;
     l = s.length() + 1;
     result = lua_load(L, reader, &s[0], “calc”);
     if (result) {
       cout << lua_tostring(L, -1) << endl;
     }
     else {
       result = lua_pcall(L, 0, 1, 0);
       if (result != LUA_ERRRUN)
         cout << lua_tonumber(L, -1) << endl;
       else
         cout << lua_tostring(L, -1) << endl;
     }
     lua_pop(L, 1);
   }
   lua_close(L);
 }

Программа считывает строку со стандартного потока ввода и пытается вычислить содержащееся в ней математическое выражение (а на самом деле – все, что догадался набрать пользователь, так что будьте осторожны с этим в реальных приложениях!). Наш микрокалькулятор принимает выражения вида 2*(3+4), math.sin(3.14) и им подобные. Работа выполняется в бесконечном цикле, из которого можно выйти с помощью Ctrl+C.

Обратите внимание, что в программе, написанной на C++, нужно явным образом указать, что функции Lua API экспортируются в формате C. После того как мы считали строку (переменная s), нужно скомпилировать ее в функцию Lua. В калькуляторе, написанном на чистом Lua, для этого можно было использовать специальные функции. В Lua C API такие тоже были (например, lua_dostring()), но теперь они признаны устаревшими. Вместо них следует использовать lua_load(), которая умеет загружать исходный текст программы Lua из любого источника с помощью специальной вспомогательной функции. В нашем примере это reader(). При каждом вызове она должна вернуть указатель на новый фрагмент исходного текста программы Lua. Длина очередного фрагмента возвращается в параметре size. Параметр data представляет собой указатель на данные, определенные программистом. Функция reader() сигнализирует о том, что она прочитала весь исходный текст, возвращая значение NULL. При первом вызове наша функция просто преобразует введенную пользователем программы строку C++ в char * и возвращает ее длину. При втором вызове reader() сразу же возвращает NULL.

Вернемся к lua_load(). Ее второй параметр – адрес вспомогательной функции для чтения исходного текста, третий – адрес данных для вспомогательной функции. Далее следует строка с именем загружаемого фрагмента (можно передать пустую строку). Обработка ошибок, возникших во время выполнения функции lua_load(), осуществляется так же, как и в случае с luaL_loadfile(). Аналогично, функция lua_load() создает выполняемый интерпретатором Lua фрагмент и помещает его на вершину стека. После завершения lua_pcall() стек содержит значение, вычисленное нашим калькулятором. В случае ошибки оно будет иметь тип nil, и мы считываем его с верхушки стека с помощью вызова lua_tonumber(). Если вершина стека содержит нечисловое значение, вызов lua_tonumber() возвращает 0. После этого нам остается только удалить значение из стека с помощью lua_pop().

И наоборот

Посмотрим теперь, как вызвать из Lua функцию, написанную на C. Разумеется, в Lua нельзя импортировать любую функцию из C-библиотеки – слишком уж разные это языки программирования. Тем не менее, написать C-функцию в «экспортном варианте» оказывается на удивление просто. Рассмотрим сначала исходный текст библиотеки testlib (файл testlib.c), написанной на C:

 #include “lua.h”
 #include “lualib.h”
 #include “lauxlib.h”
 #include <stdlib.h>
 static int _system (lua_State *L)
 {
   int result = system(lua_tostring(L, -1));
   lua_pushinteger(L, result);
   return 1;
 }
 int luaopen_testlib(lua_State *L)
 {
   static const luaL_reg Map [] = {{“system”, _system},
 {NULL,NULL}} ;
   luaL_register(L, “testlib”, Map);
   return 1;
 }

Данная библиотека предоставляет Lua одну функцию – system(). Функция testlib.system() (под этим именем она будет доступна в Lua) делает то же, что и ее тезка из стандартной библиотеки C (разумеется, в стандартной библиотеке Lua она тоже есть). Во избежание неоднозначностей, в нашей C-библиотеке экспортируемой функции присвоено имя _system().

Чтобы понять, как работает экспорт C-функций, необходимо различать передачу аргументов и возврат значений в контексте C и те же действия в контексте Lua. Единственным аргументом экспортируемой функции в контексте C должен быть указатель на структуру lua_State, а возвращаемым значением (в контексте C) – число значений, возвращаемых в контексте Lua. Да, вы правильно поняли: в контексте Lua экспортируемая функция может получать произвольное количество аргументов различных типов и возвращать любое число значений. Все аргументы и значения в контексте Lua передаются, разумеется, через стек. Мы считываем единственный аргумент функции testlib.system() с вершины стека, превращаем его в строку, вызываем C-функцию system() и помещаем результат выполнения функции в стек Lua. Поскольку в контексте Lua наша функция всегда возвращает только одно значение, в C-коде мы пишем return 1.

Чтобы подготовить библиотеку к работе с Lua, нам надо написать еще одну функцию – luaopen_libname(), где libname соответствует имени библиотеки. Она будет вызвана интерпретатором Lua при загрузке нашей библиотеки. Именно она позволяет использовать C-библиотеку как стандартный пакет Lua.

Массив Map состоит из пар «имя функции в контексте Lua – указатель на C-функцию». Эти данные интерпретатор Lua будет использовать для вызова функции, написанной на C. Между прочим, поскольку имя функции в библиотеке C не имеет значения для интерпретатора, функции, в принципе, можно экспортировать и из кода C++, не указывая формат (это не касается luaopen_*(), которую интерпретатор ищет по имени C). Функция luaL_register() регистрирует новую библиотеку и помещает ссылку на соответствующий объект на вершину стека. Вторым аргументом функции luaL_register() должно быть имя библиотеки в контексте Lua, которое может и не совпадать с именем разделяемого модуля.

Для компиляции библиотеки скомандуем

gcc testlib.c -shared -o testlib.so

Теперь можно перейти к Lua-коду:

require “testlib”
print(“Вызов функции C”)
res = testlib.system(“/usr/bin/mc”)
if res ~= 0 then
 print(“Код завершения программы”, res)
else
 print(“Нормальный выход”)
end

Наш модуль testlib подключается к программе Lua так же, как и обычные библиотеки Lua (LXF124). Обратите внимание, что хотя в библиотеке testlib и используются функции Lua API, при компиляции мы не связываем ее с liblua. В этом нет необходимости, так как библиотека testlib будет загружена интерпретатором в то же адресное пространство, что и liblua. Между прочим, экспортировать функции C в Lua можно не только из разделяемых библиотек, но и непосредственно из файла программы, загружающей код Lua на выполнение. В этом случае нам пригодится другая функция Lua API – lua_register().

Каким образом функция C может узнать, сколько аргументов ей передано? В этом ей поможет функция lua_gettop(), которая возвращает число элементов в стеке. В момент вызова функции C оно равно количеству аргументов, переданных функции. Строго говоря, lua_gettop() возвращает индекс последней ячейки стека, которая в терминологии Lua именуется вершиной (в описании стека я, как и авторы многих руководств по Lua, придерживался других обозначений: вершина – первый элемент стека). Этой функцией можно пользоваться и для того, чтобы узнать, не переполнен ли стек. Глубина стека Lua контролируется константой MAX_STACK, значение которой невелико (обычно не больше 32), но для любого разумного использования этого должно быть достаточно. В конце концов, глубина стека математического сопроцессора у Intel составляет всего 8 ячеек.

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

Персональные инструменты
купить
подписаться
Яндекс.Метрика