Язык Lua и использование скриптов на нем в программах на С++.

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

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

В качестве языка, используемого для написания скриптов, можно использовать как традиционные языки (Python, Ruby, Perl), так и специализированные языки, такие как Lua или Squirell.

Последние, хотя и уступают традиционным в гибкости и наборе библиотек, являются зато гораздо более "легковесными" - они требуют всего лишь наличия соответствующей dll-ки, отличаются большим быстродействием и меньшими требованиями к памяти. Кроме того они достаточно просты.

Далее мы рассмотрим скриптовый язык Lua и использование скриптов на нем в программах на С++.

Язык Lua пришел к нам из Бразилии (именно оттуда где в лесах много диких обезьян :))))). Вначале у его авторов было два небольших скриптовых языка для работы и визуализации данных - DEL (Data Entry Language) и SOL (Simple Object Language). Каждый из них был создан под свою задачу и они оба очень хорошо справлялись со своими задачами и поэтому стали возникать пожелания добавить к ним новые возможности.

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

Так в 1994 году и появился язык Lua. При этом с самого начала была цель, чтобы этот язык мог идти практически на любой из используемых тогда платформ, включая MS DOS, Windows, Unix и Mac OS.

На официальном сайте www.lua.org можно найти краткую историю языка - откуда он произошел, для чего использовался и как развивался.

Одним из существенных отличий языка Lua является то, что этот язык предназначен именно для использования в качестве скриптового языка, программы на котором будут вызываться из основной программы (в отличии от языков Python/Ruby/Perl, которые могут использоваться и самостоятельно).

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

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

Также существует и JIT-компилятор для Lua (который и был использован в игре STALKER) - LuaJIT.

Далее мы сначала рассмотрим установку Lua, потом сам язык, а потом уже его интеграция с С++.

Есть очень хороший русскоязычный сайт, полностью посвященный языку Lua - ilovelua.narod.ru. На этом сайте Вы найдете много интересных статей и примеров. Также есть хорошая вводная статья на сайте www.gamedev.ru.

Ниже приводится пример скрипта на Lua из игры STALKER.

local members = {}
local moved = {}
zone = nil

-------------------------------------------------------------------------
function add_member (npc)
    --проверим: а наш ли это мужик ?
    local ini = npc:spawn_ini ()
    if ini == nil or ini:section_exist ("escape_raid") == false then 
       return 
    end
    --наш, блин. Если уже есть, то пшел вон.
    for a = 1, table.getn (members), 1 do
        if members[a] == npc.id then 
           return 
        end
    end    
    --нету :( Внесем нахала в черный список.
    table.insert (members, npc.id)
    printf ("Stalker %s is added to list", npc:name ())
    -- и изгоним из мира живых
    this.switch_offline (npc.id)    
end


-------------------------------------------------------------------------
function start_raid ()
    if zone == nil then return end
    -- пошла жара !!!
    zone.raid_wait = device ():time_global ()
    for a = 1, table.getn (members), 1 do
        this.switch_online (members[a])
        printf ("Switch to online stalker with id %d", members[a])
    end
    --в идеале, здесь нужно рассортировать команды в случае гибели отдельных мужиков
end


-------------------------------------------------------------------------
class "escape_raid"
-----------------------
function escape_raid:__init (zone, ini)
    -- абсолютный стандарт
    self.ini_ok = true
    printf ("Zone %s is added to binder", zone:name ())
    self.time = 0
    self.raid_wait = 0
end
-----------------------
function escape_raid:on_enter (obj)
    if self.raid_wait ~= 0 then
       if device ():time_global () - self.raid_wait > 20000 then
          self.raid_wait = 0
       else
          return   
       end
    end   
           
    -- наш ли мужик приперся. Если нет, то проигнорируем.
    local ini = obj:spawn_ini ()
    if ini == nil or ini:section_exist ("escape_raid") == false then
       return
    end
    -- и сколько же вас разбрелось ?
    local size = table.getn (moved)
    -- никого :(
    if size == 0 then return end
    --пороемся в реестре покинувших зону и отметим вход
    for a = 1, size, 1 do
        if moved[a].id == obj:id () then
           moved[a].out = false
        end
    end
    --чекнем по поводу безвременной кончины.
    -- если жив и где-то лазит, то обождем
    for a = 1, size, 1 do
        if moved[a].out == true then
           if moved[a].object:alive () == false then 
              -- надо удалить мужика из списков нашей партии
              for b = 1, table.getn (members), 1 do
                  if members[b] == moved[a].id then
                     table.remove (members, b)
                  end
              end
              -- и вообще - он уже пришел :)        
              moved[a].out = false
           else 
              return 
           end
        end   
    end
    self.time = device ():time_global ()
    -- все вернулись. По могилам !!!
    for a = 1, size, 1 do
        this.switch_offline (moved[a].id)
    end
    -- реестр умножим на 0
    moved = {}
end

Установка и компиляция Lua.

Скачайте архив с исходным кодом для последней версии с www.lua.org(на данный момент это версия Lua 5.1.2), распакуйте архив и запустите файл luavs.bat для сборки под M$ windows (при этом у Вас должны быть установлены все необходимые для работы VC пути и переменные окружения).

В результате этого (для M$ windows) будут собраны два выполнимых файла lua.exe и luac.exe, а также библиотеки lua51.dll и lua51.lib.

Программа lua.exe позволяет Вам попробовать язык "вживую", без интеграции в какое-либо приложение. Вы просто набираете конструкции на Lua и они сразу же выполняются.

Эта программа крайне удобна как на этапе освоения языка, так и для проверки модулей расширения для языка (написанных на С/С++).

Рис 1. Скриншот программы lua.exe.

Для Linux просто используйте команду make.

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

Библиотека lua51.dll нужна для работы приложения, использующего скрипты на Lua, а lua51.lib служит для линковки библиотеки в приложение.

Язык 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 поддерживает следующие типы данных - nil, boolean, number, string, function, userdata, thread и table.

Тип nil является специальным типом - это тип значения (константы) nil. Основным свойством этого значения является его отличие от любых других значений.

Тип number служит для представления вещественных (double) значений.

Числовые значения в Lua практически полностью идентичны числовым значениям в С, например - 3, 3.0, 3.1415926, 314.16e-2, 0xff).

Тип string соответствует строкам. Строка в Lua рассматривается как последовательность 8-битовых символов (байт). Строки могут содержать любые байты, в том числе и '\0'.

Строковые значения это просто последовательности символов, заключенные в обычные или двойные кавычки. При этом внутри строки можно задавать специальные (escape) символы, аналогично языку С - '\b', '\n', '\r', '\t', '\\', '\'', '\"', '\0' и '\ddd' (в последнем случае в через ddd обозначен десятичный код символа от нуля до 255).

Также для задания строк можно использовать так называемые длинные скобки (long brackets). Открывающая длинная скобка уровня n состоит из обычной открывающей квадратной скобки ('['), n знаков равенства ('=') и еще одной открывающей квадратной скобки ('['). Так открывающая скобка нулевого уровня это просто [[, открывающая скобка первого уровня это [=[ и т.д.

Полностью аналогично вводится закрывающая скобка уровня n.

Длинная строка (точнее строковое значение) - это последовательности символов, следующая за открывающей длинной скобкой уровня n и до закрывающей длинной скобкой того же уровня.

При этом такие строки могут могут занимать несколько строк в файле, в них не поддерживаются escape-символы (т.е. символы вида '\n','\\', '\xxx') и игнорируются длинные скобки любого другого уровня.

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

Часть строки, следующая за двумя знаками минус (--) и до конца строки является комментарием. Также можно использовать многострочные (длинные) комментарии - они аналогичны длинным строковым значениям, только начинаются с двух знаков минус, за которыми идет открывающая длинная скобка уровня n и заканчивается длинной закрывающей скобкой того же уровня (это очень напоминает """ в языке Python).

Булевский тип (boolean) состоит всего из двух значений - false и true. Обратите внимание, что в Lua только nil и false являются ложными значениями (при использовании их в условных конструкциях), а все остальные значения (в том числе и пустые строки) являются истинными.

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

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

Тип thread соответствует нити выполнения. Однако эти нити никаким образом не связаны с операционной системой и поддерживаются исключительно средствами самого Lua.

Тип function соответствует функциям. Функции в Lua являются полноценными сущностями - они могут быть записаны в переменные, переданы как параметры в другие функции и возвращены как результат выполенения функций.

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

Для удобства работы можно вместо индексирования таблицы по имени (строке) использовать это имя как имя поля структуры, т.е. вместо t["abc"] можно использовать t.abc. Эти две формы обращения полностью эквивалентны.

Lua - язык с динамической типизацией, подобно Python или Ruby - тип связывается не с переменной, а с ее значением в данный момент. При этом любой переменной может быть присвоено значение любого типа (вне зависимости от того значения, которое она содержала раньше). До первого присваивания переменная содержит значение nil.

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

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

Для того, чтобы узнать тип значения служит библиотечная функция type, возвращающая строку, описывающую тип переданного значения.

x = 1
y = true
z = 'abc'
print ( type ( x ), type ( y ), type ( z ) )

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

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

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

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

Операторы Lua

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

x = 1; y = 2; z = 'abc'; print ( x, y, z )

Группа операторов может быть объединена в блок (составной оператор) при помощи конструкции do-end.

do                -- начала блока
    оператор1     -- тело блока
    оператор2
	.....
end               -- конец блока

if x < 0 then  -- пример использования блока
do
    x = 0
    print ( 'Negative value !!!' )
    return
end

1. Оператор присваивания

В простейшей форме оператор присваивания в Lua выглядит следующим образом:

var=expression

Кроме того Lua (подобно Python и Ruby) поддерживает множественное присваивание, когда одним оператором присваивания может быть присвоено сразу много значений - в этом случае слева от знака равенства ('=') стоит список переменных, а справа - список выражений.

Так для того чтобы поменять местами значения переменных x и y можно воспользоваться следующим оператором:

x,y = y,x     -- поменять местами х и у

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

i = 3                  -- записать в i число 3
i, a [i] = i+1, 20     -- записать в i 4, а в a [3] - 20
x, y     = 5           -- записать в x 5, а в y nil

2. Оператор if

Условный оператор в Lua имеет следующий вид:

if expr then block { elseif expr then block } [else block] end

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

Как уже отмечалось, любое значение выражения, кроме nil и false считается истинным.

if num < 1 then
    print ( 'less than one' )
elseif num == 1 then
    print ( 'one' )
elseif num == 2 then
    print ( 'two' )
else
    print ( 'Illegal value' )

3. Оператор while

Оператор цикла while имеет следующий вид:

while expr do block end

Для преждевременного выхода из цикла могут использоваться операторы break и return, но эти операторы могут быть только последними операторами блока, поэтому в остальных случаях может понадобиться заключить эти операторы в свой блок do-end.

flag = false
while true do
	if flag then do break end end -- так как break не последний оператор, то мы его заключаем в свой блок
	. . .
end

                       -- простой пример
i = 1
while i < 300 do    -- найти наименьшую степень двух, большую 300
    i = i * 2
end

4. Оператор repeat

Данный оператор имеет следующий вид:

repeat block until expr

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

repeat
	local x
	. . .
until x > 0

                                -- решить уравнение x=cos(x) на [0,pi/2] половинным делением
a, b = 0, math.pi/2             -- интервал поиска корня
repeat
    local x = (a + b) * 0.5     -- середина отрезка
	
    if x < math.cos ( x ) then  -- проверяем корень справа или слева
        a, b = a, x
    else
        a, b = x, b
    end                         -- закрыть оператор if
until b - a < 1e-5              -- продолжаем, пока не будет достигнута требуемая точность
print ( (a + b) * 0.5 )

5. Простая форма оператора for

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

for name = expr1, expr2> [,expr3] do block end

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

Для преждевременного выхода из цикла также как и для всех остальных операторов цикла (while, repeat) можно использовать операторы break и return, но они должны быть последними операторами в непосредственно содержащем их блоке.

Сама переменная цикла является локальной для оператора цикла и по его окончании не определена.

for x = 1,10 do               -- напечатать первые десять степеней двойки
    print( x, 2 ^ x )
end

6. Оператор for с использованием итераторов

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

Эта форма оператора имеет следующий вид:

for namelist in explist do block end

Рассмотрим оператор следующего вида:

for var1,var2,...,varN in explist do block end

Он будет эквивалентен следующей группе операторов:

do
    local f,s,var = explist
    while true do
        local var1,var2,...,varN = f(s, var)
		
        var = var1
        if var == nil then do break end end
        block
    end
end

Обратите внимание, что explist выполняется всего один раз и возвращает функцию-итератор (f), состояние (s) и начальное значение для первого параметра цикла.

Для итерирования по таблицам существует специальные функции ipairs и pairs:

for i,v in ipairs ( t ) do    -- цикл по элементам массива
    print ( i, v )
end

for k,v in pairs ( t ) do     -- цикл по элментам хэша
    print ( k, v )
end

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

7. Вызов функции как оператор

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

x = 10; s = 'abc'
foo ( x, s, 3.14 )

8. Объявления локальных переменных

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

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

local namelist [=  explist]
                  -- пример объявления локальных переменных
local x = 7
local s = x + 7

Локальными могут быть и функции:

local function foo () print ( "Hello" ) end

Выражения в Lua

Lua поддерживает все стандартные арифметические операции (+,-,*,/), включая остаток от деления (%) и возведение в степень (^).

Арифметические операторы применимы и к числам и к строкам, которые в этом случае преобразуются в числа (поэтому оператор + нельзя использовать для конкатенации строк - это арифметический оператор).

Также в Lua определены следующие операции сравнении величин:

    ==   ~=   <   >   <=   >=

Эти операторы всегда возвращают булевское значение (т.е. true или false). Обратите внимание, что оператор "не равно" в Lua это '~=' (в отличии от C/С++).

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

Также в Lua определены логические операции and, or и not. Оператор not всегда возвращает true или false.

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

Оператор or возвращает первый операнд, если он не false и не nil, иначе он возвращает второй операнд.

Таким образом, логические операторы and и or могут возвращать значения любых типов. При этом эти операторы вычисляют значение второго операнда только, если его нужно вернуть. В противном случае вычисление второго операнда не происходит. Так в выражении 10 or foo () вызова функции foo не произойдет.

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

x = 10
write ( 'First value is ' .. 7 .. ' and second is ' .. x )

Также в Lua определен оператор длины '#'. Длина строки - это количество байт в этой строке. Длина таблицы t определяется как такое целое значение n, что t[n] ~= nil, а t[n+1]==nil.

Если t - обычный массив с элементами а в позициях 1, 2, ..., n, то его длина будет равна n.

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

a = {}                      -- пустая таблица
b = { 1, 5, 7, 'abc' }      -- обычный массив с элементами в позициях 1, 2. и т.д.
c = { x = 7, y = "6" }      -- таблица с полями x и y, можно обращаться как c.x, так и c["x"]
d = { 1, 'string', x = 77 } -- смешанная таблица
у = { 1, xxx = 17, }        -- разделитель в конце допустим

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

b = { 1, 5, 7, 'abc' }      -- обычный массив с элементами в позициях 1, 2, и т.д.

Также отдельное поле может иметь вид name=expr. В этом случае задаются именованные поля, т.е. поля, ключом к которым служат имена полей, позволяя тем самым создавать аналоги структур.

Возможен также случай, когда задание поля имеет вид [expr]=expr, например t={[foo(7)]=15}. Этот пример эквивалентен следующим операторам:

t         = {}          -- создать пустую таблицу
t[foo(7)] = 15          -- произвести запись эелемента по индексу, определяемому первым выражэением.

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

t = { 1, 5, [17] = 3 }      -- массив с элементами в позициях 1, 2 и 17
x = #7                      -- в х будет знчение 2, т.к. t [2] ~= nil, t [3] == nil

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

prefixexp ( [explist] )
prefixexp:name ( [explist] )

Последняя форма служит аналогом вызова методов - запись v:name(args) эквивалента обычному вызову с дополнительным параметром v.name(v,args) (обратите внимание, что вычисление значения v происходит всего один раз).

t = { x = 1, y = 2 }        -- создали двухмерный вектор t
function t:print ()         -- создали поле t.f
    print( 'x='.. self.x ..' y=' .. self.y )
end
t:print ()                  -- вызвали t.print (t)

Значения всех аргументов функции вычисляются до момента вызова функции. Кроме того вызов вида f(fields) переводится в f({fields}) (подобно тому, как это делается в языках Python и Ruby).

foo { x = 1, y = 2 }         -- тоже что и foo ( { x = 1, y = 2 } )

Аналогично вызов f'string' (или f"string: или f([string])) переводятся в f("string") .

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

x = foo                     -- это будет интерпретировано как запись функции foo в переменную x.

(1, 2 )

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

Приоритеты операций в Lua

Приоритеты операций в Lua определены следующим образом:

     or
     and
     <     >     <=    >=    ~=    ==
     ..
     +     -
     *     /     %
     not   #     - (unary)
     ^

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

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

function funcbody

funcbody определяется одним из следующих видов:

( ) block end
( parameter-list ) block end

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

return function (n) return n^2 end

Приведенный выше пример возвращает функцию, возводящую свой аргумент в квадрат.

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

function funcname funcbody
local function funcname funcbody

Последний случай задает именованную локальную функцию - она будет доступна только в пределах того блока, в котором она объявлена.

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

name { '.' name } [ ':' name]

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

function f () body end

Приведенный выше пример на самом деле эквивалентен следующей конструкции -

f = function () body end

Т.е. объявление именованной функции фактически создает анонимную функцию и присваивает ее соответствующей переменной или полю таблицы. Поэтому следующий пример

function t.a.b.c.f () body end

на самом деле эквивалентен приведенному ниже фрагменту кода.

t.a.b.c.f = function () body end

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

function f (n)                   -- рекурсивное вычисление факториала
    if n == 0 then               -- факториал нуля равен единице
        return 1
    else 
        return n * f ( n - 1 )   -- Lua поддерживает оптимизации tail recursion
    end
end

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

Для доступа к параметрам, соответствующим многоточию в списке параметров, можно использовать специальную переменную arg

function foo ( a, ... )   -- функция, печатающая все аргументы, кроме первого
    for i = 1, arg.n do   -- для каждого переданного аргумента
       print ( arg [i] )  -- напечатать его
end

Функция может возвращать сразу много значений, и их не надо для этого "упаковывать" в таблицу - просто в операторе return укажите список возвращаемых значений через запятую:

function foo ( a, b, c )           -- возращаем значения в обратном порядке
    return c, b, a
end

function sincos ( angle )
    return math.sin ( angle ), math.cos ( angle )
end

c, s = sincos ( 3.1415926 / 8 )   -- найти сразу и синус и косинус и сохранить их в переменных
с1   = (sincos ( 0.7 ) )          -- сохранить только косинус

foo ()                            -- вызов foo ( nil, nil, nil )
foo ( 1 )                         -- вызов foo ( 1, nil, nil )
foo ( {}, 'abc' )                 -- вызов foo ( {}, 'abc, nil )
foo ( 1, 2, 3, 4, 5 )             -- вызов foo ( 1, 2, 3 )

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

function f1 ( ,... )
    return arg
end

function f2 ( ,... )
    return unpack ( arg )
end

t = f1 ( 1, 2, 3, 'a' )        -- вернет массив значений как таблицу
x, y, z = f2 ( 1, 2, 3, 'a' )  -- вернет 4 значения, которые будут присвоены переменным

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

a, b, c, d = f2 ( 1, 2, 3, 4 )

Можно создав анонимную функцию, сразу же вызвать ее, нигде не сохраняя ее:

x = 1 + (function (n) return 2^n end) ( 4 )

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

Обратите внимание, что область видимости начинается оператора, после объявления этой переменной, т.е. в самом операторе объявления она еще не видна. Поэтому в следующем примере происходит инициализация локальной переменной x значением глобальной (или внешней) переменной x.

local x = x

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

a = {}                -- создали глобальную таблицу t
local x = 10          -- создали локальную переменную x
for i = 1,10 do       -- для каждого i от 1 до 10
    local y = 0       -- создали свою локальную переменную y со значением 0
                      -- построили функцию, использующую обе переменные x и y и сохранили ее в таблицк
    a [i] = function () y = y + 1; return x + y; end
end

В результате выполнения этого фрагмента кода будет создано 10 анонимных функций, каждая со своим экземпляром локальной переменной y, но с общей локальной переменной x.

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

Модули в Lua

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

Ниже приводится простой пример такого модуля - пусть файл a.lua содержит приведенный ниже код.

module ( ..., package.seeall )
function foo ()
    print ( "Hello World!" )
end

Тем самым мы создали модуль "a", к которому можно обратиться из других файлов

require "a"     -- загрузить модуль а

a.foo ()        -- обратиться к определенной в модуле а функции foo

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

package.loaded.a = nil -- выгрузить модуль а
require "a"            -- снова загрузить модуль а

a.foo ()               -- обратиться к опеределенной в модуле функции

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

В полях package.path и package.cpath содержатся разделенные точкой с запятой списки шаблонов для загрузки модулей Lua и бинарных модулей. За счет изменения этих полей можно обеспечить поиск загружаемых модулей в нужных каталогах:

package.path = package.path .. ';D:\\Alex Books\\New-2\\Code\\Lua'

Метатаблицы и метаметоды

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

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

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

Для работы с метатаблицей служат функции setmetatable и getmetatable.

Событие Оператор Комментарий
__add(op1, op2) + бинарный плюс - сложение объектов
__sub(op1, op2) - бинарный минус - вычитание
__mul(op1, op2 * умножение
__div(op1, op2) / деление
__mod(op1, op2) % остаток от деления
__pow(op1, op2) ^ возведение в степень
__unm(op) - унарный минус
__concat(op1, op2) .. конкатенация строк
__len(op) # длина
__eq(op1, op2) == оператор "равно"
__lt(op1, op2) < оператор "меньше"
__le(op1, op2) <= оператор "меньше или равно"
__index(op, key) [] чтения по ключу
__newindex(op, key) [] запись по ключу
__call(op, ...) () вызов как функции
__gc вызывается для userdata-объектов при сборке мусора
__metatable Это поле позволяет защищать метатаблицы объектов - при установленном этом поле блокируется доступ к метатаблице объекта. Если задать это поле в метатаблице, то getmetatable будет просто возвращать его значение. setmetatable вернет ошибку.
__tostring Функция, вызываемая tostring для перевода объектов в строковое представление. Значением поля является функция, получающая на вход объект и возвращающая его строковое представление

Большая группа событий связана со стандартными операторами - сложение, вычитание, умножение, деление, конкатенация и т.п. За счет задания обработчиков соответствующих событий можно легко добавить таблице с полями x и y возможность сложения, умножения и т.п., т.е. превратить в полноценный двухмерный вектор.

                         -- создать метатаблицу с переопределенной операцией сложения
mt = { __add = function (a,b) return { x = a.x + b.x, y = a.y + b.y } end }

                         -- создать таблицы-вектора
u = { x = 1, y = 2 }
v = { x = 0, y = 1 }

setmetatable ( u, mt )   -- установить для таблицы u метатаблицу

w = u + v                -- осуществить сложение таблиц
print ( w.x, w.y )

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

Задать функцию __add можно и альтернативным способом

mt = {}

function mt.__add (a,b) 
    return { x = a.x + b.x, y = a.y + b.y }
end

Однако определенная выше операция не совсем корректна - она возвращает таблицу с правильными значениями полей x и y, но для этой таблицы операция сложения уже не определена. Это можно легко исправить, если в возвращаемой таблице установить метатаблицу, как показано ниже.

function mt.__add (a,b) 
    return setmetatable ( { x = a.x + b.x, y = a.y + b.y }, mt )
end

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

Обратите внимание, что если для всех арифметических операторов есть соответствующие события (включая и унарный минус), то для операторов сравнения событий крайне мало - всего __eq, __lt и __le. Все остальные операторы сравнения выводятся из них через отрицание, так оператор ~= определяется как not ==.

Более интересными событиями являются __index и __newindex.

Событие __index возникает при каждом обращении на чтение к элементу таблицы. Задав поле __index в метатаблице мы можем изменить механизм доступа к элементам таблицы (объекта). Простейший способ достижения этого - явное задание функции обработки данного сообщения:

function mt.__index ( t,key )
    print( 'Accessing table for key ', key )
    return rawget ( t, key )
end

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

Так можно легко добавить двухмерному вектору поле length, которое будет на самом деле просто вычислять длину вектора и возвращать ее:

function mt.__index (t,key) 
    if key ~= 'length' then          -- если не наш ключа
       return rawget ( t, key )      -- то просто вернуть запрашиваемое поле
    end
                                     -- иначе вычислить длину и вернуть ее
    return math.sqrt ( t.x * t.x + t.y * t.y )
end

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

Событие __newindex происходит каждый раз при записи значения в таблицу. Так за счет задания обработчика данного события можно легко защитить отдельное поле от изменения:

function mt.__newindex (t, key, value) 
    if key == 'xxx' then              -- если это наше поле
       error ( 'xxx is read-only' )   -- то вернуть ошибку
    else                              -- иначе осуществляем запись поля
       return rawset ( t, key, value )
    end
end

Событие __call позволяет вызывать объект, как функцию (т.е. фактически определяет оператор ()).

function mt.__call ( f, ... ) 
    print ( 'Called with: ' )
    for i = 1, arg.n do              -- просто напечатать все переданные аргументы
       print ( arg [i] )
    end
end

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

Стандартные функции в Lua

Все стандартные функции в Lua можно разбить на следующие несколько групп - основные (basic), работа с модулями (package), работа со строками, работа с таблицами, математические функции, ввод/вывод, доступ к ОС, средства отладки. Для ряда групп соответствующие функции собраны в таблицы, соответствующие группам, например string, math и т.п.

Ниже приводятся основные функции для этих групп.

Таблица 1. Основные функции.

Название Комментарий
assert (v [, message]) Вызывает ошибки, когда значение v ложно. При этом печатает сообщение message или "assertion failed!", если параметр message не задан
collectgarbage (opt [, arg]) Интерфейс к сборщику мусора. Действия определяются переданным первым параметром.

"stop": остановка сборки мусора.

"restart": снова запустить сборщик мусора.

"collect": осуществить полный цикл сборки мусора.

"count": возвращает полный объем используемой Lua памяти в Кбайтах.

"step": выполняет один шаг сборки мусора. Параметр size управляет величиной шага.

"setpause": устанавливает arg/100 в качестве нового значения для остановки сборщика.

"setstepmul": Устанавливает arg/100 как новое значение для умножителя шага в сборщике (подробнее в документации §2.10).

dofile (filename) Открывает и выполняет файл с заданным именем.
getmetatable (object) Возвращает метатаблицу заданного объекта.
setmetatable (object, mt) Устанавливает у объекта метатаблицу. Возвращает сам объект.
ipairs (t) Возвращает итератор для обхода таблицы по целочисленным индексам. Останавливается на первом отсутствующем индексе
pairs (t) Возвращает итератор для полного обхода таблицы
print (···) Печать всех переданных аргументов при помощи функции tostring.
rawequal (v1, v2) Возвращает равны ли переданные объекты не используя метаметоды.
rawget (table, index) Чтение из таблицы без использования метаметодов.
rawset (table, index, value) Запись значения в таблицу без использования метаметодов.
tonumber (e [, base]) Перевод величины в число. Параметр base является основанием системы счисления от 2 до 36 включительно.
tostring (e) Перевод величины в строку. Использует метаметод __tostring.
type (v) Возвращает тип параметра как строку.

Таблица 2. Работа с модулями.

Название Комментарий
module (name [, ···]) Создает модуль с заданным именем. Если уже есть таблицы в package.loaded[name] то она и является модулем.

Если есть глобальная таблица с именем name, то она становится модулем. В противном случае создается новая таблица с именем name, она заносится в package.loaded[name].

В этой таблице в поле _NAME записывается имя модуля, в поле _M записывается ссылка на себя, а в поле _PACKAGE - имя пакета.

После этого эта таблица становится таблицей окружения для текущей функции.

require (modname) Загружает заданный модуль. Если модуль уже загружен, то просто возвращает ссылку на него.
package.seeall(module) Устанавливает метатаблицу для модуля с полем __index указывающим на глобальное окружение. Функция предназначена для передачи в качестве дополнительного параметра в функцию module

Все функции для работы со строками находятся в таблице string. Индексирование символов начинается с единицы. Допустимы отрицательные индексы (-1 указывает на последний символ, -2 - на предпоследний и т.д.).

Таблица 3. Работа со строками.

Название Комментарий
string.byte (s [, i [, j]]) Возвращает массив чисел, соответствующих кодам байт из строки - s[i],s[i+1],...,s[j].

Если параметр i не задан, то он считается равным 1, если параметр j не задан, то он считается равным i

string.char (···) Возвращает строку, построенную из байт, числовые значения которых были переданы в качестве параметров, так string.char(97,98,99) вернет строку 'abc'
string.dump (function) Возвращает строку, содержащую бинарное представление переданной функции.
string.find (s, pattern [, pos [, plain]]) Поиск вхождения шаблона в строку.При успехе возвращает индексы начала и конца первого вхождения. Параметр pos задает начальную позицию для поиска, по умолчанию равную 1. Если параметр plain равен 1, тогда осуществляется просто поиск вхождения подстроки, без использования шаблонов.
string.format (formatstring, ···) Практически полный аналог printf, за исключением поддержки *,l,L,n,p,h. Кроме того появилась дополнительная опция q, позволяющая форматировать строки в безопасном для понимания интерпретатором Lua виде.
string.gmatch (s, pattern) Возвращает итератор по вхождениям шаблона в строку.

Так следующий пример напечатает все слова из строки

for w in string.gmatch ( s, '%a+' ) do
    print ( w )
end

Следующий пример разбирает строку, содержащую набор присваиваний вида key=value и заносит их в таблицу.

t = {}
for k,w in string.gmatch ( s, '(%w+)=(%w+)' ) do
    t [k] = w
end
		
string.gsub (s, pattern, repl [, n]) Возвращает копию строки s в которой все вхождения шаблона pattern (или же только первые n вхождений) были заменены при помощи параметра repl.

Параметр repl может быть как строкой, так и таблицей или функцией.

В случае если это строка, то происходит просто замена вхождения шаблона на эту строку.

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

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

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

string.len (s) Возвращает длину строки, с учетом нулевых байт.
string.lower (s) Возвращает копию строкит, в которой все бкувы были заменены на lower-case.
string.upper (s) Возвращает копию строкит, в которой все бкувы были заменены на upper-case.
string.match (s, pattern [, pos]) Возвращает первое найденнео вхождение шаблона в строку или nil
string.rep (s, n) Возвращает результат конкатенации n копий исходной строки, так string.rep('*', 3 ) будет '***'.
string.reverse (s) Возвращает строку, полученную обратной перестановкой байт в исходной строке, так строка 'abcd' переходит в 'dcba'.
string.sub (s, i [, j]) Возвращает подстроку s с позиции i по позицию j включительно. По умолчанию j равно -1.

Функции для работы с таблицами собраны с таблицу table. Большиство из них предназначены для работы с массивами.

Таблица 4. Функции для работы с таблицами.

Название Комментарий
table.concat (t [, sep [, i [, j]]]) Возвращает t[i]..t[i+1].. ... ..t[j]
table.insert (table, [pos,] value) Вставляет новое значение в заданное место в массиве со сдвигом эелемента массива. Если параметр pos не задан, то происходит просто добавление элемента к концу массива.
table.maxn (table) Возвращает наибольшийй положительный индекс для которого в массиве есть значение. Если таких индексов нет, то возвращается 0.
table.remove (table [, pos]) Удаление элемента в заданном месте со сдвигом остальных эелеменетов. Если параметр pos не задан, то удаляется последний элемент таблицы. Функция возвращает удаленное занчение.
table.sort (table [, comp]) Сортировка таблицы. Функция сравнения получает на вход два сравниваеммых значение и возвращает true>, если первый объект меньше второго.

Таблица 5. Математические функции.

Название Комментарий
math.abs (x) Модуль числа
math.acos (x) Аркосинус (в радианах).
math.asin (x) Синус угла (в радианах).
math.atan (x) Арктангенс
math.atan2 (y, x)
math.ceil (x) Ниаменьшее целое, большее или равное x.
math.cos (x) Косинус угла
math.cosh (x) Гиперболический косинус.
math.deg (x) Перевод угла из радиан в градусы.
math.exp (x) Фозвращает e^x.
math.floor (x) Наибольшее целое, меньшее или равное x.
math.fmod (x, y) Остаток от деления
math.log (x) Натуральный логарифм
math.log10 (x) Десятичный логарифм.
math.max (x, ···) Максимальное занчение из переданных чисел.
math.min (x, ···) Минимальное значение из переданных чисел.
math.pow (x, y) Возведение x в степень y.
math.rad (x) Перевод угла из градусов в радианы.
math.random ([m [, n]]) Случайное число. Если параметры не заданы, то возвращается случайное число в [0,1].. Иначе возвращается целое число либо из [1,m], либо из [m,n]
math.randomseed (x) Инициаолиция генерратора псевдослучайных чисел.
math.sin (x) Синус угла
math.sinh (x) Гиперболический синус
math.sqrt (x) Квадратный корень
math.tan (x) Тангенс угла
math.tanh (x) Гиперболический тангенс

Таблица 6. Функции ввода/вывода.

Название Комментарий
io.close ([file])
io.flush ()
io.input ([file])
io.lines ([filename])
io.open (filename [, mode])
io.output ([file])
io.read (···)
io.write (···)
file:close ()
file:flush ()
file:lines ()
file:read (···)
file:seek ([whence] [, offset])
file:write (···)

Таблица 7. Функции работы с ОС.

Название Комментарий
os.clock () Возвращает приблизительное время CPU в секундах, потраченнное на выполнение данной прграммы.
os.date ([format [, time]]) Возвращает строку или таблицу, содержащую заданную дату и время
os.difftime (t2, t1) Разница в секундах.
os.execute ([command]) Выполнить команду, аналог system
os.remove (filename) Удалить файл или каталог (каталог должен быть пуст).
os.rename (oldname, newname) Переименовать файл или каталог.
os.time ([table])
os.tmpname () Возвращает строку, содержащую имя, пригодное для использования в качестве имени временного файла.

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

Таблица 8. Основные глобальные перменные.

Название Комментарий
_G Глобальная таблица окружение, всегда _G._G==_G
_M Текущеий модуль
_VERSION Версия как строка, например "Lua 5.1"
math.pi Константа pi
math.huge Наибольшее представимое число

Создание и работа с объектами при помощи метатаблиц

За счет использования метатаблиц можно очень легко реализовать в Lua объекты и работу с ними.

Для каждого класса нам понадобится таблица, соответствующая самому классу, которая будет содержать методы классы и экземпляров класса. Также для каждого класа нужна своя метатаблица.

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

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

                                    -- simple vector2D class for Lua
Vector = {}                         -- Vector class
Vector_mt = { __index = Vector }    -- Vector metatable

function Vector:new(x,y)            -- конструктор - создает вектор
    return setmetatable( {x=x, y=y }, Vector_mt )
end

function Vector:dot(v)              -- скалярное произведение векторов
    return self.x*v.x + self.y*v.y
end

function Vector:length()            -- длина вектора
    return math.sqrt(self.x*self.x + self.y*self.y)
end

function Vector_mt:__add(v)         -- сложение векторов
    return setmetatable( {x=self.x+v.x, y = self.y+v.y }, Vector_mt )
end

function Vector_mt:__sub(v)         -- вычитание векторов
    return setmetatable( {x=self.x-v.x, y = self.y-v.y }, Vector_mt )
end

function Vector_mt:__mul(v)         -- покомпонентное умножение векторов
    return setmetatable( {x=self.x*v.x, y = self.y*v.y }, Vector_mt )
end

function Vector_mt:__tostring ()    -- перевод в строку
	return '(' .. self.x .. ',' .. self.y .. ')'
end

function Vector:print()             -- печать вектора
    print("Vector(x=" .. self.x .. ",y=" .. self.y .. ")" )
end

Обратите внимание на использование события __index в метатаблице - обработчик этого события ссылается на таблицу самого класса (таблицу методов). За счет этого каждый раз, когда идет обращение к методу объекта или класса будет происходить обращение к таблице класса (поскольку имен методов в метатаблице нет).

require ( 'Vector' )          -- загрузить модуль Vector
 
u = Vector:new ( 1, 1 )       -- создать вектор (1.1)
v = Vector:new ( 1, -1 )      -- создать вектор (1,-1)

u:print ()                    -- напечатать содержимое вектора u

w = u + v                     -- сложиь вектора u и v
w:print ()                    -- напечатать результат

Довольно легко реализовать наследование за счет "сцепление" таблиц классов при помощи события __index:

Base    = {}                          -- базовый (родительский) класс
Base_mt = { __index = Base }          -- его метатаблица

function Base:new ()                  -- конструктор класса 
    return setmetatable ( { x = 1 }, Base_mt )
end

function Base:foo ()                  -- виртуальная функция
    print ( 'Base:foo', self.x )
end

Derived = {}                          -- дочерний класс
Derived_mt = { __index = Base }
setmetatable ( Derived, Derived_mt )  -- его метатаблица

function Derived:new ()               -- конструктор для нового класса
    return setmetatable ( { x = 2 }, Derived_mt )
end

function Derived:foo ()                -- переопределяем функию
    print ( "Derived:foo" )
end

a = Base:new ()
b = Derived:new ()
a:foo ()
b:foo ()

C API для Lua

Все необходимые типы, функции и константы определены в файле lua.h, кроме этого также необходима библиотека lua51.lib.

Подобно OpenGL и другим библиотекам все функции в Lua начинаются с префикса lua_, а константы - с префикса LUA_.

Для работы с Lua необходимо сначала создать контекст для работы (иногда вместо термина контекст используется термин виртуальная машина). У вас может быть несколько независимых контекстов в каждом из которых происходит работа. Все остальные функции C API получают этот контекст как первый параметр. Для создания контекста служат следующие функции:

lua_State *lua_open      ();
lua_State *lua_newstate  ( lua_Alloc f, void * ud );
lua_State *luaL_newstate ();

Первая из этих функция на самом деле сейчас определена как макрос, вызывающий luaL_newstate и является устаревшей функцией. Функция lua_newstate создает новое состояния и при этом получает в качестве параметров функцию выделения памяти и параметр, передаваемый этой функции при ее вызове. Функция luaL_newstate использует стандартную функцию выделения памяти, основанную на функции realloc и служит заменой lua_open.

При ошибке создания контекста возвращается значение NULL.

Тип lua_Alloc определен следующим образом:

typedef void * (*lua_Alloc) ( void * ud, void * ptr, size_t oldSize, size_t newSize );

Для освобождения контекста служит функция lua_close:

void lua_close ( lua_State * lua );

Одной из особенностей Lua является то, что передача значений между программой на С/С++ и Lua происходит через специальный стек, каждый элемент этого стека является значением в Lua.

При вызове функции С/С++ из Lua вызываемая функция получает свой стек, независимый от предыдущих стеков и стеков других функций, активных на данный момент.

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

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

Так если стек содержит n элементов, то индекс 1 соответствует первому элементу (первому аргументу вызываемой функции), индекс 2 - второму, индекс n соответствует последнему (верхушке стека). Индекс -1 соответствует последнему элементу, -2 - предпоследнему элементу, индекс -n - первому элементу (см. рис). Индекс i является валидным (т.е. может использоваться для обращения к элементам стека) если 1<=abs(i)<=n. Нулевое значение не является валидным индексом.

Рис 2. Индексация в стеке.

Вы сами должны отвечать за то, чтобы не произошло переполнение стека (размер стека фиксирован, по умолчанию он равен 20). Для явного задания размера стека служит функция lua_checkstack:

int lua_checkstack ( lua_State * lua, int extra );

После этого вызова размер стека будет составлять не менее extra элементов. При ошибке (не удалось увеличить размер стека) возвращается ноль. Обратите внимание, что данная функция может только увеличить размер стека, но не уменьшить.

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

Доступ к глобальным переменным осуществляется через псевдоиндекс LUA_GLOBALSINDEX. Текущее окружение доступно через псевдоиндекс LUA_ENVIRONINDEX.

Для доступа к глобальной переменной используется обычный механизм доступа к полям таблицы. Так для доступа к глобальной переменной x используется следующий вызов:

lua_getfield ( lua, LUA_GLOBALSINDEX, "x" );

Этот вызов возьмет значение глобальной переменной x и добавит на вершину стека.

Как и в "маленьких мягких окошках" в Lua есть свой реестр. Только здесь это просто специальная таблица, в которой код из С/С++ может хранить значения. Для доступа к этой таблице служит псевдоиндекс LUA_REGISTRYINDEX. Не рекомендуется осуществлять запись в таблицу реестра по целочисленным ключам - они используются Lua.

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

void lua_call  ( lua_State * lua, int numArgs, int numResults );
void lua_pcall ( lua_State * lua, int numArgs, int numResults, int errFunc );

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

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

Если значение параметра numResults не равно LUA_MULTRET, то на стеке будет оставлено ровно numResults значений. Лишние значения убираются со стека, вместо недостающих заносятся nil.

Функция lua_pcall отличается тем, что в случае ошибки просто помещает на стек сообщение об ошибке и возвращает код ошибки. Код ошибки может быть равен нулю (все в порядке) или же принимать одно из следующих значений - LUA_ERRRUN (ошибка во время выполнения), LUA_ERRMEM (ошибка выделения памяти) и LUA_ERRERR (ошибка во время выполнения обработчика ошибок).

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

void lua_getfield ( lua_State * lua, int index, const char * key );

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

Пусть в стеке была таблица t. Тогда следующие рисунки иллюстрируют результаты вызовов функции lua_getfield.

Рис 3. Работа функции lua_getfield.

void lua_setfield ( lua_State * lua, int index, const char * key );

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

Рис 4. Работа функции lua_setfield

void lua_pop ( lua_State * lua, int n );

Данный вызов удаляет с вершины стека n значений.

void lua_pushboolean ( lua_State * lua, int value );

Функция добавляет на вершину стека логическое значение value (0 или 1).

void lua_pushinteger ( lua_State * lua, lua_Integer value );

Функция добавляет на вершину стека число value.

void lua_pushlstring ( lua_State * lua, const char * str, size_t len );

Функция добавляет на вершину стека копию строки с длиной len (маркер '\0' игнорируется).

void lua_pushnil ( lua_State * lua );

Функция помещает на вершину стека значение nil.

void lua_pushnumber ( lua_State * lua, lua_Number value );

Функция добавляет на вершину стека число value.

void lua_pushstring ( lua_State * lua, const char * str );

Функция помещает на вершину стека строку, законченную символом '\0'.

void lua_pushvalue ( lua_State * lua, int index );

Функция помещает копию значения, расположенного по заданному индексу на вершину стека.

void lua_pushfstring ( lua_State * lua, const char * fmt, ... );

Аналогично функции sprintf помещает на вершину стека результат форматированного вывода в строку. Функция сама отслеживает необходимый объем памяти под строку. Однако есть ограничения на использование спецификаций форматов - не поддерживается точность и флаги.

void lua_remove ( lua_State * lua, int index );

Удаляет элемент с заданным индексом из стека, сдвигая выше расположенные элементы.

Рис 5. Работы функции lua_remove.

int lua_gettop ( lua_State * lua );

Возвращает индекс элемента на вершине стека, т.е. количество элементов в стеке.

int lua_settop ( lua_State * lua, int index );

Устанавливает новое количество элементов в стеке. При этом лишние элементы автоматически удаляются, при необходимости стек дополняется nil-ми.

int lua_insert ( lua_State * lua, int index );

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

Рис. 6. Работа функции lua_insert.

int lua_gettable ( lua_State * lua, int index );

Помещает на вершину стека значение t[k], где t - таблица, расположенная по заданному индексу, а ключ k - элемент с вершины стека.

int lua_createtable ( lua_State * lua, int narr, int nass );

Создает новую пустую таблицу и помещает ее на вершину стека. При этом в таблице сразу резервируется место для narr элементов массива и nass для элементов хэша (обычно только один из этих параметров отличен от нуля).

int lua_newtable ( lua_State * lua );

Создает новую таблицу, эквивалентен lua_createtable ( lua, 0, 0 ).

void * lua_newuserdata (lua_State * lua, size_t size );

Функция выделяет блок памяти заданного размера и создает соответствующий объект типа userdata, помещая его на стек. Возвращается указатель на выделенный блок памяти.

int lua_equal ( lua_State * lua, int index1, int index2 );

Возвращает единицу, если элементы по заданным индексам равны и ноль в противном случае.

int lua_getmetatable ( lua_State * lua, int index );

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

int lua_setmetatable ( lua_State * lua, int index );

Снимает таблицу с вершины стека и устанавливает ее в качестве метатаблицы для объекта по заданному индексу

int lua_isboolean ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является логическим (типа boolean) и ноль в противном случае.

int lua_isfunction ( lua_State * lua, int index );

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

int lua_isnil ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является nil и ноль в противном случае.

int lua_isnone ( lua_State * lua, int index );

Возвращает единицу, если переданный индекс является недопустимым.

int lua_isoneornil ( lua_State * lua, int index );

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

int lua_isnumber ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является числом (типа number) и ноль в противном случае.

int lua_isstring ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является строкой (типа string) и ноль в противном случае.

int lua_istable ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является таблицей (типа table) и ноль в противном случае.

int lua_isuserdata ( lua_State * lua, int index );

Возвращает единицу, если значение по заданному индексу является типа userdata и ноль в противном случае.

int lua_toboolean ( lua_State * lua, int index );

Возвращает единицу, если значение, расположенное по заданному индексу истинно и ноль в противном случае.

lua_Integer lua_tointeger ( lua_State * lua, int index );

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

const char * lua_tolstring ( lua_State * lua, int index, size_t * length );

Преобразует значение со стека по заданному индексу в строку, если length не равен NULL, то в него заносится длина строки. Возвращается указатель на завершенную '\0' строку, расположенную во внутреннем буфере Lua. Соответствующее значение должно быть строкой или числом.

lua_Number lua_tonumber ( lua_State * lua, int index );

Преобразует значение по заданному индексу в число (обычно это double) и возвращает его. Значение должно быть строкой или числом.

const char * lua_tostring ( lua_State * lua, int index );

Эквивалентно lua_tolstring ( lua, index, NULL ).

void * lua_tosuserdata ( lua_State * lua, int index );

Если значение по заданному индексу имеет тип userdata, то возвращает указатель на соответствующий блок памяти. Иначе возвращает NULL.

int lua_type ( lua_State * lua, int index );

Возвращает тип объекта по заданному индексу или LUA_TNONE в случае недопустимого индекса. Тип объекта принимает одно из следующих значений - LUA_TNIL, LUA_TNUMBER, LUA_TBOOLEAN, LUA_TSTRING, LUA_TTABLE, LUA_TFUNCTION, LUA_TUSERDATA, LUA_TTHREAD и LUA_TLIGHTUSERDATA.

const char * lua_typename ( lua_State * lua, int typeCode );

Переводит константы типов в соответствующие строковые значения.

void lua_register ( lua_State * lua, const char * name, lua_CFunction func  );

Создает новую глобальную функцию с именем name, на основе С-функции и делает ее доступной программам на Lua.

Тип lua_CFunction определен следующим образом:

typedef int (*lua_CFunction) ( lua_State * );

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

void lua_rawget ( lua_State * lua, int index );

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

void lua_rawgeti ( lua_State * lua, int index, int n );

Если через t обозначить таблицу по индексу index в стеке, то данный вызов возвращает значение (без использования метаметодов) t[n].

void lua_rawset ( lua_State * lua, int index );

Полностью аналогично lua_settable, но для записи в таблицу не использует метаметоды.

void lua_rawseti ( lua_State * lua, int index, int n );

Если через t обозначить таблицу по индексу index в стеке, а через v значение на вершине стека, то данный вызов делает прямую запись (без использования метаметодов) t[n]=v.

Пусть у нас есть следующий фрагмент кода на Lua:

a = f ( "how", t.x, 14 )

Он будет эквивалентен следующему фрагменту кода на С/С++.

lua_getfield    ( lua, LUA_GLOBALSINDEX, "f" );   // push global function f on stack
lua_pushstring  ( lua, "how" );                   // push first argument on stack
lua_getfield    ( lua, LUA_GLOBALSINDEX, "t" );   // push global table t on stack
lua_getfield    ( lua, -1, "x" );                 // push t.x on stack
lua_remove      ( lua, -2 );                      // remove t from stack
lua_pushinteger ( lua, 14 );                      // push last argument
lua_call        ( lua, 3, 1 );                    // call function taking 3 argsuments and getting one return value
lua_setfield    ( lua, LUA_GLOBALSINDEX, "a" );   // store result from top of stack to global variable a

Рис. 7. Стек во время вызова.

Кроме перечисленных функция в C API также в входит дополнительная библиотека. Заголовочным файлом для нее служит файл lauxlib.h, а имена всех функций начинаются с префикса luaL_.

Все функции дополнительной библиотеки построены на основе основного API, но в ряде случаев более удобны.

void luaL_openlibs ( lua_State * lua );

Открывает и делает доступными в текущем контексте для скриптов на Lua все стандартные библиотеки.

int luaL_callmeta ( lua_State * lua, int index, const char * eventName );

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

int luaL_loadfile ( lua_State * lua, const char * fileName );

Загружает файл с именем fileName и помещает его на стек как функцию (поэтому после загрузки необходимо сделать вызов lua_call или lua_pcall).

При успехе возвращает 0, иначе один из следующих кодов - LUA_ERRSYNTAX, LUA_ERRMEM или LUA_ERRFILE.

int luaL_loadstring ( lua_State * lua, const char * str );

Загружает код на Lua из строки и помещает его на стек как функцию.

int luaL_dofile ( lua_State * lua, const char * fileName );

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

(luaL_loadfile(lua, filename) || lua_pcall(lua, 0, LUA_MULTRET, 0))

void luaL_register ( lua_State * lua, const char * libName, const lua_Reg * reg );

Открывает библиотеку с заданным именем libName. Если libName равно NULL, то просто регистрирует все функции из массива reg (концом массива служит запись с обоими значениями равными NULL).

Если libName не равно NULL, то создается новая таблица как глобальная переменная и именем модуля и в ней регистрируются все функции из массива reg.

Примеры интеграции скриптов на Lua в программы на C/C++

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

-- simple test of calling lua from C

print ( 'Hello World !' )

Ниже приводится код на С++. Его задача создать контекст, открыть стандартные библиотеки и выполнить заданный файл.

Все это и делает приведенный ниже исходный код.

#ifdef	__cplusplus
extern "C" {
#endif

#include	"lua.h"
#include	"lauxlib.h"
#include	"lualib.h"

#ifdef	__cplusplus
};
#endif

int main ()
{
    lua_State * lua = lua_open ();         // create Lua context

    if ( lua == NULL )
    {
        printf ( "Error creating Lua context.\n" );

        return 1;
    }
	
    luaL_openlibs ( lua );                  // open standart libraries
                                            // load and execute a file
    if ( luaL_dofile ( lua, "test-1.lua" ) )
        printf ( "Error opening test-1.lua\n" );

    lua_close     ( lua );                  // close Lua context
	
    return 0;
}

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

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

function foo ( x, y )
   print ( 'foo: ', x, y )
   return x + y
end

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

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

#ifdef	__cplusplus
extern "C" {
#endif

#include	"lua.h"
#include	"lauxlib.h"
#include	"lualib.h"

#ifdef	__cplusplus
};
#endif

int main ()
{
    lua_State * lua = lua_open ();                      // create Lua context

    if ( lua == NULL )
    {
        printf ( "Error creating Lua context.\n" );

        return 1;
    }
	
    luaL_openlibs ( lua );                              // open standart libraries
                                                        // load and execute a file
    if ( luaL_loadfile ( lua, "test-2.lua" ) )
        printf ( "Error opening test-2.lua\n" );

    lua_pcall       ( lua, 0, LUA_MULTRET, 0 );
    lua_getfield    ( lua, LUA_GLOBALSINDEX, "foo" );   // push global function f on stack
    lua_pushstring  ( lua, "17" );                      // push first argument on stack
    lua_pushinteger ( lua, 3 );                         // push second argument on stack
    lua_pcall       ( lua, 2, 1, 0 );                   // call function taking 2 argsuments and getting one return value

                                                        // get return value and print it
    printf ( "Result: %d\n", lua_tointeger ( lua, -1 ) );

    lua_close       ( lua );                            // close Lua context

    return 0;
}

Наш следующий пример сделает обратное - определит функцию на С++ и проэкспортирует ее в Lua. После этого будет выполнен скрипт, который вызовет нашу функцию.

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

av, n = ave ( 1, 2, 3, 4, 5, 6, 7, 8 )
print ( 'Average ', av )
print ( 'Count ', n )

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

#ifdef	__cplusplus
extern "C" {
#endif

#include	"lua.h"
#include	"lauxlib.h"
#include	"lualib.h"

#ifdef	__cplusplus
};
#endif


int	ave ( lua_State * lua )                             // function to be exported to Lua
{
    int     num = lua_gettop ( lua );                   // get # of arguments
    double  sum = 0;

    for ( int i = 1; i <= num; i++ )
        sum += lua_tonumber ( lua, i );

    lua_pushnumber ( lua, sum / num );                  // return average
    lua_pushnumber ( lua, num );                        // return # of arguments

    return 2;                                           // we returned two vaues
}

int main ()
{
    lua_State * lua = lua_open ();                      // create Lua context

    if ( lua == NULL )
    {
        printf ( "Error creating Lua context.\n" );

        return 1;
    }
	
    luaL_openlibs ( lua );                              // open standart libraries
    lua_register  ( lua, "ave", ave );                  // register out function
                                                        // load and execute a file
    if ( luaL_dofile ( lua, "test-3.lua" ) )
        printf ( "Error opening test-3.lua\n" );
	
    lua_close     ( lua );                              // close Lua context

    return 0;
}

Это первая часть, посвященная Lua. Во второй части будет рассмотрена работа с программой toLua, позволяющей автоматически генерировать интерфейсы с С/С++ для Lua, а также будет рассмотрен JIT-компилятор для Lua - LuaJIT.

По этой ссылке можно скачать весь исходный код к этой статье.

Valid HTML 4.01 Transitional

Напиши мне
Используются технологии uCoz