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

Всем привет.

Всем привет.

Сегодня мы поверхностно пройдёмся по языку Lua, его некоторым возможностям, а так же запуске наших сценариев в RakBot.
Lua — скриптовый язык программирования, предназначен для быстрой обработки данных. С помощью данного языка многие разработчики создают искусственный интелект в играх, пишут алгоритмы генерации уровней, а так же он используется для разработки ресурсов/игровых модов в Multi Theft Auto: San Andreas (аналог SA:MP). На самом деле, это простейший язык и с помощью него мы будем учиться писать собственную логику для ботов, которую будет использовать RakBot.

Пройдёмся по основам программирования, с которыми нам предстоит работать.

Обратите внимание

: данная статья будет урезана в плане языка Lua, так как в RakBot используется лишь небольшая её часть. Многие возможности Lua попросту отсустствуют в RakBot, поэтому я буду ориентироваться на версию из RakBot.

Есть традиция у всех авторов книг и документаций различных языков, это первая программа, которая печатает «Hello World».
Чтож, давайте попробуем написать её, но уже в RakBot. Переходим на оффициальный сайт RakBot и ищем раздел «Доступные функции», раздел «События».

Нам необходимо событие onScriptStart()

, которые вызывается автоматически при загрузке скрипта самим RakBot»ом.

В этой функции нам необходимо описать логику, которая будет писать в чат-лог RakBot»a «Hello World». Для этого, на той же странице в документации, посмотрим на раздел «Функции».

Первая фукнция printLog(text)

— это то, что нам и нужно. С помощью этой функции мы отправим сообщение в чат RakBot»а. Для этого мы напишем:

Мы написали логику в каком-то текстовом документе, но как сказать RakBot, чтобы он выполнил наш сценарий? Для этого необходимо сохранить файл с расширением .lua

и положить его в папку scripts
, в папке с RakBot.
Я сохранил текстовый документ с именем «example.lua

«. Давайте попробуем запустить RakBot и посмотреть, что у нас получилось.

Как мы видим, при запуске RakBot, он находит скрипт «example.lua
«, после чего выполняет его. Из этого мы можем сделать вывод, что инициализация сценария происходит при запуске самого RakBot или при перезагрузке всех сценариев командой !reloadscripts
.

Поздравляю, Вы только что написали свой собственный сценарий для RakBot!

Мы уже научились писать Hello World в консоли RakBot»а, но мы хотим писать сложных ботов, которые будут делать всю работу за нас, учитывая те или иные условия. На этом мы остановимся.
Практически всё, что происходит в программировании, можно описать следующим образом: возьми данные, что-то с ними сделай, отдай результат.
В данном случае данными выступает сам RakBot. Он сам запускает наши сценарии, а так же сам передаёт нам данные, которые мы можем обработать так, как хотим и в конце получить результат.

Давайте напишем простейший сценарий с условием. Условием будет являться ник бота. Если ник бота «СМaster», значит мы выведем в чат RakBot»а «CM FOREVER», если же ник бота совершенно другой — выведем в чат «Nonamer».
Для этого нам поможет условный оператор if else
, он же оператор ветвления. Он принимает на себя условие, которое должно вернуть либо true
, либо false
. Если условие равно true
, тогда код внутри будет выполнен, если false
— не будет выполнен.
На этом строится большая часть логики любого приложения. Дословно if
переводится как «ЕСЛИ», then
— «ЗНАЧИТ», else
— «ИНАЧЕ» Если это сильно сложно — не переживайте, Вы поймёте всё дальше.

В Lua есть следующие операторы сравнения:
>
Больше
<
Меньше
>=
Больше или равно
<=
Меньше или равно
~=
Не равно
==
Равно

Если мы напишем «CMaster
» == «CM
» — у нас будет значение False
, то есть, ложь
Если мы напишем «CMaster
» == «CMaster
» — у нас будет значение True
, то есть, истина.

5 > 10 — ложь
5 < 10 — истина
10 ~= 15 — истина
10 >= 5 — истина

Попробуем использовать логику ветвления в нашем предыдущем сценарии.

Тот код, который мы писали ранее:

Function onScriptStart()
printLog(«Hello world!»);
end

Преобразуем следующим образом:

Function onScriptStart()
botName = getNickName()
if(botName == «CMaster») then
printLog(«CM FOREVER»);
else
printLog(«Nonamer»);
end
end

Давайте разберём этот код начиная сверху. Я советую сразу начинать учить читать код. Поэтому попробуем прочитать, что у нас получилось

Function onScriptStart() — создаём фукнцию с именем onScriptStart
botName = getNickName() — в переменную botName записываем имя бота
if(botName == «CMaster») then — если имя бота равно «CMaster», значит
printLog(«CM FOREVER»); — пишем в чат «CM Forever».
else- ИНАЧЕ, или же если имя бота НЕ РАВНО «CMaster»
printLog(«Nonamer»); — пишем в чат «Nonamer»
end- конец условий
end- конец функции

Давайте попробуем проверить код, который мы написали. Я сохранил измененный код так же под именем «example.lua

» и запустил RakBot с ником «Mason_Bennett
«.

После загрузки нашего сценария, RakBot написал в чат Nonamer. Попробуем зайти с ником «CMaster
«.

Как мы видим, наше условие успешно работает и мы видим в чате то, что и хотели.

Пройдёмся немного по переменным. У Вас есть лист бумаги и Вы хотите его сохранить. Сохранить каким образом — куда-то положить, чтобы не потерять его. Например, мы можем положить наш лист бумаги в шкафчик и достать тогда, когда нам будет необходимо. Если у нас будет новый листок и нам не нужен будет старый — мы выкинем старый листок и положим новый.
Это и есть логика переменной. Мы можем создавать переменную с именами, которыми хотим и записывать в них значения, что мы и сделали в предыдущем примере с переменной botName.

В Lua мы можем записывать в переменную всё, что мы хотим. Например, я хочу создать переменную с именем PaperList
и записать в неё текст «Lua — урок №2
«. Для этого я напишу:

PaperList = «Lua — урок №1»

Что мы здесь сделали? Написали имя и использовали оператор присваивания «=» и теперь я могу использовать эту переменную в любом месте своего сценария.
Думаю, что если Вы вспомните математику на уровне максимум 5 класса — тут будет всё понятно.

У Lua есть несколько типов переменных, это nil, boolean, number, string
. Не бойтесь, это всё очень просто.

На самом деле их несколько больше, но я уже говорил, что в RakBot большая часть функционала отсутствует.

nil
— отсуствие значения.
boolean
— логические значения, принимает два варианта значений — либо true, либо false.
number
— вещественное число с двойной точностью. В Lua нет целочисленного типа, поэтому он выступает в качестве и вещественного и целочисленного типа.
string
— строка, здесь, я думаю, всё понятно.
Чтож, давайте попробуем создать несколько переменных и «поиграться» с ними.

number
= 0; — создаём переменную с именем number
и присваиваем значение 0
number
= number
+ 5; — присваивание значения переменной number
+ 5 (то есть, 0 + 5), теперь у нас хранится здесь число 5.
number
++; — ++ — инкремент. Другими словами — вы берёте переменную и увеличиваете её на одну единицу. То есть (5 + 1) — теперь 6 лежит у нас в переменной number.
number
—; — — декремент. Другими словами — уменьшаем на одну единицу. (6 — 1) — теперь значение равно 5.

string
= «Hello» — создаём переменную string
со значением «Hello»
string
= string
..
«,» — конкатенация строк, оно же сложение строк. Что мы здесь сделали? Указани имя переменной, указали оператор конкатенации «..
«, после чего указали ещё одну строку, которую необходимо добавить к первой. Теперь у нас в переменной «string
» лежит значение «Hello,».
string
= string
..
getNickName

() — теперь, к «Hello,» мы добавили ник бота, пускай будет «Michel». Теперь у нас в переменной string
лежит значение «Hello,Michel».

boolean
= true
; — создаём переменную boolean
со значением true
(ИСТИНА).
boolean
= getNickName

() ==
«Dimosha» — сравниваем имя бота со строкой Dimosha. Так как имя бота у нас Michel, из предыдущего примера, сюда запишется значение false (ЛОЖЬ).

Немного о функциях. Есть функции, которые возвращают значения, а есть те, которые не возвращают значение. Как Вы успели заметить, наша функция onScriptStart

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

Давайте пройдёмся по самому простому пути: фукнция без параметров и без возвращаемого значения, которая будет складывать 5 + 10 и выводить результат в консоль RakBot»а.

Я создам функцию с именем Add
:

Function Add() — Создаём фукнцию Add
printLog(5 + 10) — используем метод RakBot для вывода в консоль
end— Конец функции

Мы создали не совсем универсальную фукнцию по двум причинам:
— если мне нужно будет складывать другие числа — мне придётся создавать ещё одну такую же фукнцию
— я не могу использовать полученное значение за пределами фукнции

Попробуем исправить первый недочёт. Для этого мы добавим два параметра в фукнцию, которые будут принимать на себя числа для сложения. Сделаем мы это следующим образом:

Function Add(a, b)
printLog(5 + 10)
end

Теперь в методе у нас доступны два значения, которые содержатся в двух новых переменных a и b, но на консоль у меня всё равно выводится 15. Исправим это:

Function Add(a, b)
printLog(a + b)
end

Идеально. Теперь, при вызове этого метода, мы будем получать результат сложения в консоли. Попробуем протестировать. Изменим наш код в example.lua

на следующий:

Function Add(a, b)
printLog(a + b)
end
function onScriptStart()
Add(5, 10);
Add(123, 4324);
Add(555, 111);
end

И попробуем запустить RakBot. Посмотрим, что из этого получится:

Это решило нашу первую проблему. Попробуем решить вторую, чтобы наша функция возвращала результат.

Перепишем фукнцию Add
:

Function Add(a, b)
return a + b
end

return
— ключевое слово для возвращения значения из функции. Перепишем теперь метод onScriptStart
:

Function onScriptStart()
printLog(«Первое значение: «..Add(5, 10));
printLog(«Второе значение: «..Add(123, 4324));
printLog(«Третье значение: «..Add(555, 111));
end

Посмотрим, что получилось.

Мы могли создать три переменные, присвоив им значения из фукнций Add

и после их передавать в метод printLog

, но я не стал этого делать, так как код выглядит более читабельным и приятнее.

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

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

Lua? Что это?

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

Зачем?

Lua может вам пригодится:

* если вы геймер (плагины для World of Warcraft и множества других игр)
* если вы пишете игры (очень часто в играх движок пишут на C/C++, а AI — на Lua)
* если вы системный программист (на Lua можно писать плагины для nmap, wireshark, nginx и других утилит)
* если вы embedded-разработчик (Lua очень быстрый, компактный и требует очень мало ресурсов)

1. Научитесь программировать. Хотя бы немного. Не важно на каком языке.
2. Установите Lua. Для этого либо скачайте здесь версию 5.2 (http://www.lua.org/download.html), либо ищите ее в репозиториях. Версия 5.1 тоже пойдет, но знайте, что она очень старая.

Все примеры из статьи запускайте в терминале командой наподобие «lua file.lua».

Первые впечатления

Lua — язык с динамической типизацией (переменные получают типы «на лету» в зависимости от присвоенных значений). Писать на нем можно как в императивном, так и в объектно-ориентированном или функциональном стиле (даже если вы не знаете как это — ничего страшного, продолжайте читать). Вот Hello world на Lua:

My first lua app: hello.lua
print «hello world»;
print(«goodbye world»)

Что уже можно сказать о языке:

* однострочные комментарии начинаются с двух дефисов «—»
* скобки и точки-с-запятыми можно не писать

Операторы языка

Набор условных операторов и циклов довольно типичен:

Условные операторы (ветки else может не быть)
if a == 0 then
print(«a is zero»)
else
print(«a is not zero»)
end
— сокращенная форма if/elseif/end (вместо switch/case)
if a == 0 then
print(«zero»)
elseif a == 1 then
print(«one»)
elseif a == 2 then
print(«two»)
else
print(«other»)
end
— цикл со счетчиком
for i = 1, 10 do
print(i)
end
— цикл с предусловием
b = 5
while b > 0 do
b = b — 1
end
— цикл с постусловием
repeat
b = b + 1
until b >= 5

ПОДУМАЙТЕ: что может означать цикл «for i = 1, 10, 2 do … end» ?

В выражениях можно использовать такие вот операторы над переменными:

* присваивание: x = 0
* арифметические: +, -, *, /, % (остаток от деления), ^ (возведение в степень)
* логические: and, or, not
* сравнение: >, <, ==, <=, >=, ~= (не-равно, да-да, вместо привычного «!=»)
* конкатенация строк (оператор «..»), напр.: s1=»hello»; s2=»world»; s3=s1..s2
* длина/размер (оператор #): s=»hello»; a = #s (‘a’ будет равно 5).
* получение элемента по индексу, напр.: s

Битовых операций в языке долгое время не было, но в версии 5.2 появилась библиотека bit32, которая их реализует (как функции, не как операторы).

Типы данных

Я вам соврал, когда сказал что у языка один тип данных. Их у него много (как и у каждого серьезного языка):

* nil (ровным счетом ничего)
* булевы числа (true/false)
* числа (numbers) — без деления на целые/вещественные. Просто числа.
* строки — кстати, они очень похожи на строки в паскале
* функции — да, переменная может быть типа «функция»
* поток (thread)
* произвольные данные (userdata)
* таблица (table)

Если с первыми типами все понятно, то что же такое userdata? Вспомним о том, что Lua — язык встраиваемый, и обычно тесно работает с компонентами программ, написанными на других языках. Так вот, эти «чужие» компоненты могут создавать данные под свои нужды и хранить эти данные вместе с lua-объектами. Так вот, userdata — и есть подводная часть айсберга, которая с точки зрения языка lua не нужна, но и просто не обращать внимания на нее мы не можем.

А теперь самое важное в языке — таблицы.

Таблицы

Я вам снова соврал, когда сказал, что у языка 8 типов данных. Можете считать что он один: всё — это таблицы (это, кстати, тоже неправда). Таблица — это очень изящная структура данных, она сочетает в себе свойства массива, хэш-таблицы («ключ»-«значение»), структуры, объекта.

Итак, вот пример таблицы как массива:
a = {1, 2, 3} — массив из 3-х элементов
print(a) — выведет «2», потому что индесы считаются с единицы
— А таблица в виде разреженного массива (у которого есть не все элементы)
a = {} — пустая таблица
a = 1
a = 5

ПОДУМАЙТЕ: чему равно a в случае разреженного массива?

В примере выше таблица ведет себя как массив, но на самом деле — у нас ведь есть ключи (индексы) и значения (элементы массива). И при этом ключами могут быть какие угодно типы, не только числа:

A = {}
a[«hello»] = true
a[«world»] = false
a = 1
— или так:
a = {
hello = 123,
world = 456
}
print(a[«hello»))
print(a.hello) — то же самое, что и a[«hello»], хотя выглядит как структура с полями

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

T = {
a = 3,
b = 4
}
for key, value in pairs(t) do
print(key, value) — выведет «a 3», потом «b 4»
end

А как же объекты? О них мы узнаем чуть позже, вначале — о функциях.

Функции

Вот пример обычной функции.

Function add(a, b)
return a + b
end
print(add(5, 3)) — напечатает «8»

Функции языка позволяют принимать несколько аргументов, и возвращать несколько аргументов. Так аргументы, значения которых не указаны явно, считаются равными nil.

ПОДУМАЙТЕ: зачем может понадобиться возвращать несколько аргументов?

Function swap(a, b)
return b, a
end
x, y = swap(x, y)
— кстати, это можно сделать и без функции:
x, y = y, x
— и если уж функция возвращает несколько аргументов,
— а они вам не нужны — игнорируйте их с помощью
— специальной переменной-подчеркивания «_»
a, _, _, d = some_function()

Функции могут принимать переменное количество аргументов:

В прототипе переменное число аргументов записывается как троеточие
function sum(…)
s = 0
for _, n in pairs(arg) do — в функции обращаются к ним, как к таблице «arg»
s = s + n
end
return a
end
sum(1, 2, 3) — вернет 6
sum(1, 2, 3, 4) — вернет 10

Поскольку функции — это полноценный тип данных, то можно создавать переменные-функции, а можно передавать функции как аргументы других функций

A = function(x) return x * 2 end — функция, умножающая на 2
b = function(x) return x + 1 end — функция, увеличивающая на 1
function apply(table, f)
result = {}
for k, v in pairs(table) do
result[k] = f(v) — заменяем элемент на какую-то функцию от этого элемента
end
end
— ПОДУМАЙТЕ: что вернут вызовы
t = {1, 3, 5}
apply(t, a)
apply(t, b)

Объекты = функции + таблицы

Раз мы можем сохранять функции в переменных, то и в полях таблиц тоже сможем. А это уже получаются как-бы-методы. Для тех, кто не знаком с ООП скажу, что основная его польза (по крайней мере в Lua) в том, что функции и данные, с которыми они работают находятся рядом — в пределах одного объекта. Для тех, кто знаком с ООП скажу, что классов здесь нет, а наследование прототипное.

Перейдем к примерам. Есть у нас объект, скажем, лампочка. Она умеет гореть и не гореть. Ну а действия с ней можно сделать два — включить и выключить:

Lamp = {
on = false
}
function turn_on(l)
l.on = true
end
function turn_off(l)
l.on = false
end
— это просто функции для работы со структурой
turn_on(lamp)
turn_off(lamp)

А если лампочку сделать объектом, и функции turn_off и turn_on сделать полями объекта, то получится:

Lamp = {
on = false
turn_on = function(l) l.on = true end
turn_off = function(l) l.on = false end
}
lamp.turn_on(lamp)
lamp.turn_off(lamp)

Мы вынуждены передавать сам объект лампочки в качестве первого аргумента, потому что иначе наша функция не узнает с какой именно лампочкой надо работать, чтобы сменить состояние on/off. Но чтобы не быть многословными, в Lua есть сокращенная запись, которую обычно и используют — lamp:turn_on(). Итого, мы уже знаем несколько таких упрощений синтаксиса:

Lamp:turn_on() — самая общепринятая запись
lamp.turn_on(lamp) — то с точки зрения синтаксиса это тоже правильно
lamp[«turn_on»](lamp) — и это

Продолжая говорить о сокращениях, функции можно описывать не только явно, как поля структуры, но и в более удобной форме:

Lamp = {
on = false
}
— через точку, тогда аргумент надо указывать
function lamp.turn_on(l) l.on = true end
— через двоеточкие, тогда аргумент неявно задается сам, как переменная «self»
— «self» — и есть та лампочка, для которой вызвали метод
function lamp:turn_off() self.on = false end

Интересно?

Специальные функции

Некоторые имена функций таблиц (методов) зарезервированы, и они несут особый смысл:

* __add(a, b), __sub(a, b), __div(a, b), __mul(a, b), __mod(a, b), __pow(a, b) — вызываются, когда выполняются арифметические операции над таблицей
* __unm(a) — унарная операция «минус» (когда пишут что-то типа «x = -x»)
* __lt(a, b), __le(a, b), __eq(a, b) — вычисляют результат сравнения (<, <=, ==)
* __len(a) — вызывается, когда делается «#a»
* __concat(a, b) — вызывается при «a..b»
* __call(a, …) — вызывается при «a()». Переменные аргументы — это аргументы при вызове
* __index(a, i) — обращение к a[i], при условии, что такого элемента не существует
* __newindex(a, i, v) — создание «a[i] = v»
* __gc(a) — когда объект удаляется при сборке мусора

Подменяя эти методы, можно перегружать операторы и использовать синтаксис языка для своих целей. Главное не переусердствовать.

Наследование

Для тех, кто не знает ООП, наследование позволяет расширить функциональность уже существующего класса. Например, просто лампочка умеет включаться-выключаться, а супер-ламкочка будет еще и яркость менять. Зачем нам переписывать методы turn_on/turn_off, если можно их повторно использовать?

В Lua для этого есть понятие мета-таблицы, т.е. таблицы-предка. У каждой таблицы есть одна таблица-предок, и дочерняя таблица умеет делать все, что умеет предок.

Допустим, что объект-таблицу lamp мы уже создали. Тогда супер-лампочка будет выглядеть так:

Superlamp = {
brightness = 100
}
— указываем родительскую таблицу
setmetatable(superlamp, lamp)
— и ее методы теперь доступны
superlamp:turn_on()
superlamp:turn_off()

Расширение функциональности

Родительские таблицы есть у многих типов (ну у строк и таблиц точно, у чисел и булевых чисел, и у nil их нет). Допустим, мы хотим складывать все строки с помощью оператора «+» , а не «..» . Для этого надо подменить функцию «+» (__add) для родительской таблицы всех строк:

S = getmetatable(«») — получили родительскую таблицу строки
s.__add = function(s1, s2) return s1..s2 end — подменили метод
— проверяем
a = «hello»
b = «world»
print(a + b) — напишет «helloworld»

Собственно, мы еще можем заменить функцию print с помощью «print = myfunction», да и много других хакерских дел можно сделать.

Области видимости

Переменные бывают глобальные и локальные. При создании все переменные в Lua являются глобальными.

ПОДУМАЙТЕ: почему?

Для указания локальной области видимости пишут ключевое слово local:

Local x
local var1, var2 = 5, 3

Не забывайте об этом слове.

Обработка ошибок

Часто, если возникают ошибки, надо прекратить выполнение определенной функции. Можно, конечно, сделать множество проверок и вызывать «return», если что-то пошло не так. Но это увеличит объем кода. В Lua используется что-то наподобие исключений (exceptions).

Ошибки порождаются с помощью функции error(x). В качестве аргумента можно передать все, что угодно (то, что имеет отношение к ошибке — строковое описание, числовой код, объект, с которым произошла ошибка и т.д.)

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

Function f(x, y)

if … then
error(«failed to do somthing»)
end

end
status, err = pcall(f, x, y) — f:функция, x-y: ее аргументы
if not status then
— обработать ошибку err. В нашем случае в err находится текст ошибки
end

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

Нестандартных библиотек много, их можно найти на LuaForge, LuaRocks и в других репозиториях.

Между Lua и не-Lua

А если нам недостаточно функциональности стандартных библиотек? Если у нас есть наша программа на C, а мы хотим вызывать ее функции из Lua? Для этого есть очень простой механизм.

Допустим, мы хотим создать свою функцию, которая возвращает случайное число (в Lua есть math.random(), но мы хотим поучиться). Нам придется написать вот такой код на C:

#include
#include
#include
/* собственно, что делать при вызове `rand(from, to)` */
static int librand_rand(lua_State *L) {
int from, to;
int x;
from = lua_tonumber(L, 1); /* первый параметр функции */
to = lua_tonumber(L, 2); /* второй параметр функции */
x = rand() % (to — from + 1) + from;
lua_pushnumber(L, x); /* возвращаемое значение */
return 1; /* возвращаем только один аргумент */
}
/* в Lua «rand» соответствует нашей функции librand_rand() */
static const luaL_reg R = {
{«rand», librand_rand},
{NULL, NULL} /* конец списка экспортируемых функций */
};
/* вызывается при загрузке библиотеку */
LUALIB_API int luaopen_librand(lua_State *L) {
luaL_openlib(L, «librand», R, 0);
srand(time(NULL));
return 1; /* завершаемся успешно */
}

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

Расширенная форма оператора for

В расширенной форме оператора for для последовательного получения значений переменной цикла используется вызов итератора. Цикл завершается, когда итератор возвращает nil.

Примечание

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

Расширенная форма оператора for имеет следующий вид:

for var1, var2, …, varN in do

… — тело цикла

var1, var2, …, varN — список переменных, получающих значения на каждом шаге цикла. Список может состоять из одной или нескольких переменных, разделённых запятыми. Первую в списке переменную называют управляющей переменной цикла. Когда эта переменная получает возвращённое итератором значение nil, цикл завершается. Остальные переменные на ход выполнения цикла влияния не оказывают;

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

Оператор for в расширенной форме имеет те же особенности, что и числовой for:

переменные цикла var1, var2, …, varN являются локальными для оператора цикла и по его окончании не определены;

значения переменных цикла нельзя изменять внутри цикла.

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

for key, val in pairs(t) do

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

Работает расширенный оператор for следующим образом:

Вызывает функцию pairs(t), от которой принимает три значения:

стандартную функцию next в качестве итератора;

таблицу, которую требуется обойти (t), в качестве состояния;

nil в качестве начального значения управляющей переменной цикла.

Вызов функции pairs выполняется только один раз.

Оператор for приступает к выполнению, собственно, итераций цикла:

вызывает функцию-итератор next с двумя параметрами: таблицей t и nil. Функция next, вызванная с этими параметрами, возвращает начальный ключ таблицы и соответствующее ему значение (при условии, что таблица не пуста);

вновь вызывает функцию next, передавая ей таблицу t и ключ, полученный на первой итерации. Функция nextвозвращает следующую пару ключ-значение. Этот процесс продолжается до тех пор, пока функция next не вернётnil.

Описываемые действия можно представить в виде следующего кода:

local f, s, next_key = pairs(t)

local key, val = f(s, next_key)

if next_key == nil then break end

MsgBox(«key == »..key..»; val == «..val)

Выполняющий все описанные действия цикл for можно реализовать и без использования функции pairs. Известно, чтоpairs возвращает функцию next и таблицу, которую следует обойти. Поэтому вызов pairs можно заменить списком соответствующих переменных:

for key, val in next, t do

MsgBox («key == »..key..»; val == «..val)

Примечание

Помимо pairs, стандартные библиотеки Lua предоставляют ещё несколько функций-фабрик итераторов. Так, для перебора элементов массива предусмотрена функция ipairs, а для итерирования по строкам файла — функция io.lines.

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

Операторы break и return в Lua

Оператор break прерывает цикл (while, repeat или for), в теле которого встречается. В результате выполнения оператора break управление передаётся первой инструкции, следующей непосредственно за оператором цикла.

for i = 1,#a do — ищем в массиве отрицательное значение

if a[i] < 0 then — если найдено…

index = i — сохраняем индекс найденного значения…

break — и прерываем цикл

Оператор return возвращает результаты из функции (или блока).

Оператор return может просто завершать работу функции (блока), не возвращая никаких результатов.

Обратите внимание

Операторы break и return могут быть только последними операторами блока (иначе следующие за ними операторы никогда не выполнятся). Если действительно необходимо вставить return или break в середину блока, например, чтобы временно отключить выполнение части кода функции, эти операторы следует заключить в свой блок do–end.

return — НЕВЕРНО!

<другие операторы>

do return end — Правильно

<другие операторы>

Создание таблиц в Lua. Работа с полями

Создать пустую таблицу можно следующим образом:

Для доступа к полю таблицы используется запись вида:

имя_переменной[ключ]

t = {} — создаем пустую таблицу

t = «first» — новое поле таблицы, с ключом 1 и значением «first»

t = 20 — новое поле, с ключом 2 и значением 20

t[k] = «Jane» — новое поле, с ключом «name» и значением «Jane»

a = t — переменная a получает значение «first»

b = t — переменная b получает значение 20

c = t[«name»] — переменная c получает значение «name»

В случае строковых ключей вместо записи t[«name»] можно использовать запись t.name:

t.name = «name» — эквивалентно t[«name»] = «name»

a = t.name — эквивалентно a = t[«name»]

Обратите внимание

Выражение t.name не равнозначно t. Первое выражение представляет поле таблицы, ключом которого являетсястрока «name» (то есть эквивалентно t[«name»]). Второе выражение представляет поле, ключом которого является значение переменной name. Различия между этими выражениями показаны в следующем примере:

name = «somebody»

t = «Jane» — в поле «somebody» помещено значение «Jane»

a = t — переменная a получает значение поля «somebody» («Jane»)

b = t.name — поля «name» не существует, переменная b получает nil

c = t.somebody — переменная c получает значение поля «somebody» («Jane»)

Если поля таблицы с заданным ключом не существует, обращение к нему дает nil:

a = t.name — переменная a получает значение nil

Для удаления поля таблицы достаточно присвоить ему nil:

Таблицу можно заполнить значениями непосредственно при создании. Для этого в фигурных скобках следует перечислить ключи и значения элементов таблицы (в формате [ключ]=значение). Элементы отделяются друг от друга запятыми (,) или точками с запятой (;):

t = {[«red»]=«красный», [«green»]=«зеленый», [«blue»]=«синий»}

t.red = «красный»; t.green = «зеленый»; t.blue = «синий»

В случае строковых ключей квадратные скобки (и двойные кавычки) можно не указывать:

Если необходимо создать таблицу, поля которой также являются таблицами, это можно сделать следующим образом:

a = {x=20, y=1},

Приведённая запись эквивалентна следующему коду:

p.a = {x=20, y=1},

p.b = {x=40, y=2}

Работа с массивами в Lua

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

Это выражение эквивалентно следующему коду:

Обратите внимание

В Lua массивы индексируются, начиная с 1 (а не с 0, как в некоторых языках программирования).

Оператор получения длины #, применённый к массиву, возвращает его максимальный индекс (или размер):

t = {«красный», «зеленый», «синий»}

n = #t — n равно 3

В примере ниже приведён ряд типичных для Lua синтаксических конструкций (идиом), основанных на использовании оператора #:

a = t[#t] — присвоим переменной a значение последнего элемента массива t

t[#t] = nil — удалим последний элемент массива t

t[#t+1] = a — добавим значение переменной a в конец массива t

Обратите внимание

При работе с массивами следует учитывать важную особенность оператора #. Этот оператор рассматривает любой неинициализированный (имеющий значение nil) элемент массива как признак конца массива. Поэтому, например, для таблицы:

t = {=«first», =«third»} — элемент с индексом 2 отсутствует (t==nil)

оператор получения длины вернёт 1, а не 2 или 3. Таким образом, для корректной работы оператора # необходимо, чтобы массив не содержал «пустых» элементов.

Если таблица не содержит целочисленных ключей (либо элемент с индексом 1 равен nil), оператор # возвращает 0:

t = {red=«красный», green=«зеленый», blue=«синий»}

n = #t — n равно 0

t = {=«красный», =«зеленый», =«синий»}

n = #t — n равно 0 (поскольку t имеет значение nil)

Обход элементов таблицы в Lua

Для обхода всех элементов таблицы обычно используют расширенную форму оператора for совместно со стандартной функцией pairs.

t = { name = «Евгений»,

surname = «Степанов»,

for key, val in pairs(t) do

MsgBox(key..»: «..val)

На каждом шаге цикла переменная key получает ключ очередного поля таблицы t, а переменная val — соответствующее ключу значение поля. Цикл выполняется по всем полям таблицы.

Для обхода массива, то есть таблицы с целочисленными ключами, удобнее использовать другую стандартную функцию Lua — ipairs.

for i, val in ipairs(t) do

MsgBox(«№»..i..»: «..val)

На каждом шаге цикла переменная i получает числовой индекс очередного поля таблицы t, а переменная val- соответствующее индексу значение поля. Цикл продолжается до первого целого ключа, отсутствующего в таблице.

Примечание

При обходе массива необходимость в получении индекса зачастую отсутствует. В этом случае переменную i можно не вводить, заменив её символом подчеркивания (_).

t = { «Иванов», «Степанов», «Петров» }

for _, val in ipairs(t) do

Обойти массив можно и с помощью обычного числового for.

t = { «Иванов», «Степанов», «Петров» }

for i = 1, #t do

MsgBox(«Значение элемента №»..i..»: «..t[i])

Определение функций в Lua

Типичное определение функции выглядит следующим образом:

return x*y — тело функции

Как видно из этого примера, определение функции состоит из следующих элементов:

ключевого слова function;

имени (f в данном примере);

заключённого в круглые скобки списка аргументов функции (возможно, пустого);

тела функции, размещённого между закрывающей скобкой и ключевым словом end.

Приведённое выше определение функции является просто более удобным способом записи следующего выражения:

f = function (x,y)

return x*y — тело функции

Таким образом, определение функции фактически выполняет два действия:

Создаёт объект типа «функция» (это делает выражение function (x,y)… end).

Обратите внимание

Имя f принадлежит переменной, в которую помещена ссылка на функцию, но не самой функции. Любые функции в Lua анонимны, то есть не имеют имён. Когда говорят об имени функции, например, «функция f», на самом деле подразумевают переменную f, содержащую ссылку на эту функцию. У переменных, содержащих ссылки на функции, нет жёсткой привязки к самим функциям; работа с такими переменными осуществляется точно так же, как и с любыми другими переменными.

function f(x,y) return x*y end — создаем функцию, переменная f содержит ссылку на эту функцию

MsgBox(f(2,3))—> 6

s = f — переменная s ссылается на ту же функцию, что и f

MsgBox(s(2,3))—> 6

t.sqr = f — поле sqr таблицы t ссылается на ту же функцию, что и f

MsgBox((t.sqr(2,3))—> 6

f = MsgBox — f теперь «превратилась» в функцию MsgBox

f(«Привет, мир!») —> «Привет, мир!»

Несмотря на то, что обычно ссылка на созданную функцию присваивается переменной, такое присваивание не является обязательным. В приведённом ниже примере создаётся безымянная функция, которая сразу передаётся в качестве параметра в функцию table.sort:

table.sort (t, function (a, b) return (a > b) end)

Вызов функций в Lua

Вызов функции состоит из имени переменной, содержащей ссылку на функцию, и заключённого в круглые скобки списка аргументов (возможно, пустого).

a = f(2,3) — вызов функции f с двумя аргументами: 2 и 3

Возвращаемое функцией значение помещается в переменную a

Если функции передаётся только один аргумент, и этот аргумент является строковой константой или конструктором таблицы, круглые скобки при вызове функции можно не использовать.

require «myscript» — то же самое, что и require («myscript»)

MsgBox [[Это многострочное значение]] — то же самое, что и MsgBox([[Это многострочное значение]])

render {x=1, y=2} — то же самое, что и render({x=1, y=2})

t.f() — вызов функции, на которую ссылается поле f таблицы t

В Lua имеется возможность вызова функции в объектно-ориентированном стиле:

t:f() —эквивалентно t.f(t)

Таким образом, конструкция t:f() вызывает функцию, на которую ссылается поле f таблицы t, и эта же таблица передаётся в функцию в качестве неявного первого аргумента.

В качестве аргументов функция может принимать несколько значений. Если при вызове функции последние значения не заданы, им присваивается nil. Если значений больше, чем аргументов, «лишние» значения отбрасываются. Аргументы функции являются локальными переменными внутри функции.

function f(x, y) — определение функции

MsgBox («x=»..tostring(x)..»; «..«y=»..tostring(y))

f() — вызов функции. x, y равны nil

f(1) — вызов функции. x = 1, y равен nil

f(1, 2) — вызов функции. x = 1, y = 2

f(1, 2, 3) — вызов функции. x = 1, y = 2, 3 — лишний параметр

Функции с переменным числом аргументов в Lua

Функция может принимать переменное число параметров. Для этого список аргументов в определении функции должен заканчиваться многоточием (…).

function f(x, y, …)

Все значения, скрытые за многоточием, передаются функции через локальную таблицу arg. Поле n этой таблицы содержит число переданных аргументов. Поэтому значения переданных в функцию аргументов можно получить, например, так:

for i = 1, arg.n do

MsgBox (tostring(arg[i]))

Возврат значений из функции Lua

Функции могут возвращать одно или несколько значений, а также не возвращать значений вообще.

function f0() — функция f0 не возвращает значений

MsgBox («Вызвана функция f0»)

MsgBox («Вызвана функция f1»)

return 1 — функция f1 возвращает одно значение

MsgBox («Вызвана функция f3»)

return 1, 2, 3 — функция f3 возвращает три значения

f0() — вызов функции f0

a = f1() — вызов функции f1. a = 1

a, b, c = f3() — вызов функции f3. a = 1, b = 2, c = 3

Если количество возвращаемых функцией значений превышает число переменных, которым эти значения должны быть присвоены, «лишние» значения отбрасываются. Если возвращаемых значений меньше, чем переменных, отсутствующие значения заменяются nil.

a = f3() — a = 1, значения 2 и 3 отброшены

a, b, c = f3() — a = 1, b = 2, значение 3 отброшено

a, b, c = f0() — a = nil, b = nil, c = nil

a, b, c = f1() — a = 1, b = nil, c = nil

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

f3() — значения, возвращенные функцией f3, отброшены

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

local a, b, c = f3()

Как не задавать лишних имён переменных?

Здесь общепринятым способом является использование переменной с именем «_» (символом подчёркивания). Это упрощает внешний вид выражения и указывает на реально используемые переменные.

local _, _, c = f3() — нам нужно только третье возвращаемое значение

В данном случае, переменная «_» принимает первое, а потом второе возвращаемое значение.

Такой же подход часто используется в работе с итераторами:

for _, value in pairs() do

… — нам нужно только очередное значение, ключ будет присвоен переменной _

Обратите внимание

Переменная с именем «_» является обычной переменной. При использовании этой переменной в других выражениях, она будет переопределяться, т. е. содержать то значение, которое было ей присвоено в последний раз:

local _, _, c = f3() — переменная _ будет содержать второй параметр

f4(_, true) — первым параметром функуции f4 будет не пустое значение,

а значение содеражащееся в переменной _

Возврат значений при множественном присваивании

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

a, b, c, d = 5, f3() — a = 5, b = 1, c = 2, d = 3

a, b, c, d = f3(), 5 — a = 1, b = 5, c = nil, d = nil

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

t = {5, f3()} — t = {5, 1, 2, 3}

t = {f3(), 5} — t = {1, 5}

func (5, f3()) — вызов функции func с аргументами 5, 1, 2, 3

func (f3(), 5) — вызов функции func с аргументами 1, 5

return 5, f3() — возврат значений 5, 1, 2, 3

return f3(), 5 — возврат значений 1, 5

Возврат функций из функций

Функции можно возвращать из функций. Например:

function outer()

function inner()

local i = outer() — значение переменной i равно функции inner

local a = i() — вызываем функцию i, значение переменной a равно 1

Базовая библиотека стандартных функций Lua

Функции базовой библиотеки Lua

Функции базовой библиотеки Lua размещаются в глобальном контексте.

В базовую библиотеку входят следующие функции:

Имя функции
— Описание

assert-
Генерирует ошибку с необязательным сообщением об ошибке, если значение первого аргумента false или nil.

collectgarbage
-Обеспечивает интерфейс к сборщику мусора. Первый аргумент определяет выполняемое действие (остановка, запуск сборщика мусора, выполнение сборки мусора и др.).

error-
Завершает выполнение последней функции, вызванной в защищённом режиме, с заданным сообщением об ошибке.

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

getmetatable-
Если объект не имеет метатаблицы, возвращает nil. Иначе, если в метатаблице есть поле __metatable, возвращает значение этого поля. В противном случае возвращает метатаблицу объекта.

ipairs
— Возвращает итератор, таблицу и 0. Возвращаемый итератор проходит таблицу по целочисленным индексам от значения 1 до первого индекса со значением nil. Итератор возвращает текущий индекс и соответствующее ему значение.

next-
Возвращает следующий (после заданного) индекс в таблице и соответствующее ему значение. Позволяет последовательно получить все поля таблицы.

pairs
— Возвращает итератор next(), таблицу и nil. Возвращаемый итератор проходит таблицу по всем значениям индекса. Итератор возвращает текущий индекс и соответствующее ему значение.

pcall
— Вызывает заданную функцию с аргументами в защищённом режиме. Возвращает статус успешности выполнения.

rawequal
— Сравнивает два объекта, без вызова каких-либо метаметодов. Возвращает значение типа boolean.

rawget-
Возвращает реальное значение с заданным индексом из таблицы, без вызова каких-либо метаметодов.

rawset
— Помещает значение в поле таблицы с заданным индексом, без вызова каких-либо метаметодов.

select
— Если первый аргумент функции имеет числовое значение, возвращаются все аргументы, следующие за аргументом с этим номером. Если первый аргумент — строка ’#’, возвращается общее число полученных аргументов.

setfenv
— Устанавливает таблицу в качестве контекста для заданной функции. Аргумент, задающий функцию, может быть числом, определяющим положение функции на стеке вызовов. Если он равен 0, устанавливается глобальный контекст текущего потока.

setmetatable
— Устанавливает (удаляет) метатаблицу для данной таблицы. Если метатаблица содержит поле __metatable, генерирует ошибку.

tonumber
— Пытается конвертировать аргумент в число. Если конвертирование не удаётся, возвращает nil.

tostring
— Преобразует аргумент любого типа в строку.

type
— Возвращает тип аргумента в виде строки.

unpack-
Возвращает элементы из заданной таблицы.

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

Глобальные переменные

Помимо функций, базовая библиотека предоставляет также следующие глобальные переменные:

Имя переменной
Описание

_G
Таблица, содержащая глобальное окружение скрипта.

_M
Таблица, содержащая текущий модуль.

Библиотека для работы со строками Lua

Все функции для работы со строками собраны в таблице string. Поэтому для вызова функций используется запись вида:

string.имя_функции(…)

Например:

Поддерживается также объектно-ориентированная форма записи. Например:

s:trim() — эквивалентно string.trim(s)

В библиотеку для работы со строками входят следующие стандартные функции:

Имя функции-
Описание

len-
Возвращает длину строки.

rep
— Возвращает строку, содержащую указанное число копий исходной строки.

lower
— Заменяет все прописные буквы в строке на строчные.

upper
— Заменяет в строке все строчные буквы на прописные.

reverse
— Возвращает строку, в которой символы исходной строки расположены в обратном порядке.

format-
Генерирует строку по форматной строке и аргументам по правилам, принятым в языке C.

byte
— Возвращает числовые коды символов строки.

char
— Преобразует набор числовых кодов символов в строку.

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

match-
Выполняет поиск в строке первого вхождения подстроки, соответствующей заданному шаблону. Возвращает найденную подстроку.

gmatch-
Возвращает итератор, который на каждой итерации возвращает подстроку, соответствующую заданному шаблону.

sub
— Возвращает подстроку исходной строки.

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

dump-
Возвращает строку, содержащую двоичное представление функции Lua с заданным именем.

Библиотека для работы с таблицами Lua

Все функции для работы с таблицами собраны в таблице table. Для вызова функций используется запись вида:

table.имя_функции(…)

Например:

Имя функции-
Описание

insert-
Вставляет элемент в заданную позицию таблицы, сдвигая остальные элементы таблицы вправо.

remove
— Удаляет заданный элемент таблицы, сдвигая остальные элементы влево. Возвращает значение удалённого элемента.

sort-
Сортирует элементы таблицы в заданном порядке. Вторым аргументом может быть задана функция, которая будет использована вместо стандартного оператора «<» для сравнения элементов в процессе сортировки.

concat-
Выполняет склейку указанных элементов массива через заданный разделитель. По умолчанию разделителем является пустая строка.

maxn-
Возвращает наибольший положительный числовой индекс в таблице. Возвращает 0, если таблица не имеет положительных числовых индексов.

Математическая библиотека Lua

Функции математической библиотеки Lua

Математические функции собраны в таблице math. Для вызова функций используется запись вида:

math.имя_функции(…)

Например:

В данную библиотеку включены следующие стандартные функции:

Имя функции-
Описание

abs
— Возвращает модуль числа.

ceil-
Возвращает наименьшее целое число, большее или равное заданному (выполняет округление «вверх»).

floor-
Возвращает наибольшее целое число, меньшее или равное заданному (выполняет округление «вниз»).

max-
Возвращает максимальный из аргументов.

min-
Возвращает минимальный из аргументов.

fmod-
Возвращает остаток от деления одного числа на другое.

modf-
Возвращает целую и дробную части исходного числа.

frexp-
Возвращает нормализованную мантиссу и показатель аргумента.

ldexp-
Строит число по мантиссе и показателю.

pow-
Возводит число в степень. Вместо вызова функции возможно использование выражения вида x^y.

sqrt-
Вычисляет квадратный корень числа. Вместо вызова функции возможно использование выражения вида x^0.5.

exp-
Возвращает ex.

log-
Вычисляет натуральный логарифм.

log10-
Вычисляет логарифм по основанию 10.

cos-
Вычисляет косинус угла, заданного в радианах.

sin-
Вычисляет синус угла, заданного в радианах.

tan-
Вычисляет тангенс угла, заданного в радианах.

cosh-
Вычисляет гиперболический косинус.

sinh-
Вычисляет гиперболический синус.

tanh-
Вычисляет гиперболический тангенс.

acos-
Вычисляет арккосинус (в радианах).

asin-
Вычисляет арксинус (в радианах).

atan-
Вычисляет арктангенс (в радианах).

atan2-
Возвращает арктангенс x/y (в радианах), но использует знаки обоих параметров для вычисления «четверти» на плоскости. Также корректно обрабатывает случай когда y равен нулю.

deg-
Переводит величину угла из радиан в градусы.

rad-
Переводит величину угла из градусов в радианы.

random-
Функция, вызванная без аргументов, возвращает псевдослучайное число из интервала . Эта же функция, вызванная с двумя аргументами l, u возвращает целое псевдослучайное число из интервала .

randomseed Устанавливает стартовое число генератора псевдослучайных чисел.

Переменные математической библиотеки Lua

Таблица math предоставляет следующие переменные:

Имя переменной
— Описание

huge-
Наибольшее представимое число.

pi-
Число пи.

Библиотека ввода/вывода в Lua

Библиотека ввода-вывода предоставляет два различных «стиля» для работы с файлами.

Использование неявных дескрипторов файлов

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

Для вызова функций используется запись вида:

io.имя_функции(…)

Например:

Ниже приведён список стандартных функций ввода/вывода, предоставляемых таблицей io:

Имя функции
— Описание

open-
Открывает файл в заданном режиме.

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

output-
Аналогична io.input, но работает со стандартным файлом вывода.

close-
Закрывает файл. Эквивалентна file:close. Вызванная без параметра, закрывает стандартный файл вывода.

read-
Аналогична io.input():read.

lines-
Открывает файл с заданным именем в режиме чтения и возвращает функцию-итератор (iterator function), которая при каждом последующем вызове возвращает новую строчку из файла. При обнаружении функцией-итератором конца файла, она возвращает nil (для окончания цикла) и автоматически закрывает файл. Вызов io.lines без указания имени файла эквивалентен io.input():lines(); таким образом, он обрабатывает строки стандартного файла ввода. В этом случае файл по окончанию итераций не закрывается автоматически.

write-
Эквивалентна io.output():write.

flush-
Сохраняет все данные, записанные в стандартный поток вывода. Эквивалентна file:flush для стандартного потока вывода.

tmpfile-
Возвращает дескриптор для временного файла. Этот файл открывается в режиме изменения и автоматически удаляется при завершении программы.

type-
Проверяет, является ли объект, переданный функции в качестве параметра, корректным дескриптором файла. Возвращает строку «file», если объект является открытым дескриптором файла; строку «closed file» — если объект является закрытым дескриптором файла; nil — если объект не является дескриптором файла.

Использование явных дескрипторов файлов

При использовании явных файловых дескрипторов сначала создаётся объект-дескриптор file (для этого используется функция io.open), после чего все операции с файлом выполняются при помощи методов данного дескриптора.

Для вызова методов используется запись вида:

handle: имя_метода(…),

где handle — имя переменной, содержащей объект-дескриптор.

Ниже приведён список стандартных функций ввода/вывода, предоставляемых объектом file:

Имя функции-
Описание

read-
Читает данные из файла в соответствии с заданными форматами.

lines-
Возвращает функцию-итератор, которая при каждом вызове возвращает новую строку из файла. В отличие от io.lines, эта функция не закрывает файл по окончании цикла (т. е. достижении конца файла). При вызове функции без параметров, возвращает текущий файл ввода по умолчанию. В случае ошибок, данная функция возбуждает ошибку вместо того, чтобы возвратить код ошибки.

write-
Записывает в файл значения всех своих аргументов.

flush-
Сохраняет все данные, записанные в файл.

setvbuf-
Устанавливает режим буферизации для выходного файла.

seek-
Устанавливает текущую позицию в файле.

close-
Закрывает файл.

Библиотека для доступа к средствам операционной системы в Lua

Функции, обеспечивающие доступ к средствам операционной системы, собраны в таблице os. Для вызова функций используется запись вида:

os.имя_функции(…)

Например:

В данную библиотеку включены следующие стандартные функции:

Имя функции-
Описание

clock-
Возвращает примерное количество времени (в секундах), которое программа выполнялась на CPU.

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

difftime-
Возвращает число секунд, прошедшее от времени t1 до времени t2.

execute-
Передаёт заданную команду на исполнение оболочке операционной системы.

getenv-
Возвращает значение заданной переменной окружения или nil, если переменная не определена.

remove-
Удаляет файл или папку с заданным именем. Папки должны быть пусты.

rename-
Переименовывает файл или папку.

time-
Возвращает текущее время при вызове без аргументов, или время и дату, указанные в передаваемой таблице. Эта таблица должна иметь поля year, month, и day, и может иметь поля hour, min, sec, и isdst.

tmpname-
Возвращает строку с именем файла, который может быть использован в качестве временного файла. Файл должен быть явно открыт до использования и явно удалён, если больше не будет

Часть 2
: Разобравшись с базовыми возможностями Lua
, Андрей Боровский
пробует эмулировать в нем конструкции, знакомые по другим языкам.

На предыдущем уроке мы узнали о существовании Lua
– встраиваемого языка сценариев; мы разобрались, чем он может быть полезен, и рассмотрели примеры написанных на нем простых программ. Мы освоили ввод-вывод и основные управляющие конструкции и познакомились с таблицами – фундаментальным типом данных Lua, лежащим в основе всего мало-мальски сложного (и интересного).

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

Функции Lua

Давайте рассмотрим такую коротенькую программу:

Function foo()
print(“Привет, я — функция foo()!”)
return 1
end
print(foo())
print(foo)

Первые четыре строки в пояснениях особо не нуждаются. Ключевое слово function
объявляет функцию. Далее следуют ее имя и список аргументов, заключенный в скобки (у нас он пуст). Тело функции – это блок, обязанный заканчиваться ключевым словом end
. Обратите внимание, что хотя наша функция возвращает значение 1, оператором return
, тип возвращаемого значения в ее объявлении не указывается. Аналогично тому, как одна и та же переменная Lua
может принимать значения любых определенных в языке типов, одна и та же функция Lua
может возвращать значения всех возможных типов. Все-таки Lua
не зря назван именем небесного тела, обозначающего в символике многих народов переменчивость и обманчивость. В пятой строке мы распечатываем значение, возвращаемое foo()
(при этом, естественно, выполняется сама функция foo()
). Шестая строка выглядит интереснее. В нашем фрагменте, foo
– это переменная, содержащая значение типа «функция» (на самом деле – идентификатор функции, но об этом ниже). В шестой строке мы печатаем значение переменной foo
, а не результат, возвращаемый функцией. Вот что мы получим:

Привет, я — функция foo()!
1
function: 00379B20

Первые две строки вывода – результат выполнения выражения print(foo())
. Последняя строка показывает содержимое переменной foo
. Слово function
свидетельствует о том, что она содержит идентификатор функции. Далее следует само значение идентификатора (в нашем случае – 32‑битное шестнадцатеричное число). Возникает соблазн назвать идентификатор адресом функции, но следует помнить, что концепция адресов и указателей в Lua
отсутствует.

Для завершения примера приведем определение функции, которая принимает параметры:

Bar = function(a, b)
print(“a+b=”.. a + b)
end
bar(2,3)

Конструкция

Bar = function(a, b)

эквивалентна

Function bar(a, b)

как, например, в JavaScript
. А вот еще один интересный момент:

Function baz()
return 1, true, “три”
end
a,b,c = baz()
print(a,b,c)

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

Итераторы

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

Function sum(…)
r = 0
for i, v in ipairs(arg) do
r = r + v
end
return r
end
print(sum(1,2,4,8,16,32))

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

Вспомнив определение оператора #
(LXF122), вы поймете, что выражение #arg
вернет число аргументов функции. Однако разработчикам Lua
этого показалось мало, и в таблице arg
есть еще одно поле с индексом n
, которое содержит число аргументов, так что вместо #arg
можно (и предпочтительно) использовать arg.n
.

Зная все это, мы могли бы использовать уже известную нам форму оператора for
для работы с численными индексами элементов таблицы arg
(предлагаю вам сделать это самостоятельно). Мы же рассмотрим другой вариант, обладающий более широкими возможностями. В общем виде он выглядит так:

For <список переменных> in <итератор, данные> do

end

Функции-итераторы служат для последовательного перебора элементов таблицы и могут использоваться не только в операторе for
. В нашем примере мы используем встроенную функцию ipairs()
, которая, будучи совмещена с циклом for
, последовательно заполняет две переменные парами значений «индекс аргумента – его значение» (в нашем примере i
содержит индекс элемента arg
, а v
– значение индексированного элемента). В результате переменная v последовательно принимает значения всех аргу-ментов (т. е. элементов таблицы arg
). У функции ipairs()
есть брат-близнец pairs()
, который оперирует парами «ключ–значение», а не «индекс–значение» (см. врезку).

Расставим точки над ‘i’

Если в рассмотренном нами примере итератор ipairs()
заменить на pairs()
, результат выполнения функции sum()
будет другим. Дело в том, что ipairs()
перебирает только индексируемые элементы массива, тогда как pairs()
учтет и arg.n
. Значение этого элемента в нашем примере равно 6, так что вместо ожидаемой суммы 63 мы получим 69.

Теперь вам явно хочется написать собственный итератор! Давайте реализуем итератор bpairs()
, перебирающий элементы массива arg
в обратном порядке. Как ни странно, для этого потребуется объявить не одну, а две функции:

Function backwards(table, count)
count = count — 1
if table then
return count, table
end
end
function bpairs(table)
return backwards, table, #table+1
end

Аргументами функции backwards()
должны быть таблица table
и значение count
, равное количеству индексируемых элементов плюс 1. Внутри самой функции значение count
уменьшается на 1, и возвращается это уменьшенное значение и соответствующий ему элемент таблицы. Так будет происходить до тех пор, пока table
не окажется равным nil
. Если вам кажется, что с функцией backwards()
не все так просто, читайте врезку.

Повышенная передача

Параметры-переменные функций Lua
передаются не по значению, а по ссылке. Таким образом, изменение значения любого аргумента внутри функции приводит к изменению этого значения и за ее пределами. Этим фактом мы и пользуемся в функции backwards()
.

Функция bpairs()
работает и того проще. Она возвращает три вещи: саму функцию backwards()
и значения аргументов для ее первого вызова. Оператор for
вызывает функцию backwards()
, используя «для затравки» значения, полученные от bpairs()
, до тех пор, пока backwards()
возвращает результат. Если вы не поняли это место, не пугайтесь: сейчас будет еще один наглядный пример. Теперь мы можем заменить строку

For i, v in ipairs(arg) do

For i, v in bpairs(arg) do

Аргументы функции sum()
будут перебираться в обратном порядке, в чем можно убедиться, вставив в цикл вызов print(i,v)
. Сам результат от перемены мест слагаемых не изменится.

Зная, как работают функции-итераторы, мы можем воспроизвести механику оператора for
и без обертки bpairs()
:

For i, v in backwards, arg, #arg+1 do
r = r + v
end

For i, v in backwards, arg, arg.n+1 do
r = r + v
end

В принципе, функция bpairs()
нам не нужна. Это просто удобство, позволяющее написать одно выражение вместо трех.

Чего только нет

Как и в любых других блоках, в теле функции можно объявлять локальные переменные, видимые только внутри нее. В отличие от C/C++
, эти переменные нельзя объявить статическими, то есть сделать так, чтобы они хранили данные в перерывах между вызовами функции. Впрочем, статические локальные переменные можно эмулировать. Вот одно из возможных решений:

Do
local loc=0
function fred(a)
loc=loc+a
return loc
end
end

Переменная loc
объявлена как локальная, и за пределами блока do…end
видна не будет. Функция fred()
, напротив, не локальная, и ее можно вызывать за пределами блока. Поскольку переменная loc
объявлена вне блока функции fred()
, она будет существовать в перерывах между вызовами fred()
, но поскольку loc
локальна для блока, в котором определена функция fred()
, никто, кроме fred()
, не сможет получить к ней доступ.

Нет в синтаксисе Lua
и концепции параметра со значением по умолчанию (как в C++
), но и тут нам на помощь приходит хакерская изобретательность:

Function defval(v)
v = v or «default value»
return v
end
print(defval())
print(defval(‘Мое значение’))

То, что при объявлении функции указан список параметров, не означает, что соответствующие им значения необходимо вводить при вызове. Если параметру функции не сопоставлено значение, он будет равен nil
. Смысл строки

V = v or «default value»

можно перевести так: если v
не равно nil
, присвоить v
значение v
, иначе присвоить v
значение «default value»
. Оператор or
ведет себя здесь не так, как при работе с логическими значениями, а как краткая форма if
. Таким образом, если при вызове defval()
мы не указываем v
, в теле функции ему назначается значение по умолчанию. В противном случае используется значение, переданное через v
.

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

Например, строка

Backwards = nil

удаляет функцию backwards()
. Тут, правда, есть один тонкий момент. Рассмотрим фрагмент

Foo = backwards

После первого присваивания идентификатор foo
можно использовать так же, как идентификатор backwards
. Например:

For i, v in foo, arg, arg.n+1 do

При этом мы не делаем из одной функции две. Как было сказано выше, у каждой определенной нами функции есть численный идентификатор, который и копируется в процессе присваивания. Если теперь мы напишем

Backwards = nil

переменная backwards
перестанет указывать на функцию, а foo
– не перестанет. В результате память, занятая функцией, освобождена не будет. Уследить за тем, чтобы ни одна переменная не содержала идентификатор функции (а только в этом случае произойдет ее удаление) очень сложно. Эту задачу выполняет автоматический сборщик мусора. Контрольный вопрос: при каких условиях сборщик мусора сможет удалить переменную loc
из примера с функцией fred()
? Ответ: когда будут удалены все ссылки на fred()
.

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

Объекты в Lua

Родные объекты в Lua
отсутствуют, и нам придется их эмулировать. Гибкость синтаксиса это позволяет, но прежде необходимо понимать основы реализации ООП в других языках. В первом приближении, объект – это совокупность структур данных и методов для оперирования ими. Если структура данных и набор методов у двух объектов совпадают, эти объекты могут принадлежать (а могут и не принадлежать) одному классу. Как правило, в программе используется несколько объектов одного класса. Для каждого из них создается своя область данных (чем же иначе объекты будут отличаться друг от друга?), но для ее обработки у всех объектов одного класса используются (физически) одни и те же методы. Каким образом метод, который мало чем отличается от обычной функции, узнает, с какой именно структурой данных ему предстоит работать? Для этой цели у него есть скрытый параметр (в одних языках он называется this
, в других – self
, в третьих – dontuseme
), который представляет собой указатель на структуру данных того объекта, для которого вызывается метод.

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

С учетом изложенного выше, давайте рассмотрим определение объекта Employee
(сотрудник).

Employee = {name = , age = 0, salary = 0, position =
}
function Employee.incAge(self)
self.age = self.age + 1
end
function Employee.scaleSalary(self, factor)
self.salary = self.salary*factor
end
function Employee.print(self)
print(self.name, «age: «..self.age, «salary: «..self.salary, «position: «..self.position)
end;
Employee.name = «Vasya Pupkin»
Employee.age = 25
Employee.position = «Manager»
Employee.salary = 1000
Employee:incAge()
e = Employee
Employee = nil
e:scaleSalary(2);
e:print()

У объекта (таблицы) Employee
есть четыре поля данных, имена которых говорят сами за себя. Кроме того, для объекта Employee
определено три метода: incAge()
, увеличивающий значение поля age
на единицу, scaleSalary()
, умножающий поле salary
на заданный коэффициент (желательно – больший единицы) и print()
, выводящий сведения о сотруднике.

Обращаю ваше внимание на то, что конструкция

эквивалентна

Employee.print = function (self)

Мы просто создаем еще один элемент таблицы Employee
со значением типа «функция». Практически все элементы синтаксиса в представленном фрагменте вам уже знакомы. Новшеством является только выражение типа

Employee:incAge()

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

Employee.incAge(Employee)

Однако такая форма записи более громоздка и не всегда верна: например, она не сработает при использовании полиморфизма.

Оператор: применим не только при вызове, но и при объявлении методов. Например, вместо

Function Employee.print(self)

можно написать

Function Employee:print()

Employee.setName(self, name)

использовать

Employee::setName(name)

Параметр self
для функций будет создан автоматически.

Рассмотрим четыре последних строки программы. Переменной e
присваивается ссылка на объект Employee
, а Employee
устанавливается в nil
. Тут демонстрирует свою полезность параметр self
: будь в методах объекта Employee
зашита ссылка на Employee
, после выполнения операций они перестали бы работать (ведь переменная Employee
будет содержать nil
). Параметр self
позволяет использовать методы объектов, не заботясь об имени переменной, которой присвоена ссылка на объект.

Ну, а как создавать экземпляры объекта Employee
? Для этого задействуем мета-таблицы. Мета-таблицами в Lua
именуются таблицы, описывающие правила обращения с некоторым значением – в том числе с другими таблицами. Вот как может выглядеть мета-таблица для объектов Employee
:

Function Employee:new (name, age, salary, position)
obj = {name = name, age = age, salary = salary, position = position}
setmetatable(obj, self)
self.__index = self
return obj
end

В результате можно будет написать:

E1 = Employee:new(«Vasya Pupkin», 25, 1000, «manager»)
e2 = Employee:new(«Ivan Petrov», 31, 1500, «accountant»)

и убедиться, что вызовы e1:print()
и e2:print()
выдают информацию о двух разных сотрудниках.

Я понимаю, что от синтаксических выкрутасов Lua
вы уже готовы лезть на стену. Но, как говорят католики из Рио-де-Жанейро, «Терпение и труд все перетрут». Сейчас мы все поймем.

Метамагия

Прежде всего, new
– это обычный элемент-функция таблицы Employee
. В ней создается новая таблица obj
с четырьмя элементами, значения которых берутся из параметров функции. Строка

Setmetatable(obj, self)

провозглашает, что Employee
– мета-таблица для таблицы obj
. Теперь при выполнении над obj
нестандартных операций (например, индексации несуществующих элементов) таблица obj
будет неявно обращаться к мета-таблице Employee
за описанием необходимых действий. Еще интереснее строка

Self.__index = self

Она означает, что если при работе с obj
произойдет обращение к элементу, отсутствующему в таблице, Lua
будет искать элемент с соответствующим ключом в мета-таблице. Заметьте, что, создавая таблицу obj
, мы не указывали методов, а значит, вызов

E1:print()

обратится к элементу Employee.print
. Благодаря параметру self
метод Employee.print
будет работать с данными объекта e1
, а не
Employee
. Кстати, теперь присвоение переменной Employee
значения nil
аннулирует методы всех объектов, созданных с помощью Employee:new()
: ведь их описания исчезнут вместе с мета-таблицей. Как вы уже поняли, при работе с объектами мета-таблица играет роль класса. Стало быть, в Lua
можно удалить не только данные объекта, но и код его методов (но вашему преподавателю по C++
об этом молчок).

А можем ли мы написать такое?

Function Employee:new (name, age, salary, position)
obj = {name = name, age = age, salary = salary, position = position, print = self.print}
setmetatable(obj, self)
self.__index = self
return obj
end

Да, можем, и тогда при вызове метода print()
объекту obj
не придется обращаться к мета-таблице. Но наш код потеряет гибкость. Если в ходе выполнения программы описание метода print()
в мета-таблице изменится, ранее созданные объекты об этом не узнают: ведь у них уже есть свое поле print
, и обращаться к мета-таблице им незачем. Можно, наоборот, полностью перенести описание объекта (не только методов, но и полей) в мета-таблицу. Для этого перепишем функцию Employee:new()
так:

Function Employee:new (obj)
obj = obj or {}
setmetatable(obj, self)
self.__index = self
return obj
end

Тогда синтаксис вызова функции Employee:new()
тоже изменится:

E1 = Employee:new{name = «Vasya Pupkin», age = 25, salary = 1000, position = «manager»}

Обратите внимание на скобки. Этот вариант кажется неудобным: по сути, объект obj
конструируется «вручную» и приходится явно указывать имена полей, уже определенных в мета-таблице. Зато легко организовать наследование классов. Пусть нужно создать объект-потомок класса Employee
с переопределенным методом print()
. Вот что для этого требуется:

Function newPrint(self)
print(«name: «..self.name..» age: «..self.age..» salary: «..self.salary..»position: «..self.position)
end
e3 = Employee:new{name = «Ivan Sidorov», age = 20, salary = 800, position=»security manager», print = newPrint}
e3:print()

Создавая объект e3
, мы заменяем функцию Employee:print()
на newPrint()
. В результате при вызове e3:print()
на самом деле будет вызвана функция newPrint()
– одним махом мы получаем не только наследование, но и, в некотором смысле, полиморфизм. Тем же способом можно добавлять в объекты-потомки Employee
новые поля данных и методы, не меняя описания мета-таблицы Employee
.

Мы подошли к важной мысли: скрипты Lua
способны само-модифицироваться, а значит, быть самообучаемыми! Я не упоминал об этом достоинстве Lua
– не буду врать, что просто забыл, скажу честно: новичкам, не прошедшим вторую стадию посвящения, знать о таком было рано. А что же дальше-то будет?! LXF

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

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

Язык Lua является свободно распространяемым (лицензия MIT), с открытыми исходными кодами на С. Это интерпретируемый язык, а значит, программы, написанные на нем, преобразуются в байт-код непосредственно во время исполнения, без предварительной компиляции. Этот байт-код исполняется на виртуальной машине (представляющей из себя абстрактный виртуальный процессор с определенным набором регистров), что обеспечивает отличную кроссплатформность. Аналогично .Net и Java, виртуальная машина Lua содержит сборщик мусора. Цена такого подхода — ухудшение скорости работы и компактности хранимого кода, однако оба эти недостатка в Lua сведены к минимуму. Несмотря на то, что язык носит процедурную направленность и не поддерживает принципы объектно-ориентированного программирования, некоторые возможности ООП (например, наследование) вполне реализуются стандартными средствами языка.

С самого начала Lua предназначался для использования не самостоятельно, а совместно с языками более низкого уровня, такими как С. Малый размер интерпретатора, относительно высокая скорость исполнения и легкая расширяемость позволили этому языку найти применение в сфере разработки компьютерных игр (в 2003 г. Lua был признан самым популярным скриптовым языком для разработки игр по версии сообщества GameDev.net). Также Lua активно используется для написания расширений и плагинов к программным продуктам, при создании пользовательских интерфейсов и в конфигурационных файлах. На данный момент язык Lua лежит в основе Codea — единственной среды разработки для iPad, позволяющей создавать приложения на самом устройстве.

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

О проекте eLua

Проект eLua (Embedded Lua) позволяет использовать полноценную поддержку языка Lua во встраиваемых системах. Проект разрабатывается совместно Богданом Маринеску, программистом из Румынии, и Дадо Саттером, главой Led Lab в бразильском университете PUC-Rio. Как и сам язык, проект eLua является свободным, открытым и распространяется по лицензии MIT. Этот продукт работает как самостоятельное приложение, без необходимости использования операционных систем. В случае необходимости многопоточность обеспечивается стандартными средствами скриптового языка.

Однако eLua — это не только интерпретатор. Благодаря модульной структуре это еще и множество полезных компонентов, серьезно расширяющих его функционал. На сегодня в eLua версии 0.8 присутствуют следующие полезные компоненты:

  • консоль через UART или Ethernet с возможностью передачи файлов исходных кодов через Xmodem;
  • поддержка файловых систем (виртуальной ROMFS, FAT на SD/MMC-картах либо удаленной NFS);
  • DHCP-клиент и DNS-resolver, а также мультиплексирование последовательных портов.

Набор используемых компонентов задается при компиляции проекта и может быть скорректирован для снижения объема кода.

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

Для лучшего понимания принципов взаимодействия eLua с «внешним миром» стоит остановить внимание на таком понятии, как модуль. С точки зрения Lua-программиста модуль представляет собой таблицу в глобальном окружении, полями которой являются определенные функции и переменные, объединенные по смысловому принципу. В какой-то степени аналогом понятия модуль является namespace в C++. Определяемые пользователем модули можно использовать для разделения большой программы на логические части для «эмуляции» некоторых принципов объектно-ориентированного программирования и для вынесения вовне обособленных частей кода, отвечающих за определенные функции.

Таблица. Платформы, поддерживаемые eLua

Платформа Модели микропроцессоров
Cortex-M3 LM3S8962, LM3S6965, LM3S6918,
LM3S9B92, LM3S1968
LPC1768
STM32F103ZE, STM32F103RE
AVR32 AT32UC3A0512, AT32UC3B0256, AT32UC3A0512
ARM7TDMI AT91SAM7X256, AT91SAM7X512
LPC2468, LPC2888
STR711FR2
ARM966E-S STR912FAW44
х86 Эмуляция на i386

Последнее широко используется в eLua. В виде модулей представлены все функции, связывающие интерпретатор с периферией. В версии 0.8 можно обнаружить следующие модули:

  • pio — доступ к портам ввода/вывода;
  • tmr — доступ к физическим и виртуальным таймерам;
  • pwm — доступ к ШИМ;
  • uart — доступ к последовательным портам;
  • spi — доступ к интерфейсу SPI;
  • net — поддержка сети TCP/IP;
  • adc — доступ к АЦП;
  • cpu — низкоуровневый доступ к регистрам процессора;
  • pd — получение информации о платформе;
  • term — доступ к терминалу;
  • bit — битовые операции;
  • pack — упаковка/распаковка бинарных данных в строки;
  • i2c — доступ к интерфейсу I2C;
  • can — доступ к интерфейсу CAN;
  • rpc — удаленный вызов процедур;
  • elua — управление поведением системы.

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

Например, чтобы отправить данные через UART с помощью одноименного модуля, достаточно в любой части Lua-скрипта вызвать функцию:

uart.write(ID, Data1, [Data2], ѕ, [DataN])

где ID — идентификатор нужного последовательного порта; Data1–DataN — данные для передачи, в качестве которых может выступать строка либо восьмибитное число.

Синтаксис и общие принципы напоминают доступ к методу объекта в объектно-ориентированных языках, с той разницей, что таблица uart всегда присутствует в глобальном окружении без необходимости создания каких-либо переменных, объектов и т. п. Это облегчает работу с модулями и в целом упрощает использование скриптового языка Lua.

Возможности применения eLua

Проект eLua может использоваться на многих аппаратных платформах и архитектурах, как на базе микроконтроллеров и микропроцессоров, так и в виде приложений для ПК x86. В таблице представлен список платформ, поддерживаемых eLua.

Рекомендуемые минимальные требования к платформе: 32-разрядный CPU, не менее 256 кбайт flash-памяти и не менее 64 кбайт RAM. Точные требования зависят от набора инструкций конкретного процессора и используемого набора расширений.

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

Можно выделить три варианта использования eLua:

  • использование готовых образов;
  • сборка образа с помощью Web Builder;
  • сборка образа из исходных кодов.

На своем сайте [1] авторы проекта предоставляют готовые для использования образы последней стабильной версии eLua для наиболее распространенных комплектов разработчика. Это позволяет максимально упростить процесс первоначального ознакомления с системой.

Если необходимо внести какие-либо изменения в настройки готового образа, можно воспользоваться сервисом eLua Web Builder, он доступен на официальном сайте проекта после регистрации. Этот сервис позволяет выполнить компиляцию eLua с заданными настройками на стороне веб-сервиса, получив на выходе готовый образ и его исходный код с указанными настройками.

Но наибольший интерес, конечно, представляет самостоятельная сборка eLua из исходных кодов. Код eLua доступен на сайте GitHub по адресу [2] в виде готового для кросс-компиляции проекта. Проект написан на языке С, часть кода инициализации для конкретных платформ написана на ассемблере. Также при компиляции проекта используются Python-скрипты.

Множество полезных описаний и руководств, которые помогут в настройке, использовании и написании расширений для eLua, можно найти на сайте проекта.

Пример использования eLua

Рассмотрим пример использования eLua с комплектом разработчика EK-LM3S9B92 на базе микроконтроллера Stellaris компании Texas Instruments. Микроконтроллер lm3s9b92 имеет на борту 256 кбайт flash-памяти и 96 кбайт RAM, чего вполне хватит для использования всех возможностей eLua, доступных для этой платформы.

Для повторения данного примера понадобится следующее:

  • Сама плата с целевым 32-разрядным микроконтроллером (рис. 1) (в примере использована EK-LM3S9B92 от TI) и внутрисхемный отладчик для прошивки контроллера (рис. 2).

    Оценочная плата EK-LM3S9B92 Texas Instruments

    Рис. 1. Оценочная плата EK-LM3S9B92 Texas Instruments

    Интерфейсная плата для внутрисхемной отладки

    Рис. 2. Интерфейсная плата для внутрисхемной отладки

  • ПК под управлением любой операционной системы семейства Linux (в примере использована ОС Ubuntu 12.04).
  • Установленный ARM toolchain (в примере использован бесплатный пакет Sourcery CodeBench Lite Edition).
  • Установленные пакеты SCons и Python.

Для начала загрузим исходные коды последней стабильной версии eLua (на момент подготовки статьи — 0.8). Это можно сделать с помощью Git:

git clone git://github.com/elua/elua.git

Кроме того, исходные коды можно загрузить в виде .zip либо .gzip архива через веб-интерфейс ветки проекта eLua на GitHub [2]. Так или иначе, мы получим папку с проектом eLua. Взглянем на ее содержимое. Основная часть кода расположена в папке src. Наибольший интерес представляет содержимое src/platform, это платформозависимые модули для различных микроконтроллеров (код инициализации на ассемблере, функции для работы с периферией и т. д.).

В папке romfs содержатся Lua-скрипты, которые будут включены в создаваемый бинарный образ проекта и будут доступны для запуска через встроенную файловую систему read-only. Если в этой папке будет обнаружен скрипт с именем autorun.lua, он будет автоматически запущен после загрузки системы.

В корне папки проекта находится файл Sconstruct, который содержит все параметры сборки eLua (используемый для конфигурирования язык — Python), включая указание ссылок на установленный набор инструментов ARM toolchain (массив toolchain_list) и список отношений платформа–контроллер–плата для поддерживаемых платформ (platform_list и board_list). Поскольку используемый микроконтроллер (lm3s9b92 от TI) и toolchain (Sourcery CodeBench Lite) изначально поддерживаются eLua, никаких изменений в этот файл вносить не будем.

Теперь сконфигурируем eLua для платформы lm3s. Для этого откроем файл /src/platform/lm3s/platform_conf.h — именно здесь описан список компонентов и модулей, которые будут включены в проект.

Используемые компоненты заданы с помощью директив #define. В данном примере понадобится поддержка сети (стек uIP), telnet-консоль и ROM FS. Закомментируем все директивы #define BUILD_*, кроме:

#define BUILD_SHELL
#define BUILD_ROMFS
#define BUILD_UIP
#define BUILD_CON_TCP

Список подключаемых модулей (то есть расширений языка Lua) задает #define-секция LUA_PLATFORM_LIBS_ROM. По умолчанию все поддерживаемые модули включены и будут добавлены в проект. Те из них, которые мы не планируем использовать, можно отключить для уменьшения объема занимаемой flash-памяти. Поскольку перед нами сейчас не стоит задача экономии, оставим эту директиву без изменений.

Мы не будем использовать получение IP-адреса через DHCP (который включен по умолчанию и был отключен нами ранее), нам необходимо указать параметры сети вручную. Для этого в секции Static TCP/IP configuration файла platform_conf.h укажем IP-адрес 192.168.1.100. Маску подсети, адреса шлюза по умолчанию и DNS сервера можно оставить без изменения.

Подготовим тестовый скрипт. Создадим в папке romfs файл hello.lua следующего содержания:

Теперь все готово для окончательной сборки бинарного образа eLua. Здесь используется утилита сборки Scons со следующими параметрами:

scons [target=lua | lualong | lualonglong] [cpu=<target_mc>] [board=<taget_board>] [cpumode=arm | thumb] [allocator = newlib | multiple | simple] [toolchain = <toolchain>] [optram = 0 | 1] [romfs = verbatim | compress | compile] [prog]

Не будем подробно останавливаться на описании каждого параметра, поскольку подробная информация доступна в соответствующей документации [3]. В нашем случае команда сборки будет выглядеть так:

scons cpu=lm3s9b92 board=ek-lm3s9b92 toolchain=codesourcery prog

Процесс сборки займет некоторое время, после чего, в случае отсутствия ошибок, мы получим файл вида elua_lua_lm3s9b92.bin в корневой папке проекта. Данный бинарный образ готов к записи во flash-память используемой платы, что мы и делаем.

После прошивки подключим нашу плату к сети Ethernet и попробуем получить доступ ко встроенной консоли через telnet:

В случае использования DNS-resolving нет необходимости в знании IP-адреса платы, и достаточно выполнить:

Если мы все сделали верно на предыдущих этапах, перед нами появится консоль (рис. 3).

Консоль eLua

Рис. 3. Консоль eLua

Список доступных команд не велик, но позволяет выполнять все необходимые нам операции:

  • help — вывод в консоль списка доступных команд;
  • ver — отображение версии eLua;
  • recv — получение файла через Xmodem (только для UART);
  • lua <имя файла> либо lua -e ‘команда’ -i — запуск скрипта на выполнение;
  • ls либо dir — отображение списка файлов в файловых системах eLua;
  • cat либо type <имя файла> — вывод содержимого файла в консоль;
  • cp <источник> <назначение> — копирование файлов в пределах eLua;
  • exit — закрытие telnet-соединения и выход из консоли.

При использовании консоли через UART работа с ней осуществляется аналогичным образом. Параметры порта: 115200 (38400 для STR7), 8N1. Возможна сборка eLua только с одним из способов доступа к консоли (через UART либо через Ethernet), готовые образы на сайте проекта собраны с использованием консоли по UART.

Пришло время проверить работу интерпретатора, запустив тестовый скрипт hello.lua. Для этого введем в консоль eLua следующую команду:

В результате выполнения скрипта в консоль будет выведено приветствие интерпретатора и заветная строчка “Hello world!”. Такого же результата можно добиться, выполнив:

lua -e ‘print(“Hello world!”)’

Существует множество различных примеров Lua-скриптов для eLua с использованием различных модулей, от мигания светодиодами до веб-сервера. Эти примеры помогут в минимальные сроки освоить синтаксис языка и работу с периферией.

В целом eLua является довольно неплохой средой для исполнения скриптов во встраиваемых системах, но далеко не единственной. Как альтернативу можно рассмотреть интерпретатор PyMite, использующий язык Python. При необходимости совсем легковесного решения можно порекомендовать С-подобный язык Pawn с минимальным размером интерпретатора. Ну и, конечно же, нельзя не вспомнить гораздо более популярное решение для схожих задач — Java, оно обладает более обширными возможностями, но и проявляет бóльшую требовательность к ресурсам.

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

Так или иначе, использование скриптовых языков во встраиваемых системах позволяет решать широкий круг задач, и с развитием элементной базы популярность таких решений только увеличивается.

Литература

  1. http://www.eluaproject.net
  2. Git-репозиторий проекта git://github.com/elua/elua.git
  3. Официальная документация проекта eLua http://www.eluaproject.net/doc
  4. Примеры программ, библиотеки, описание готовых проектов http://wiki.eluaproject.net/

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

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

Переменные используются для хранения значений в процессе выполнения скрипта.

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

Обратите внимание

Язык Lua различает регистр символов, поэтому abc, Abc, ABC являются различными именами.

В таблице ниже приведены слова, которые зарезервированы языком Lua и не могут использоваться в именах переменных:

and break do else elseif

end false for function if

in local nil not or

repeat return then true until

while

Кроме того, все имена, начинающиеся с символа подчеркивания, за которым идут заглавные буквы (например, _VERSION) также являются зарезервированными.

Какие переменные бывают в Lua?

Переменные в Lua могут быть глобальными и локальными. Если переменная не объявлена явно как локальная, она считается глобальной.

Глобальные переменные Lua

Глобальная переменная появляется в момент присваивания ей первого значения. До присваивания первого значения обращение к глобальной переменной даёт nil.

MsgBox(tostring (g))  —> nil

g = 1

MsgBox(tostring (g))  —> 1

Глобальная переменная существует до тех пор, пока существует среда исполнения скрипта и доступна любому Lua-коду, выполняемому в этой среде.

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

g = 1 — создаем глобальную переменную g со значением 1

g = nil — удаляем глобальную переменную g

MsgBox(tostring (g)) —> nil

Все глобальные переменные являются полями обычной таблицы, называемой глобальным окружением. Эта таблица доступна через глобальную переменную _G. Поскольку полями глобального окружения являются все глобальные переменные (включая саму _G), то _G._G == _G.

Локальные переменные Lua

Любые локальные переменные должны быть объявлены явно с использованием ключевого слова local. Объявить локальную переменную можно в любом месте скрипта. Объявление может включать в себя присваивание переменной начального значения. Если значение не присвоено, переменная содержит nil.

local a — объявляем локальную переменную a

local b = 1 — объявляем локальную переменную b, присваиваем ей значение 1

local c, d = 2, 3 — объявляем локальные переменные c и d, присваиваем им значения 2 и 3

Область видимости локальной переменной начинается после объявления и продолжается до конца блока.

Примечание

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

Под блоком понимается:

тело управляющей конструкции (if-then, else, for, while, repeat);

тело функции;

фрагмент кода, заключённый в ключевые слова do…end.

Если локальная переменная определена вне какого-либо блока, её область видимости распространяется до конца скрипта.

a = 5 — глобальная переменная a

local i = 1  — переменная i локальна в пределах скрипта

   while i <= a do — цикл от 1 до 5

   local a = i^2 — переменная а локальна внутри цикла while

   MsgBox(a) —> 1, 4, 9, 16, 25

   i = i + 1

end

MsgBox(a) —> 5 (здесь обращение к глобальной a)

if i > 5 then

   local a — переменная а локальна внутри then

   a = 10

   MsgBox(a) —> 10

else

   MsgBox(a) —> 5 (здесь обращение к глобальной a)

end

do

   local a = 20 — переменная а локальна внутри do-end

   MsgBox(a) —> 20

end

MsgBox(a) —> 5 (здесь обращение к глобальной a)

Обратите внимание

Когда возможно, рекомендуется использовать локальные переменные вместо глобальных. Это позволит избежать «засорения» глобального пространства имён и обеспечит лучшую производительность (поскольку доступ к локальным переменным в Lua выполняется несколько быстрее, чем к глобальным).

Типы данных Lua

Какие типы данных поддерживает язык Lua?

Lua поддерживает следующие типы данных:

1. Nil (ничего). Соответствует отсутствию у переменной значения. Этот тип представлен единственным значением — nil.

2. Boolean (логический). К данному типу относятся значения false (ложь) и true (истина).

При выполнении логических операций значение nil рассматривается как false. Все остальные значения, включая число 0 и пустую строку, рассматриваются как true.

3. Number (числовой). Служит для представления числовых значений.

В числовых константах можно указывать необязательную дробную часть и необязательный десятичный порядок, задаваемый символами «e» или «E». Целочисленные числовые константы можно задавать в шестнадцатеричной системе, используя префикс 0x.

Примеры допустимых числовых констант: 3, 3.0, 3.1415926, 314.16e-2, 0xff.

4. String (строковый). Служит для представления строк.

Строковые значения задаются в виде последовательности символов, заключённой в одинарные или двойные кавычки:

a = «это строка»

b = ‘это вторая строка’

Строки, заключённые в двойные кавычки, могут интерпретировать C-подобные управляющие последовательности (escape-последовательности), начинающиеся с символа «» (обратный слэш):

b (пробел),

n (перевод строки),

 r (возврат каретки);

t (горизонтальная табуляция),

 \ (обратный слеш);

» (двойная кавычка);

‘ (одинарная кавычка).

Обратите внимание

Символ в строке также может быть представлен своим кодом с помощью escape-последовательности:

ddd,

где ddd — последовательность из не более чем трёх цифр.

Кроме кавычек для определения строки могут также использоваться двойные квадратные скобки:

local a = [[Компания «Кронос»]]

Определение строки с помощью двойных квадратных скобок позволяет игнорировать все escape-последовательности, т. е. строка создаётся полностью так, как описана:

local a = [[string

string1

string2

   string3

]] — «string

       string1

       string2

          string3»

Примечание

При определении строки с помощью двойных квадратных скобок учитываются символы табуляции и переноса.

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

local a = [=[определение строки [[string]] в Lua]=]

— будет срока: «определение строки [[string]] в Lua»

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

6. Table (таблица). Таблица представляет собой набор пар «ключ» — «значение», которые называют полями илиэлементами таблицы. Как ключи, так и значения полей таблицы могут иметь любой тип, за исключением nil. Таблицы не имеют фиксированного размера: в любой момент времени в них можно добавить произвольное число элементов.

Подробнее — в статье «Создание таблиц в Lua»

7. Userdata (пользовательские данные). Является особым типом данных. Значения этого типа не могут быть созданы или изменены непосредственно в Lua-скрипте.

Userdata используется для представления новых типов, созданных в вызывающей скрипт программе или в библиотеках, написанных на языке С. Например, библиотеки расширений Lua для «CronosPRO» используют этот тип для представления таких объектов, как:

банки данных (класс Bank);

базы данных (класс Base);

записи (класс Record) и т. п.

8. Thread (поток). Соответствует потоку выполнения. Эти потоки никаким образом не связаны с операционной системой и поддерживаются исключительно средствами самого Lua.

Как в Lua задать тип переменной?

Lua не предусматривает явного задания типа переменной. Тип переменной устанавливается в момент присвоения переменной значения. Любой переменной может быть присвоено значение любого типа (вне зависимости от того, значение какого типа она содержала ранее).

a = 123 — переменная a имеет тип number

a = «123» — теперь переменная a имеет тип string

a = true — теперь переменная a имеет тип boolean

a = {} — теперь переменная a имеет тип table

Обратите внимание

Переменные типа table, function, thread и userdata не содержат самих данных, а хранят ссылки на соответствующие объекты. При присваивании, передачи в функцию в качестве аргумента и возвращении из функции в качестве результата копирования объектов не происходит, копируются только ссылки на них.

a = {} — создаем таблицу. В переменную a помещается ссылка на таблицу

b = a — переменная b ссылается на ту же таблицу, что и a

a[1] = 10 — элементу таблицы с индексом 1 присвоено значение 10

MsgBox(b[1]) —> ’10’

b[1] = 20

MsgBox(a[1]) —> ’20’

Остальные данные являются непосредственными значениями.

a = 10

b = a

a = 20

MsgBox(a) —> ’20’

MsgBox(b) —> ’10’

Как в Lua получить тип переменной?

Тип значения, сохранённого в переменной, можно выяснить при помощи стандартной функции type. Эта функция возвращает строку, содержащую название типа («nil», «number», «string», «boolean», «table», «function», «thread», «userdata»).

t = type («это строка») — t равно «string»

t = type (123) — t равно «number»

t = type (type) — t равно «function»

t = type (true) — t равно «boolean»

t = type (nil) — t равно «nil»

t = type (CroApp.GetBank()) — t равно «userdata»

Как в Lua преобразовать тип переменной?

Lua при необходимости автоматически преобразует числа в строки и наоборот. Например, если строковое значение является операндом в арифметической операции, оно преобразуется в число. Аналогично числовое значение, встретившееся в том месте, где ожидается строковое, будет преобразовано в строку.

a = «10» + 2 — a равно 12

a = «10» + 2 — a равно «10 + 2»

a = «-5.3e-10″*«2» — a равно -1.06e-09

a = «строка» + 2 — Ошибка! Невозможно преобразовать «строка» в число

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

a = tostring (10) — a равно «10»

a = tostring (true) — a равно «true»

a = tostring (nil) — a равно «nil»

a = tostring ({[1] = «это поле 1»}) — a равно «table: 06DB1058»

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

a = render (10) — a равно «10»

a = render (true) — a равно «true»

a = render (nil) — a равно «nil»

a = render ({[1] = «это поле 1»}) — a равно «{[1] = «это поле 1»}»

Для явного преобразования значения в число можно использовать стандартную функцию tonumber. Если значение является строкой, которую можно преобразовать в число (или уже является числом), функция возвращает результат преобразования, в противном случае возвращает nil.

a = tonumber («10») — a равно «10»

a = tonumber («10»..».5″) — a равно 10.5

a = tonumber (true) — a равно «nil»

a = tonumber (nil) — a равно «nil»

Расстановка комментариев в Lua

Комментарий в Lua начинается двумя знаками «минус» (—) и продолжается до конца строки.

local a = 1 — однострочный комментарий

Если непосредственно после символов «—» идут две открывающие квадратные скобки ([[), комментарий являетсямногострочным и продолжается до двух закрывающих квадратных скобок (]]).

local a = 1 — [[ многострочный

комментарий ]]

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

local a = [[Компания «Кронос»]] — [=[

local a = [[Компания «Кронос»]]

]=]

Количество символов «=» определяет вложенность:

local a = [=[определение некоторой строки [[string]] в языке Lua]=] —[==[

local a = [=[определение некоторой строки [[string]] в языке Lua]=]

]==]

Операции, применяемые в Lua

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

1. Арифметические операции.

Lua поддерживает следующие арифметические операции:

+ (сложение);

— (вычитание);

* (умножение);

/ (деление);

^ (возведение в степень);

% (остаток от деления).

Обратите внимание

Арифметические операции применимы как к числам, так и к строкам, которые в этом случае преобразуются в числа.

2. Операции сравнения.

В Lua допустимы следующие операции сравнения величин:

== (равно);

~= (не равно);

< (меньше);

> (больше);

<= (меньше или равно);

>= (больше или равно).

Обратите внимание

Операции сравнения всегда возвращают логическое значение true или false.

Правила преобразования чисел в строки (и наоборот) при сравнениях не работают, т. е. выражение «0» == 0 даёт в результате false.

3. Логические операции.

К логическим операциям относятся:

and (логическое И).

Операция and возвращает свой первый операнд, если он имеет значение false или nil. В противном случае, операция возвращает второй операнд (причём этот операнд может быть произвольного типа).

a = (nil and 5) — a равно nil

a == (false and 5) — a равно false

a == (4 and 5) — a равно 5

or (логическое ИЛИ).

Операция or возвращает первый операнд, если он не false и не nil, иначе он возвращает второй операнд.

a == (4 or 5) — a равно 4

a == (false or 5) — a равно 5

Обратите внимание

Логические операции and и or могут возвращать значения любых типов.

Логические операции and и or вычисляют значение второго операнда только в том случае, если его нужно вернуть. Если этого не требуется, второй операнд не вычисляется. Например:

a == (4 or f()) — вызова функции f() не произойдет

not (логическое НЕ).

Операция not всегда возвращает true или false.

4. Операция конкатенации.

Для конкатенации (объединения) строк служит операция… (две точки).

a = «Кронос»..»-«..«Информ» — переменная a получит значение «Кронос-Информ»

Обратите внимание

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

a = 0..1 — переменная a получит значение «01»

5. Операция получения длины.

В Lua определена операция длины #, которую можно использовать для получения длины строки.

a = «строка»

len = #a — len равно 6

len = #«ещё строка» — len равно 10

Обратите внимание

С помощью операции # можно также узнать максимальный индекс (или размер) массива. Подробнее — в статье «Работа с массивами в Lua» .

Приоритет операций в Lua

В языке Lua выполнение операций осуществляется в соответствии со следующим приоритетом (в порядке убывания):

1. ^

2. not # — (унарный)

3. * / %

4. + —

5. ..

6. < > <= >= ~= ==

7. and

8. or

 Вызов скриптов из форм

С каждой формой (включая вложенные формы) связан отдельный скрипт, который обычно содержит функции, выполняющие обработку событий формы и её элементов.

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

Необходимо отметить, что скрипт формы, хотя и не содержит вызова функции module, фактически является модулем. Это означает, что переменные, объявленные в скрипте формы без ключевого слова local, не выносятся в глобальное окружение и доступны только внутри этого скрипта. Если необходимо сделать какое-либо значение доступным для скриптов других форм, его следует явным образом определить в глобальной таблице _G:

_G.var = 123

Другой скрипт форм сможет прочитать это значение следующим образом:

local a = _G.var

Блоки операторов (инструкций)

К основным операторам Lua относятся:

присваивание;

условный оператор;

операторы для организации циклов.

Группа операторов может быть объединена в блок (составной оператор) при помощи конструкции do… end.

do — начало блока

   <оператор1> — тело блока

   <оператор2>

   …

   <операторN>

end — конец блока

Блок открывает новую область видимости, в которой можно определять локальные переменные.

a = 5 — глобальная переменная a

do

   local a = 20 — внутри do-end определяется локальная переменная а

   MsgBox(a) —> 20

end

MsgBox(a) —> 5 (здесь обращение уже к глобальной a)

Оператор присваивания в Lua

Присваивание изменяет значение переменной или поля таблицы. В простейшем виде присваивание может выглядеть так:

a = 1 — переменной a присвоено значение 1

a = b + c — переменной a присвоена сумма значений переменных b и с

a = f(x) — переменной a присвоено значение, возвращённое функцией f(x)

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

a, b = 1, 5*c — a равно 1; b равно 5*c

Если переменных больше чем значений, «лишним» переменным присваивается nil.

a, b, c = 1, 2 — a равно 1; b равно 2; c равно nil

Если значений больше чем переменных, «лишние» значения игнорируются.

a, b = 1, 2, 3 — a равно 1; b равно 2; значение 3 не использовано

Множественное присваивание можно использовать для обмена значениями между переменными:

a = 10; b = 20 — a равно 10, b равно 20

a, b = b, a — теперь a равно 20, b равно 10

Условный оператор (if) в Lua

Оператор if проверяет истинность заданного условия. Если условие является истинным, выполняется часть кода, следующая за ключевым словом then (секция then). В противном случае, выполняется код, следующий за ключевым словом else (секция else).

if a > b then

   return a — если a больше b, вернуть a

else

   return b — в противном случае — вернуть b

end

Секция else является необязательной.

if a < 0 then

   a = 0 — если a меньше 0, присвоить a значение 0

end

Вместо вложенных операторов if можно использовать конструкцию elseif. Например, приведенный код:

if a == 1 then

   return «Иван» — если a равно 1

else

   if a == 2 then

     return «Петр» — если a равно 2

   else

      if a == 3 then

         return «Сергей» — если a равно 3

      else

         return «Нет такого игрока» — если a — ни одно из перечисленных

      end

   end

end

будет проще для восприятия, если заменить его следующим:

if a == 1 then

   return «Иван» — если a равно 1

elseif a == 2 then

   return «Петр» — если a равно 2

elseif a == 3 then

   return «Сергей» — если a равно 3

else

   return «Нет такого игрока» — если a — ни одно из перечисленных

end

Цикл с предусловием (while) в Lua

Оператор while предназначен для организации циклов с предусловием и имеет следующий вид:

while <condition> do

   … — тело цикла

end

Перед каждой итерацией цикла проверяется условие <condition>:

если условие ложно, цикл завершается и управление передаётся первому оператору, следующему за оператором while;

если условие истинно, выполняется тело цикла, после чего все действия повторяются.

i = 10; t = {}

while i > 0 do — цикл от 10 до 1

   t[i] = «поле »..i

   i = i — 1

end

Для выхода из цикла до его завершения можно использовать оператор break.

a = {3, 5, 8, -6, 5}

i = #a

while i > 0 do — ищем в массиве отрицательное значение

   if a[i] < 0 then break end — если найдено, прерываем цикл

   i = i — 1 — иначе переходим к следующему элементу

end

if i > 0 then

   MsgBox («Индекс отрицательного значения: »..i)

else

   MsgBox («Массив не содержит отрицательных значений»)

end

Примечание

Подробнее об особенностях использования оператора break — в статье «Операторы break и return»

Цикл с постусловием (repeat) в Lua

Оператор repeat предназначен для организации циклов с постусловием и имеет следующий вид:

repeat

   … — тело цикла

until <condition>

Тело цикла выполняется до тех пор, пока условие <condition> не станет истинным. Проверка условия осуществляется после выполнения тела цикла, поэтому в любом случае тело цикла выполнится хотя бы один раз.

— суммируем значения массива a, пока сумма не превысит 10

a = {3, 2, 5, 7, 9}

i = 0; sum = 0

repeat

   i = i + 1

   sum = sum + a[i]

until sum > 10

MsgBox («Сложено »..i..» элементов. Сумма равна «..sum)

Для выхода из цикла до его завершения можно использовать оператор break.

Примечание

Подробнее об особенностях использования оператора break — в статье «Операторы break и return»

Циклы с оператором for в Lua

Оператор for предназначен для организации циклов и допускает две формы записи:

простую (числовой for);

расширенную (универсальный for).

Простая форма оператора for

Простая форма оператора for имеет следующий вид:

for var = exp1, exp2, exp3 do

   … — тело цикла

end

Тело цикла выполняется для каждого значения переменной цикла (счётчика) var в интервале от exp1 до exp2, с шагом exp3.

Примечание

Шаг может не задаваться. В этом случае он принимается равным 1.

for i = 1, 10 do — цикл от 1 до 10 с шагом 1

MsgBox («i равно »..i)

end

for i = 10, 1, -1 do — цикл от 10 до 1 с шагом -1

   MsgBox («i равно »..i)

end

Обратите внимание

Выражения exp1, exp2 и exp3 вычисляются всего один раз, перед началом цикла. Так, в примере ниже, функция f(x) будет вызвана для вычисления верхнего предела цикла только один раз:

for i = 1, f(x) do — цикл от 1 до значения, возвращенного функцией f()

MsgBox («i равно »..i)

end

Переменная цикла является локальной для оператора цикла и по его окончании не определена.

for i = 1, 10 do — цикл от 1 до значения, возвращенного функцией f()

   MsgBox («i равно »..i)

end

MsgBox («После выхода из цикла i равно »..i) — Неверно! i равно nil

Обратите внимание

Значение переменной цикла нельзя изменять внутри цикла: последствия такого изменения непредсказуемы.

Для выхода из цикла до его завершения используется оператор break.

a = {3, 5, 8, -6, 5}

for i = 1,#a do — ищем в массиве отрицательное значение

   if a[i] < 0 then — если найдено…

      index = i — сохраняем индекс найденного значения…

      break — и прерываем цикл

   end

end

MsgBox («Индекс отрицательного значения: »..index)

Примечание

Подробнее об особенностях использования оператора break — в статье «Операторы break и return»    )

нужен.

Встраиваемые языки: почему Lua? +38

Программирование, Lua, Блог компании Social Quantum


Рекомендация: подборка платных и бесплатных курсов Smm — https://katalog-kursov.ru/

Этот материал продолжает серию публикаций, основанных на докладах, которые мы сделали на конференции Games Gathering 2017 в декабре прошлого года. В одном из докладов была затронута тема выбора встраиваемого скриптового языка.

Что такое и зачем нужны скриптовые языки

Как уже упоминалось в предыдущем посте нашего блога, в нашей компании написан собственный движок. Сегодня речь пойдёт о том, чем мы руководствовались во время выбора скриптового языка для этого движка.

Почему вообще возникает потребность в скриптовых языках? Как говорится, «в игровой индустрии нет сложных проблем, незачем требовать сложных решений» . Помимо опытных (и дорогих!) программистов, решающих сложные задачи, нам нужны люди (много людей!), которые будут заниматься, скажем, квестами. И, если честно, нам бы хотелось, чтоб эти программисты были не такие дорогие, а в идеале и вовсе не программисты, а непосредственно геймдизайнеры и сценаристы.

Таким образом, есть потребность в средстве для описания несложной, но всё-таки логики, без привлечения тяжёлой артиллерии программистов. Сделаем вывод — что такое для нас скриптовый язык? Это средство, которое позволит сделать разработку игр быстрее и дешевле.
Сразу возникает вопрос, а почему бы нам просто не использовать что-то вроде XML? Дело в том, что для наших целей нам часто нужны управляющие конструкции — ветвление и циклы, в то время как XML это декларативное описание.

Ещё одно преимущество скриптовых языков в том, что скрипты в проекте могут быть как кодом, так и ресурсом. И, соответственно, обновлять скриптовую часть игры можно не только вместе с кодом, то есть в ходе обычных обновлений через механизмы магазинов приложений. Но и вместе с ресурсами — то есть вместе с графическими и прочими материалами, с использованием CDN.

Требования к идеальному скриптовому языку

Сформулируем требования к идеальному скриптовому языку.

  • Динамический. В нашем понимании идеальный скриптовый язык должен быть динамическим.
  • Популярность. Под популярностью языка мы понимаем наличие у него достаточно большого сообщества, готового отвечать на вопросы на специализированных ресурсах наподобие StackOverflow.
  • Пологая кривая обучения. Мы хотим взять, условно говоря, практически любого человека, и быстро обучить его до уровня, который позволит этому человеку продуктивно работать над нашими задачами.
  • Широкие возможности. Язык должен быть мощным и обладать достаточно широкими возможностями, должен поддерживать разные парадигмы программирования. Профессиональный программист, которому предложат писать на таком языке, сможет делать это с комфортом и с удовольствием.
  • Высокая производительность. Производительность — это один из краеугольных камней игровой индустрии.
  • Большое количество библиотек. Очень часто мы, в ходе решения встающих перед нами задач, не создаём принципиально новый код, а пользуемся тем, что уже кто-то написал. Чем больше стабильных, хорошо поддерживаемых библиотек мы можем задействовать, применяя некий язык — тем лучше.
  • Лёгкость встраивания. Речь идёт о встраиваемых языках, поэтому при выборе скриптового языка возможность его встраивания играет важную роль.

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

Python

Python — динамический язык, который пользуется немалой популярностью. Он характеризуется достаточно пологой кривой обучения, его довольно просто выучить. Однако изучить его как следует уже не так-то просто. Как результат, хорошие Python-программисты встречаются редко и дорого стоят. Это противоречит нашему желанию ускорить и удешевить разработку игровой логики.

Python обладает широкими возможностями, отличается хорошей производительностью. Его проблемой является неконсистентная система библиотек. Ещё одна его проблема, которая играет для нас важную роль, заключается в том, что он, на самом деле, не является встраиваемым языком. Это язык, из которого удобно вызывать библиотеки, написанные на C или C++.

По поводу возможностей по встраиванию Python можно сказать, что, например, существует Maya, где используется именно Python. Но тот, кто видел изнутри плагины для Maya, написанные на Python, согласится с нами в том, что выглядят они не очень хорошо.

В итоге можно сказать, что Python, при всех его сильных сторонах, нам не подходит. Теперь рассмотрим JavaScript.

JavaScript

JavaScript — это, без преувеличений, великий язык, который буквально захватил мир.

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

Если нам, для построения игрового движка, нужен какой-нибудь интерпретатор языка — мы можем найти множество таких интерпретаторов. В реальности же придётся выбирать из двух подобных проектов — V8 и WebKit. И тот и другой имеют достаточно большие размеры. Как результат, если речь идёт о настольных играх, можно было бы рискнуть и включить в игру весь интерпретатор, но в случае мобильных игр нас такой вариант не устраивает.

В компании SocialQuantum есть собственный интерпретатор JavaScript, который проходит 98% тестов, мы планируем перевести этот проект в разряд опенсорсных.

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

Haxe

Тут надо отметить, что когда заходит разговор о JavaScript, следующим обычно вспоминают Haxe. Но, на самом деле, о возможности использования этого языка в качестве встраиваемого говорить нет смысла, так как Haxe, по сути, является не столько языком, сколько транс-компилятором в другие языки. А это не то, что нам нужно.

Может быть, нас устроит ActionScript или какой-нибудь другой скриптовый язык?

ActionScript

Если формально проанализировать ActionScript на соответствие вышеозначенным требованиям, то может показаться, что идеальный скриптовый язык найден. На его стороне динамическая природа, популярность, лёгкость изучения, хорошие возможности, производительность, наличие библиотек, лёгкость встраивания. Этот язык любят и помнят в игровой индустрии, на нём написано огромное количество замечательных Flash-игр. Главная проблема ActionScript заключается в том, что язык этот почти мёртв. Поэтому нас он тоже не устраивает.

AngelScript, Squirrel и другие

Помимо ActionScript существует множество скриптовых языков, таких, как AngelScript, Squirrel и другие. Среди них можно найти такие, которые, формально, почти полностью удовлетворяют нашим требованиям, но обычно это — языки, которые привязаны к их разработчику, в них бывают какие-то застарелые проблемы, которые годами не исправляются. Они, скорее всего, не слишком популярны, недостаточно хорошо документированы, по ним мало учебных материалов, у них не очень большие сообщества. Одним из следствий такого положения дел является тот факт, что их сложно изучать — хотя бы потому, что не до конца ясно — что они собой представляют и как работают.

Как видно, идеального встраиваемого языка мы пока не нашли. Что если создать собственный язык?

Создание собственного языка

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

Есть компании, которые разрабатывают и используют собственные языки, среди них есть и успешные игроки игрового рынка, но, скорее всего, это — не очень хорошая идея.

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

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

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

Например, вот как выглядит наша рабочая среда на основе IDE CLion от JetBrains. Здесь можно видеть созданный нами механизм автодополнения для Lua, который планируется сделать опенсорсным. Опенсорсным мы собираемся сделать и отладчик.

Рабочая среда

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

Возражения по поводу использования Lua

Lua предназначен для C а не для С++

Никто не спорит с тем, что Lua — отличный встраиваемый язык. Главное, что считают его минусом, заключается в том, что он создан для использования с языком C, а не C++. Из-за этого, пытаясь применить в Lua что-то такое, что есть в C++ и нет в C, мы сталкиваемся с проблемами. Однако тут надо понимать, что проблемы эти решало множество довольно умных людей. Среди средств, решающих проблемы встраивания Lua в C++-проекты, можно отметить такие, как Luabind, Luabridge, toLua++, SQLuaHost. Это — далеко не полный список. Они обладают разными достоинствами и недостатками, но, тем не менее, скорее всего, всё, что вам может потребоваться, уже реализовано в одном из этих решений.

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

Lua — это медленно

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

function myGame:onUpdate()
    local tex = Texture::new(name)
    self.background:setTexture(tex)
end

Здесь в каждом игровом тике создаётся новая текстура и устанавливается в качестве фона. Конечно, работать такая конструкция будет не особенно быстро, но никто не мешает писать такие вот вещи.

Lua подходит только для маленьких проектов

Следующее возражение заключается в том, что Lua сделан для того, чтобы писать какие-то маленькие вещи и что-то большое на этом языке написать невозможно. С одной стороны это правда. Но у этого языка высокая модульность. И из множества маленьких блоков можно делать достаточно большие и сложные системы. А если вспомнить то, что было уже сказано о мультипарадигменности и об ООП, то окажется, что ООП подталкивает разработчика к тому, чтобы создавать маленькие модули, которые можно использовать при создании больших и сложных конструкций.

При этом зачастую на Lua какие-то маленькие модули пишутся быстро, а в игровой индустрии «быстрее» — значит «дешевле».

Другие аргументы против Lua

Критикуя Lua, говорят о том, что язык это древний, что он, что называется, «из коробки», не поддерживает ООП, что нумерация элементов в его таблицах начинается не с 0, как можно было бы ожидать от любого приличного языка, а с 1.

Говорят, что его минус в том, что в нём нет тернарного оператора. На самом деле, таких вот аргументов против Lua довольно много, но мы не будем их обсуждать, так как полагаем, что они, по большей части, относятся к привычкам и личным предпочтениям разработчиков.

Итоги

Подведём итоги. Если ваша задача — с минимальными усилиями обзавестись встраиваемым языком — возьмите Lua. В то же время, если у вас есть время и ресурсы на разработку собственного языка или собственных биндингов — опять же — обратите внимание на Lua. Почему и в первом и во втором случаях мы рекомендуем Lua?

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

Во втором случае, если у вас есть ресурсы на разработку инструментов, вы, выбрав Lua, сможете с толком потратить эти ресурсы, так как Lua, несмотря на его популярность в среде разработки игр, язык весьма недооценённый. Как результат, у вас будет возможность, взяв за основу Lua, учесть свои потребности и получить именно то, что вам нужно.

Понравилась статья? Поделить с друзьями:
  • Солдат ?нил?ре сценарий
  • Современные персонажи на праздник
  • Создайте диаграмму сценария на основе диаграммы idef3
  • Сокровища главного волшебника сценарий
  • Современные необычные праздники