Создание встраиваемых сценариев на языке Lua

В то время как интерпретируемые языки программирования, такие как Perl, Python, PHP и Ruby, пользуются все большей популярностью для Web-приложений (и уже давно предпочитаются для автоматизации задач по системному администрированию), компилируемые языки программирования, такие как C и C++, по-прежнему необходимы. Производительность компилируемых языков программирования остается несравнимой (она уступает только производительности ручного ассемблирования), поэтому некоторое программное обеспечение (включая операционные системы и драйверы устройств) может быть реализована эффективно только при использовании компилируемого кода.

Действительно, всегда, когда программное и аппаратное обеспечение нужно плавно связать между собой, программисты инстинктивно приходят к компилятору C: C достаточно примитивен для доступа к «голому железу» (то есть, для использования особенностей какой-либо части аппаратного обеспечения) и, в то же время, достаточно выразителен для описания некоторых высокоуровневых программных конструкций, таких как структуры, циклы, именованные переменные и области видимости.

Однако языки сценариев тоже имеют четкие преимущества. Например, после успешного переноса интерпретатора языка на другую платформу подавляющее большинство написанных на этом языке сценариев работает на новой платформе без изменений, не имея зависимостей, таких как системные библиотеки функций (представьте множество DLL-файлов операционной системы Microsoft® Windows® или множество libcs на UNIX® и Linux®). Кроме того, языки сценариев обычно предлагают высокоуровневые программные конструкции и удобные операции, которые программистам нужны для повышения продуктивности и скорости разработки. Более того, программисты, использующие язык сценариев, могут работать быстрее, поскольку этапы компиляции и компоновки не нужны. В сравнении с С и его родственниками цикл «кодирование, компоновки, связывание, запуск» сокращается до ускоренного «написание, запуск».

Новшества в Lua

Как и любой язык сценариев, Lua имеет свои особенности:

  • Типы в Lua. В Lua значения имеют тип, но переменные типизируются динамически. Типы nil, boolean, number и string работают так, как вы могли бы ожидать.
      • Nil — это тип специального значения nil; используется для представления отсутствия значения.
      • Boolean — это тип констант true и false (Nil тоже представляет значение false, а любое не nil значение представляет true).
      • Все числа в Lua имеют тип doubles (но вы можете легко создать код для реализации других числовых типов).
      • string — это неизменяемый массив для символов (следовательно, для добавления к строке вы должны сделать ее копию).
  • Типы table, function и thread являются ссылками. Каждый такой тип может быть назначен переменной, передаваемой в качестве аргумента, или возвращаемой из функции. Ниже приведен пример сохранения функции:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    -- пример анонимной функции,
    -- возвращаемой как значение
    -- см. http://www.tecgraf.puc-rio.br/~lhf/ftp/doc/hopl.pdf
    function add(x)
      return function (y) return (x + y) end
    end
    f = add(2)
    print(type(f), f(10))
    function  12
  • Потоки в Lua. Поток — это сопрограмма, создаваемая вызовом встроенной функции coroutine.create(f), где f — это функция Lua. Потоки не запускаются при создании; они запускаются позже при помощи функции coroutine.resume(t), где t — это поток. Каждая сопрограмма может время от времени отдавать процессор другим сопрограммам при помощи функции coroutine.yield().
  • Выражения присваивания. Lua разрешает множественные присваивания, и выражения сначала вычисляются, а затем присваиваются. Например, результат выражений
    1
    2
    3
    4
    i = 3
    a = {1, 3, 5, 7, 9}
    i, a[i], a[i+1], b = i+1, a[i+1], a[i]
    print (i, a[3], a[4], b, I)

    равен 4 7 5 nil nil. Если список переменных больше, чем список значений, лишним переменным присваивается значение nil; поэтому b равно nil. Если значений больше, чем переменных, лишние значения просто игнорируются. В Lua названия переменных зависят от регистра символов, что объясняет, почему переменная Iравна nil.

  • Порции (chunks). Порцией называется любая последовательность Lua-операторов. Порция может быть записана в файл или в строку в Lua-программе. Каждая порция выполняется как тело анонимной функции. Следовательно, порция может определять локальные переменные и возвращать значения.
  • Дополнительные интересные возможности. Lua имеет сборщик мусора «отметь и выкинь». В Lua 5.1 сборщик мусора работает в инкрементном режиме. Lua имеет полное лексическое замыкание (как Scheme, но не как Python). Кроме того, Lua имеет надежную семантику последовательных вызовов (tail call) (опять же, как Scheme, но не как Python).

Большее количество примеров Lua-кода приведено в руководстве «Программирование в Lua» и в wiki Lua-пользователей (ссылки приведены в разделе «Ресурсы»).

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

Беря все лучшее из обоих миров

Что, если бы вы могли взять лучшее из обоих миров: производительность работы с «голым железом» и высокоуровневые, мощные абстракции? Более того, если бы вы могли оптимизировать алгоритмы и функции, зависящие от системы и требующие много процессорного времени, так же как и отдельную логику, не зависящую от системы и очень чувствительную к изменениям требований?

Баланс требований для высокопроизводительного кода и высокоуровневого программирования является сутью Lua, встраиваемого языка программирования. Приложения, включающие Lua, представляют собой комбинацию компилируемого кода и Lua-сценариев. Компилируемый код может при необходимости заняться железом, и, в то же время, может вызывать Lua-сценарии для обработки сложных данных. И поскольку Lua-сценарии отделены от компилируемого кода, вы можете изменять сценарии независимо от него. С Lua цикл разработки более похож на «Кодирование, компоновка, запуск, создание сценариев, создание сценариев, создание сценариев …».

Например, на странице «Uses» Web-сайта Lua (см. раздел «Ресурсы») перечислены некоторые компьютерные игры для массового рынка, включая World of Warcraft и Defender (версия классической аркады для бытовых консолей), которые интегрируют Lua для запуска всего, начиная с пользовательского интерфейса и заканчивая искусственным интеллектом противника. Другие приложения Lua включают в себя механизмы расширения для популярного инструментального средства обновления Linux-приложений apt-rpm и механизмы управления чемпионатом Robocup 2000 «Сумасшедший Иван». На этой странице есть много хвалебных отзывов о маленьком размере и отличной производительности Lua.

Начало работы с Lua

Lua версии 5.0.2 на момент написания данной статьи была текущей версией (недавно появилась версия 5.1). Вы можете загрузить исходный код Lua с lua.org, а можете найти различные предварительно откомпилированные двоичные файлы на wiki Lua-пользователей (ссылки приведены в разделе «Ресурсы»). Полный код ядра Lua 5.0.2, включая стандартные библиотеки и Lua-компилятор, по размерам не превышает 200KB.

Если вы работаете на Debian Linux, то можете быстро и просто установить Lua 5.0 при помощи следующей команды

1
# apt-get install lua50

с правами суперпользователя. Все приведенные здесь примеры запускались на Debian Linux «Sarge» с использованием Lua 5.0.2 и ядра Linux 2.4.27-2-686.

После установки Lua на вашей системе попробуйте автономный Lua-интерпретатор. Все Lua-приложения должны быть встроены в базовое приложение. Интерпретатор — это просто специальный тип базового приложения, используемого для разработки и отладки. Создайте файл factorial.lua и введите в него следующие строки:

1
2
3
4
5
6
7
8
9
10
11
12
-- определяет функцию факториала
function fact (n)
  if n == 0 then
    return 1
  else
    return n * fact(n-1)
  end
end
print("enter a number:")
a = io.read("*number")
print(fact(a))

Код в factorial.lua (точнее, любая последовательность Lua-операторов) называется порцией (chunk), как было описано выше в разделе «Новшества в Lua». Для запуска созданной вами порции выполните команду lua factorial.lua:

1
2
3
4
$ lua factorial.lua
enter a number:
10
3628800

Или, как в других языках сценариев, вы можете добавить строку со знаками (#!) («shebang») в начало сценария, делая сценарий исполняемым, а затем запустить файл как автономную команду:

1
2
3
4
5
6
$ (echo '#! /usr/bin/lua'; cat factorial.lua) > factorial
$ chmod u+x factorial
$ ./factorial
enter a number:
4
24

Язык Lua

Lua обладает многими удобствами, имеющимися в современных языках программирования сценариев: область видимости, управляющие структуры, итераторы и стандартные библиотеки для обработки строк, выдачи и сбора данных и выполнения математических операций. Полное описание языка Lua приведено в «Справочном руководстве по Lua 5.0» (см. раздел «Ресурсы»).

В Lua тип имеют только значения, а переменные типизируются динамически. В Lua есть восемь фундаментальных типов (или значений): nil, boolean, number, string, function, thread, table и userdata. Первые шесть типов говорят сами за себя (исключения приведены в разделе «Новшества в Lua»); два последних требуют пояснения.

Таблицы в Lua

Таблицы — это универсальная структура данных в Lua. Более того, таблицы — это единственная структура данных в Lua. Вы можете использовать таблицу как массив, словарь (называемый также хеш-таблицей или ассоциативным массивом), дерево, запись и т.д.

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

Для исследования таблиц запустите Lua-интерпретатор и введите строки, показанные жирным шрифтом в листинге 1.

Листинг 1. Экспериментируя с таблицами Lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
$ lua
> -- создать пустую таблицу и добавить несколько элементов
> t1 = {}
> t1[1] = "moustache"
> t1[2] = 3
> t1["brothers"] = true
> -- создать таблицу и определить элементы (употребляется чаще)
> all at once
> t2 = {[1] = "groucho", [3] = "chico", [5] = "harpo"}
> t3 = {[t1[1]] = t2[1], accent = t2[3], horn = t2[5]}
> t4 = {}
> t4[t3] = "the marx brothers"
> t5 = {characters = t2, marks = t3}
> t6 = {["a night at the opera"] = "classic"}
> -- создать ссылку и строку
> i = t3
> s = "a night at the opera"
> -- индексами могут быть любые Lua-значения
> print(t1[1], t4[t3], t6[s])
moustache   the marx brothers classic
> -- фраза table.string эквивалентна фразе table["string"]
> print(t3.horn, t3["horn"])
harpo   harpo
> -- индексы могут быть также "многомерными"
> print (t5["marks"]["horn"], t5.marks.horn)
harpo   harpo
> -- i указывает на то же значение, что и t3
> = t4[i]
the marx brothers
> -- несуществующие индексы возвращают значения nil
> print(t1[2], t2[2], t5.films)
nil     nil     nil
>  -- даже функция может быть ключом
> t = {}
> function t.add(i,j)
>> return(i+j)
>> end
> print(t.add(1,2))
3
> print(t['add'](1,2))
3
>  -- и другой вариант функции в качестве ключа
> t = {}
> function v(x)
>> print(x)
>> end
> t[v] = "The Big Store"
> for key,value in t do key(value) end
The Big Store

Как вы могли ожидать, Lua также предоставляет несколько функций-итераторов для обработки таблиц. Функции предоставляет глобальная переменная table (да, Lua-пакеты — это тоже просто таблицы). Некоторые функции, например table.foreachi(), ожидают непрерывный диапазон целых ключей, начиная с 1 (цифра один):

1
2
3
> table.foreachi(t1, print)
1 moustache
2 3

Другие, например table.foreach(), выполняют итерацию по всей таблице:

1
2
3
4
5
6
7
8
> table.foreach(t2,print)
1       groucho
3       chico
5       harpo
> table.foreach(t1,print)
1       moustache
2       3
brothers        true

Хотя некоторые итераторы оптимизированы для целых индексов, все они просто обрабатывают пары (ключ, значение).

Ради интереса создайте таблицу t с элементами {2, 4, 6, language="Lua", version="5", 8, 10, 12, web="www.lua.org"} и выполните команды table.foreach(t, print) и table.foreachi(t, print).

Userdata

Поскольку Lua предназначен для встраивания в базовое приложение, написанное на таких языках, как, например, C или C++, для взаимодействия с базовым приложением данные должны совместно использоваться средой C и Lua. Как указано в «Справочном руководстве по Lua 5.0«, тип userdata позволяет «произвольным C-данным храниться в Lua-переменных». Вы можете рассматривать тип userdata как массив байтов — байтов, которые могут представлять указатель, структуру или файл в базовом приложении.

Содержимое userdata происходит от C, поэтому оно не может быть модифицировано в Lua. Естественно, поскольку userdata происходит от C, в Lua не существует предопределенных операций для userdata. Однако вы можете создать операции, которые работают с userdata, используя еще один механизм Lua, называемый мета-таблицами(metatables).

Мета-таблицы

Из-за такой гибкости типов table и userdata Lua разрешает перегружать операции для объектов каждого из этих типов (вы не можете перегружать шесть остальных типов). Мета-таблица — это (обычная) Lua-таблица, которая отображает стандартные операции в предоставляемые вами пользовательские функции. Ключи мета-таблицы называются событиями (event); значения (другими словами, функции) называются мета-методами (metamethod).

Функции setmetatable() и getmetatable() изменяют и запрашивают мета-таблицу объекта соответственно. Каждый объект table и userdata может иметь свою собственную мета-таблицу.

Например, одним из событий является __add (для добавления). Можете ли вы определить, что делает следующая порция?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- Перегрузить операцию add
-- для конкатенации строк
--
mt = {}
function String(string)
  return setmetatable({value = string or ''}, mt)
end
-- Первый операнд - это String table
-- Второй операнд - это string
-- .. - это операция конкатенации в Lua
--
function mt.__add(a, b)
  return String(a.value..b)
end
s = String('Hello')
print((s + ' There ' + ' World!').value )

Эта порция отображает следующий текст:

1
Hello There World!

Функция function String() принимает строку (string), заключает ее в таблицу ({value = s or ''}) и назначает мета-таблицу mt этой таблице. Функция mt.__add() является мета-методом, добавляющим строку b к строке, находящейся в a.valueb раз. Строка print((s + ' There ' + ' World!').value ) активизирует мета-метод дважды.

__index — это еще одно событие. Мета-метод для __index вызывается всегда, когда ключ в таблице не существует. Вот пример, который запоминает («memoizes») значение функции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- код, любезно предоставленный Рики Лэйком (Rici Lake), [email protected]
function Memoize(func, t)
  return setmetatable(
     t or {},
    {__index =
      function(t, k)
        local v = func(k);
        t[k] = v;
        return v;
        end
    }
  )
end
COLORS = {"red", "blue", "green", "yellow", "black"}
color = Memoize(
  function(node)
    return COLORS[math.random(1, table.getn(COLORS))]
    end
)

Поместите этот код в Lua-интерпретатор и введите print(color[1], color[2], color[1]). Вы должны увидеть что-то подобное blue black blue.

Этот код, получающий ключ и узел, ищет цвет узла. Если он не существует, код присваивает узлу новый, выбранный случайно цвет. В противном случае возвращается цвет, назначенный узлу. В первом случае мета-метод __indexвыполняется один раз для назначения цвета. В последнем случае выполняется простой и быстрый поиск в хеш-таблице.

Язык Lua предлагает много мощных функциональных возможностей, и все они хорошо документированы. Но всегда, когда вы столкнетесь с проблемами или захотите пообщаться с мастером, обратитесь за поддержкой к энтузиастам — IRC-канал Lua Users Chat Room (см. раздел «Ресурсы»).

Встроить и расширить

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

Сердцем симбиотического взаимодействия между Lua и его базовым языком является виртуальный стек. Виртуальный стек (как и реальный) является структурой данных «последний вошел — первый вышел» (last in-first out — LIFO), которая временно сохраняет аргументы функции и ее результаты. Для вызова из Lua базового языка (и наоборот) вызывающая сторона помещает значения в стек и вызывает целевую функцию; принимающая сторона достает аргументы из стека (конечно же, проверяя тип и значение каждого аргумента), обрабатывает данные и помещает в стек результаты. Когда управление возвращается вызывающей стороне, она извлекает значения из стека.

Фактически, все С-интерфейсы прикладного программирования (API) для Lua-операций работают через стек. Стек может хранить любое Lua-значение; однако тип значения должен быть известен как вызывающей стороне, так и вызываемой, а конкретные функции помещают в стек и извлекают из него каждый тип (например, lua_pushnil() и lua_pushnumber()).

В листинге 2 показана простая C-программа (взятая из главы 24 книги «Программирование в Lua«, ссылка на которую приведена в разделе «Ресурсы»), реализующая минимальный, но функциональный Lua-интерпретатор.

Листинг 2. Простой Lua-интерпретатор
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
1 #include <stdio.h>
 2 #include <lua.h>
 3 #include <lauxlib.h>
 4 #include <lualib.h>
 5
 6 int main (void) {
 7   char buff[256];
 8   int error;
 9   lua_State *L = lua_open();   /* открывает Lua */
10   luaopen_base(L);             /* открывает основную библиотеку */
11   luaopen_table(L);            /* открывает библиотеку table */
12   luaopen_io(L);               /* открывает библиотеку I/O */
13   luaopen_string(L);           /* открывает библиотеку string */
14   luaopen_math(L);             /* открывает библиотеку math */
15
16   while (fgets(buff, sizeof(buff), stdin) != NULL) {
17     error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
18             lua_pcall(L, 0, 0, 0);
19     if (error) {
20       fprintf(stderr, "%s", lua_tostring(L, -1));
21       lua_pop(L, 1);  /* извлечь сообщение об ошибке из стека */
22     }
23   }
24
25   lua_close(L);
26   return 0;
27 }

Строки с 2 по 4 включают стандартные Lua-функции, несколько удобных функций, используемых во всех Lua-библиотеках, и функции для открытия библиотек, соответственно. Строка 9 создает Lua-структуру. Все структуры сначала пусты; вы добавляете библиотеки или функции к структуре при помощи luaopen_...(), как показано в строках с 10 по 14.

В строке 17 luaL_loadbuffer() принимает входную информацию с stdin в виде порции и компилирует ее, помещая порцию в виртуальный стек. Строка 18 извлекает порцию из стека и выполняет ее. Если во время исполнения возникает ошибка, Lua-строка помещается в стек. Строка 20 обращается к вершине стека (вершина стека имеет индекс -1) как к Lua-строке, распечатывает сообщение и удаляет значение из стека.

Используя C API, ваше приложение может также «достать» информацию из Lua-структуры. Следующий фрагмент кода извлекает две глобальные переменные из Lua-структуры:

1
2
3
4
5
6
7
8
9
10
..
if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
  error(L, "cannot run configuration file: %s", lua_tostring(L, -1));
lua_getglobal(L, "width");
lua_getglobal(L, "height");
..
width = (int) lua_tonumber(L, -2);
height = (int) lua_tonumber(L, -1);
..

Опять же, обратите внимание на то, что передачу разрешает стек. Вызов любой Lua-функции из C аналогичен следующему коду: извлечь функцию при помощи lua_getglobal(), поместить аргументы, выполнить lua_pcall() и обработать результаты. Если Lua-функция возвращает n значений, первое значение находится по индексу -n в стеке, а последнее — по индексу -1.

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

Lua великолепен

Lua — это чрезвычайно легкий в использовании язык, но его простой синтаксис маскирует его мощь: язык поддерживает объекты (аналогичные объектам Perl), мета-таблицы делают его тип table абсолютно гибким, а C API разрешает отличную интеграцию и расширение сценариев и базового языка. Lua может использоваться совместно с языками C, C++, C#, Java™ и Python.

Перед созданием еще одного формата конфигурационного файла или ресурса (и еще одного синтаксического анализатора для него) попробуйте Lua. Язык Lua (так же как и его сообщество) надежен, изобретателен и готов прийти на помощь.