LXF124:LUA

Материал из Linuxformat.

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

Содержание

Lua: Модули и иже с ними

LUA
Часть 3: Если вы прочли предыдущие статьи этой серии, то уже неплохо представляете себе синтаксис Lua. Андрей Боровский довершает картину несколькими точными мазками.

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

Благодаря модулям, Lua может нарисовать для вас красивые графики.

Перегрузка операторов

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

x = ‘string’
string_mt = getmetatable(x)
string_mt.__add = function(a, b)
 return a..b
end
print (‘А ‘ + ‘и ‘ + ‘Бе ‘ + ‘сидели на трубе’)

Стандартная функция getmetatable() позволяет получить ссылку на мета-таблицу, описывающую тип данных ее аргумента. Проще говоря,

string_mt = getmetatable(x)

присваивает переменной string_mt ссылку на мета-таблицу для значения x. А поскольку x содержит 'string’, мы получаем ссылку на мета-таблицу для типа «строка». Каждый раз, когда к значениям некоторого типа применяется оператор +, Lua вызывает из его мета-таблицы функцию с именем __add(). В терминологии Lua функции, ссылки на которые хранятся в мета-таблицах, называются мета-функциями (metamethods), а соответствующие им ключи – событиями (events). Таким образом, string_mt.__add() – это мета-функция для события __add. Ее аргументы – операнды, расположенные справа и слева от оператора +. В нашем примере мы заменяем стандартную мета-функцию __add() для строк на собственную, и теперь оператор +, выполняемый над строками, делает именно то, чего мы от него хотим. Таким образом, желающие могут использовать его для конкатенации строк.

Рассмотрим еще один, более практичный пример. Если к переменной, содержащей строку, применить оператор индексирования [], будет возвращено значение nil: по умолчанию он предназначен только для таблиц. Но теперь мы знаем, как это изменить:

string_mt.__index = function(s, i)
 return string.format('%c', string.byte(s, i))
end

Вызов string.byte(s, i) возвращает численное значение байта строки s под номером i (считая с единицы, а не с нуля). Функция string.format() позволяет нам преобразовать его в символ. В результате мы можем обращаться к элементам строк, как если бы это были массивы символов:

s = 'abcdef'
print(s[2])

Тут, правда, есть одно ограничение. Выражения с перегруженным оператором индексирования для строк могут использоваться только в качестве r-значений, то есть они должны стоять справа от оператора присваивания. Иными словами, конструкции вида

char = s[1]

допустимы, а выражения типа

s[1] = ‘A’

приведут к выводу сообщения об ошибке.

Если вы захотите изменить подобным образом поведение операторов для численного типа, у вас ничего не выйдет, так функция getmetatable() для него возвращает значение nil. В документации по Lua вы найдете описание всех мета-функций, реализующих стандартные операции над данными различных типов. Классический пример использования мета-таблиц для определения операций над данными – библиотека для работы с комплексными числами. Тип complex можно представить как таблицу вида {Re, Im}, а с помощью ее мета-таблицы можно определить все алгебраические операции над комплексными числами и задействовать для них принятые в математике символы.

Самостоятельная реализация такой библиотеки не должна вызвать у вас затруднений. Единственное, на что еще следует обратить внимание – механизм, с помощью которого мы указываем Lua, что некоторое значение вида {x, y} является значением типа complex (по умолчанию Lua, естественно, воспринимает его как обычную таблицу). Для решения этой задачи можно воспользоваться одним из тех способов, с помощью которых мы создавали объекты Lua (которые, по сути, тоже являются таблицами) в LXF123.

Таблица глобальных элементов

Все переменные и функции Lua, объявленные как глобальные, являются элементами глобальной таблицы _G. Зная этот факт, мы всегда можем получить перечень глобальных элементов программы с помощью следующей функции:

function list_globals()
 for name, value in pairs(_G) do
  print (name, value)
 end
end

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

Стандартные библиотеки

tekUI содержит богатый набор настраиваемых элементов управления.

В рассмотренных выше примерах мы уже пользовались функциями из стандартных библиотек Lua. Собственно говоря, все функции, за исключением тех, что мы определили сами, происходили из стандартных библиотек. Функции, которые мы вызывали, не указывая префикса, принадлежат базовой библиотеке Lua. Помимо уже знакомых нам print(), ipairs(), next(), setmetatable(), getmetatable(), tostring(), tonumber() и других, базовая библиотека содержит кое-что, с чем мы еще не встречались.

Функция dofile() – представитель замечательного семейства, позволяющего модифицировать код программы Lua непосредственно в процессе ее выполнения. Она загружает текст программного фрагмента из файла (или, если его имя не указано, из стандартного потока ввода) и выполняет его – как видно, программы Lua могут служить скриптами не только для приложений C/C++. Благодаря dofile(), сценарии Lua можно сделать самомодифицирующимися, но вообще-то она – не лучшее средство для организации системы автоматизации. Если исходный код, загруженный dofile(), содержит ошибки, они будут переданы «на самый верх» и, скорее всего, приведут к аварийному завершению всей Lua-программы. В отличие от dofile(), функции load(), loadfile() и loastring() предохраняют хозяйскую программу от ошибок во внешнем фрагменте, возвращая ссылку на функцию, инкапсулирующую загруженный код или же nil и текстовое сообщение об ошибке. Если вызов load*() вернул ссылку на функцию, значит, код загрузился успешно, и для его выполнения нужно вызвать ее по ссылке. Важное уточнение: когда я говорю, что load*() не порождает ошибок, я имею в виду ошибки, возникающие в процессе загрузки кода. Но то, что фрагмент удалось преобразовать в функцию, еще не значит, что она не содержит ошибок времени выполнения. Если таковые присутствуют, то в процессе вызова функции они проявят себя.

Страшно и представить, что может сделать изощренный ум программиста с самомодифицирующимся кодом. Мы рассмотрим очень простой и безобидный пример – программу-калькулятор (файл calculator-1.lua на диске).

s = 0
repeat
 f = loadstring('print('..s..')')
 if f ~= nil then
  status, error = pcall(f)
  if status == false then
   print('error: '..error)
  end -- status == false then
  end -- if f ~= nil then
  s = io.read()
 until s == 'q'

Мы считываем из stdin строку, содержащую математическое выражение, превращаем ее в функцию, возвращающую результат, и пытаемся выполнить эту функцию. Выйти из цикла можно, введя ‘q’. Функция io.read() из стандартной библиотеки io в данном примере считывает строку из стандартного потока ввода. Функция базовой библиотеки pcall(), с которой мы еще не встречались, выполняет вызов f() в защищенном режиме. Его не следует путать с защищенным режимом процессора; он просто означает, что любые ошибки, возникающие в ходе выполнения f() (см. далее), не приведут к остановке программы. Если вызов f() прошел успешно, pcall() возвращает true и результат выполнения f(), если же возникла ошибка, первым значением будет false, а вторым – описание исключительной ситуации. Строка ввода нашего калькулятора может содержать любое выражение на языке Lua, которое возвращает значение (не обязательно числовое).

Особенности Lua-ошибок

Ошибки времени выполнения в Lua могут возникать там, где их никогда не встретишь в C или Pascal. Рассмотрим, например, определение функции:

function f()
  return a + b
end

Встретив такой фрагмент кода, интерпретатор Lua будет считать, что a и b – переменные, которым где-то за пределами f() присвоены числовые значения. Будет ли вызов функции f() успешен, зависит от того, справедливо ли такое предположение. Если перед вызовом f() мы присвоим переменным a и b числа, f() сработает, но если мы забудем это сделать, программа аварийно завершится с сообщением, что мы пытаемся сложить нечисловые значения. Иначе говоря, из-за синтаксической гибкости Lua то, что в других языках было бы ошибкой времени компиляции, здесь становится ошибкой времени выполнения.

Прочие запасности

Кратко рассмотрим остальные стандартные библиотеки Lua. Coroutine, являющаяся частью базовой библиотеки, содержит функции для работы с сопроцедурами Lua. Как отмечалось ранее, многозадачность в Lua носит корпоративный характер, то есть сопроцедуры уступают друг другу место только по вашему повелению. Сопроцедуры создаются вызовом coroutine.create(), аргументом которой должна быть ссылка на функцию, определенную в Lua, запускаются через coroutine.resume() и выполняются до тех пор, пока добровольно не уступят машинное время другим сопроцедурам Lua с помощью вызова coroutine.yield(). Для возобновления выполнения можно снова вызвать coroutine.resume().

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

Библиотека string экспортирует функции для манипуляции со строками, например, string.format() и string.byte(). Корме того, она создает мета-таблицу для строк, которой мы и воспользовались выше при перегрузке операторов. String также содержит функции для работы с регулярными выражениями.

Библиотека table, как вы уже поняли, предназначена для работы с таблицами. Функция table.insert() вставляет новые значения в произвольном порядке, а table.sort() выполняет сортировку элементов таблицы. Функция table.maxn() возвращает максимальный положительный индекс в таблице. Любопытно, что, в отличие от оператора #, она умеет находить нецелочисленные индексы.

Списки-«тельняшки» — хит сезона не только в Web 2.0.

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

Очень интересна стандартная библиотека package, которая управляет модулями Lua и подключением к программам Lua разделяемых библиотек, написанных на C. Давайте остановимся на ней подробнее.

Модули

Модули Lua похожи на стандартные библиотеки: главное отличие заключается в том, что они не загружаются автоматически. Модули бывают двух типов: двоичные разделяемые библиотеки, которые обычно пишутся на C/C++ с использованием специального API, и файлы, содержащие исходный текст на самом Lua. Хотя по сути они принципиально отличаются друг от друга, для работы с ними используются одни и те же функции библиотеки package. Двоичные модули применяются в тех случаях, когда требуется расширить функциональные возможности в сфере взаимодействия с другими элементами системы, либо тогда, когда от функций модуля требуется высокое быстродействие.

Приступая к изучению модулей Lua, следует разобраться с терминологией. То, что мы будем называть модулем, в оригинальной документации носит название module. Модули в Lua могут иметь иерархическую структуру (то есть каждый модуль может содержать несколько подмодулей). Термин package (мы будем переводить его как «пакет») используется для обозначения полной иерархии модулей (которая представляет собой минимальную единицу распространения разделяемого кода в Lua).

В качестве примера использования модулей мы рассмотрим замечательное расширение Lua под названием tekUI (http://tekui.teklib.org). Как можно догадаться, tekUI позволяет программам Lua создавать графический пользовательский интерфейс. TekUI – не единственный графический инструментарий, доступный в Lua: для нее существуют пакеты-обертки, позволяющие подключать к Lua-программам библиотеки Qt, GTK, wxWidgets (мы видели примеры в LXF122) и FLTK. Конечно, любая из них превосходит tekUI по возможностям, но для их использования в Lua-программах понадобятся корректно установленные «родные» версии библиотек (а это дополнительная зависимость), тогда как tekUI – самостоятельный пакет, который займет в дистрибутиве значительно меньше места, чем полновесная графическая библиотека со всеми необходимыми привязками. На Unix-платформах tekUI работает напрямую с X11, а в Windows – с GDI. При этом возможности tekUI весьма широки, что демонстрируют входящие в состав дистрибутива примеры.

Кроме того, на данный момент, у tekUI есть еще одно серьезное преимущество. Все протестированные мной модули графического интерфейса, использующие большие библиотеки, оказались не полностью совместимыми с последней версией Lua (исключение – wxLua, который, впрочем, использовался в составе с собственным интерпретатором wxlua). В то же время, с tekUI не возникло никаких проблем. Исходные тексты tekUI вы можете найти на диске или загрузить из Интернета.

Ядром tekUI является несколько небольших библиотек, написанных на C, так что прежде чем ставить графический пакет, убедитесь, что в вашей системе есть GCC и сопутствующий инструментарий. Ниже приводится исходный текст простой программы, использующей tekUI:

ui = require “tek.ui”
ui.Application:new { Theme = “tutorial”, Children = 
 {ui.Window:new {Title = “Tutorial 1”, Children = 
 { ui.Text:new {Text = “Hello world”, } } } } }:run()

Бесконечные скобки могут напугать тех, кто незнаком с LISP, но на самом деле все не так уж и страшно. В этом примере создается три объекта Lua: ui.Application, ui.Window и ui.Text. Это происходит «по месту», так что иерархия вложенности объектов соответствует иерархии элементов интерфейса (см. рисунок).

Элементы интерфейса простой программы tekUI.

Останавливаться подробнее на особенностях разработки с tekUI мы не будем. Эта штука, хотя и не так сильна, как «Фауст» Гете, заслуживает, тем не менее, отдельного произведения. Мы рассмотрим часть, касающуюся модуля tekUI как такового. Программа начинается с вызова функции require() с аргументом “tek.ui”, представляющим собой имя загружаемого модуля. Хотя require() и не требует префикса, она экспортируется библиотекой package, а не базовой. Как и наша простая программа, многие более сложные приложения, написанные на Lua, не используют никаких функций библиотеки package, кроме require(). Тем не менее, package предоставляет множество функций и переменных, которые позволят нам исследовать механизм загрузки пакетов в Lua.

Если добавить в Lua-программу строку

print(package.cpath)

на консоли будет распечатано содержимое переменной cpath библиотеки package. Она содержит список директорий, в которых интерпретатор Lua будет искать модули, скомпилированные в разделяемые библиотеки. В моей системе cpath равна

./?.so;/usr/local/lib/lua/5.1/?.so;/usr/local/lib/lua/5.1/loadall.so

При поиске модуля символ “?” в этих выражениях заменяется на имя модуля.

Если добавить в программу строку print(package.path), будет выведен аналогичный список директорий, в которых интерпретатор будет искать модули, написанные на Lua.

Вернемся к нашей функции require(). Точка в имени модуля указывает, что ui является подмодулем tek. На практике это означает, что система будет искать нужный модуль в подкаталоге tek, который должен быть расположен в одной из директорий, перечисленных в переменных package.path и package.cpath. Де-факто интерпретатор Lua загружает файл /usr/local/share/lua/5.1/tek/ui.lua. Этот модуль, написанный на Lua, представляет собой прокси, управляющий загрузкой других модулей пакета tekUI. Из исходного текста мы видим, что функция require() возвращает значение, которое мы присваиваем переменной ui. В дальнейшем оно используется для обращения к функциям и переменным пакета. Что именно содержит переменная ui? Это можно узнать из описания функции require(), а можно провести эксперимент и добавить в программу вызов

print(type(ui))

Функция type() возвращает строку, описывающую тип значения аргумента. Таким образом мы узнаем, что переменная ui содержит значение типа «таблица». Как и все сложные структуры Lua, модуль Lua является таблицей. Теперь нетрудно понять и синтаксический смысл префиксов в именах библиотек – это просто имена переменных, в которых содержатся таблицы (вы могли заметить этот факт, когда изучали выдачу функции list_globals()).

Таблица package.loaded содержит информацию обо всех уже загруженных модулях. Например, выражение

package.loaded[“tek.ui”]

возвращает ссылку на модуль (таблицу) tek.ui, или nil, если модуль не загружен.

Интроспектива

Как выглядят модули Lua изнутри? О модулях, написанных на C, мы поговорим в следующий раз; сейчас рассмотрим модуль, написанный на Lua. В качестве примера рассмотрим algorithms.lua (его текст вы найдете на диске). Этот модуль экспортирует функции bininsert() – вставка значения в отсортированную таблицу, binsearch() – поиск значений в отсортированной таблице методом половинного деления, shellsort() – сортировку элементов таблицы методом Шелла, orderedPairs() – функцию-итератор для перебора элементов таблицы в порядке возрастания значений индекса. Исходные тексты всех функций я позаимствовал с сайта lua-users.org/wiki/SampleCode, и они не должны содержать для вас ничего принципиально нового. Сейчас мы сосредоточимся на коде, превращающем набор разрозненных функций в модуль.

Текст модуля algorithms начинается с вызова функции module():

module (“algorithms”, package.seeall)

Она создает таблицу с именем, соответствующем имени модуля, настраивает таблицу package.loaded и выполняет остальную рутину по созданию модуля. Функция package.seeall() позволяет модулю «видеть» все глобальные элементы, объявленные в загрузившей его программе. Помимо прочего, модуль выполняет функцию пространства имен. Все, что объявлено в модуле локально, не будет видимо за его пределами, а все глобальные элементы за пределами модуля необходимо будет вызывать с префиксом algorithms. Ниже приводится текст программы algtest, которая использует некоторые функции модуля algorithms.

require(“algorithms”)
t = {}
math.randomseed(os.time())
for i = 1, 10 do
   algorithms.bininsert(t, math.random(100))
end
for index, value in algorithms.orderedPairs(t) do
   print(index, value)
end

Если модуль algorithms.lua находится в той же директории, что и программа algtest.lua, или в одном из каталогов, перечисленных в переменной package.path, функция require() справится с загрузкой модуля самостоятельно, используя стандартный загрузчик модулей Lua. Но иногда бывает необходимо реализовать для определенного модуля свой собственный загрузчик. Эту задачу решает таблица package.preload. Ключами таблицы служат имена модулей, требующих специальной загрузки, а значениями – ссылки на функции-загрузчики.

Ну вот, теперь вы знаете о программировании в Lua достаточно для того, чтобы писать собственные программы. Но мы до сих пор не касались самого главного – включения Lua в проекты, написанные на других языках. Этим мы и займемся в следующий раз. LXF

Личные инструменты
  • Купить электронную версию
  • Подписаться на бумажную версию