воскресенье, 20 января 2008 г.

Lua и C++, объекты.

Сейчас практически почти каждый язык программирования имеет встроенную поддержку ООП, С++ имеет, а Lua к сожалению нет :'( Точнее имеет, но несколько странную... Основная структура данных в языке lua - таблицы. В качестве ключей и значений таблицы, используются переменные, значения которых, типизируются динамически. Это означает что таблица может содержать пары, ключ - значение, разных типов. Таблица - единственная структура данных в lua. С помощью таблиц реализуются все остальные структуры данных, а так-же объекты. Например таблица, в качестве ключей которой используются численные значения, является аналогом одномерного массива. Таблица так-же может содержать функции.
--пример таблицы ... а это комментарий
local table = {}
table["pi"] = 3.14159
table[1] = "some string"
table[3.14159] = "pi"
Язык lua позволяет перегружать операции для таблиц и userdata, это делается с помощью мета-таблиц. Мета-таблица связывает события и их обработчики и представляет из себя обычную таблицу, в качестве ключей которой используются строки - имена событий, а в качестве значений - функции - обработчики событий. В свою очередь с каждой мета-таблицей, может быть связана еще одна мета-таблица, если событие не определено в первой мета-таблице, то оно ищется во второй, и так далее... События (ключи таблицы) - могут иметь как произвольные значения, так и предопределенные (всегда начинаются с "__"), например, "__add" - перегружает операцию сложения, "__gc" - вызывается перед удалением сборщиком мусора, ключ "__metatable" может содержать другую мета-таблицу и тд... Синтаксис языка предполагает использование мета-таблиц, для создания объектов, для этого предусмотрен оператор : (двоеточие). При вызове функции из таблицы, с помощью этого оператора, в качестве первого параметра будет неявно передана эта таблица. Теперь о том как это все работает. В языках программирования поддерживающих классы, объекты всегда создаются на основе информации о типах. В lua классов нет, но объекты все-таки можно создавать. Скрипт должен содержать код, для создания экземпляра объекта. Для этого он должен, создать новую таблицу и мета-таблицу, содержащую все методы объекта. Ниже приведен код, котрый позволяет использовать объекты класса PersonInfo, в lua скрипте. Для взаимодействия скрипта с объектами PersonInfo, предназначен класс PersonInfoBinder.
extern "C" {
#    include "lua.h"
#    include "lauxlib.h"
#    include "lualib.h"
}
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <conio.h>
class PersonInfo
{
    friend class PersonInfoBinder;
protected:
    std::string name;
    std::string sname;
    int age;
public:
    
    static int ObjCount;
    
    PersonInfo():
        name(),
        sname(),
        age(0)
    {
    }
    //конструктор    
    PersonInfo(const char* n, const char* sn, int a):
        name(n),
        sname(sn),
        age(a)
    {
        ObjCount++;
    }
    //деструктор
    virtual ~PersonInfo()
    {
        ObjCount--;
    }
    //метод который мы будем вызывать с помощью Lua
    virtual void ShowInfo()
    {
        printf("PersonInfo %s %s, age %d\n",name.c_str(), sname.c_str(), age);        
    }
};
int PersonInfo::ObjCount = 0;
//размещающий оператор new, для создания объекта внутри userdata 
void* operator new(size_t size, lua_State* L, const char* metatableName)
{    //создаем userdata на вершине стэка
    void* ptr = lua_newuserdata(L, size);
    /* состояние стэка
     * [-1] - объект userdata
     */
    //кладем на вершину стэка metatable с именем metatableName
    luaL_getmetatable(L, metatableName);
    /* состояние стэка
     * [-1] - metatable
     * [-2] - объект userdata
     */
    //привязываем metatable к объекту userdata
    lua_setmetatable(L, -2);
    return ptr;
}
//Класс через который интерпретатор Lua может работать с объектами PersonInfo
class PersonInfoBinder
{
public:
    typedef PersonInfo* pPersonInfo;
    
    //метод возвращает указатель на PersonInfo или NULL
    static pPersonInfo checkPersonInfo(lua_State* L, int ix)
    {    //если на вершине стэка объект userdata
        if (lua_isuserdata(L,ix))
        {    //возвращаем указатель на объект
            void* udata = luaL_checkudata(L,ix,"PersonInfo");
            if (udata != NULL) //если получится :-)
                return (pPersonInfo)udata;
        }
        return NULL;
    }
    
    //Ф-я вызывает метод ShowInfo объекта находящегося на вершине стэка
    static int Print(lua_State* L)
    {    //получаем указатель на объект
        pPersonInfo p = checkPersonInfo(L, 1);
        //и надеемся на удачу
        p->ShowInfo();
        return 0;
    }
    
    //метод создает объект PersonInfo
    static int New(lua_State* L)
    {    //получаем черезстэк параметры для создания
        const char* name = lua_tostring(L,1);
        const char* sname= lua_tostring(L,2);
        int age =  lua_tointeger(L,3);
        //создаем объект PersonInfo на вершине стэка
        pPersonInfo p = new (L, "PersonInfo") PersonInfo(name, sname, age);
        return 1;//сообщаем интерпретатору, что ф-я вернула один параметр
    }
    //Деструктор для объекта - функция вызываетс для объекта во время сборки мусора
    static int GC(lua_State* L)
    {    //единственный параметр - указатель на уничтожаемый объект
        pPersonInfo p = checkPersonInfo(L, 1);
        //вызываем деструктор явно, Lua самостоятельно освободит память занимаемую userdata
        p->~PersonInfo();
        return 0;
    }    
    //Ф-я делает объект типа PersonInfo доступным интерпретатору
    static int Bind(lua_State* L)
    {    //методы которые содержит метатаблица объекта
        static const luaL_Reg somebody_meta[] = {
          {"__gc",  &GC},
          {0, 0}
        };
        //методы самого объекта
        static const luaL_Reg somebody_methods[] = {
          {"New",  &New},
          {"Print", &Print},
          {0, 0}
        };
        //Ф-я создает глобальную переменную (таблицу) PersonInfo, и записывает в нее все методы из somebody_methods
        //данная таблица будет общей для всех объектов этого типа
        luaL_register(L,"PersonInfo",somebody_methods);
        //используем таблицу PersonInfo, в качестве мета-таблицы
        luaL_newmetatable(L, "PersonInfo");
        //создаем еще одну таблицу, на этот раз в стеке, содержащую методы из массива somebody_meta
        luaL_register(L,NULL,somebody_meta);
        //metatable.__index = methods
        lua_pushliteral(L, "__index");
        //копируем таблицу методов
        lua_pushvalue(L, -3);   
        lua_rawset(L, -3);
        /* metatable.__index = methods */
        lua_pushliteral(L, "__metatable");
        //копируем таблицу методов
        lua_pushvalue(L, -3);     
        lua_rawset(L, -3);  
        lua_pop(L, 1); 
        return 1;
    }
};
int
main(void)
{
    int status, result;
    double sum;
    lua_State *L;
    L = lua_open();
    
    luaL_openlibs(L);
    PersonInfoBinder::Bind(L);
    
    status = luaL_loadfile(L, "script.lua");
    if (status) {
        (void)fprintf(stderr, "Syntax error\n");
        getch();
        exit(1);
    }
    result = lua_pcall(L, 0, LUA_MULTRET, 0);
    if (result) {
        fprintf(stdout, "runtime error\n");
        getch();
        exit(1);
    }
    lua_close(L); 
    
    printf("Objects: %d", PersonInfo::ObjCount);
    getch();
    return 0;
}
Сценарий, который этот код выполняет:
-- script.lua
-- ввод значений пользователем
function read_record(table)
    io.write("first name: ")
    name = io.read()
    io.write("second name: ")
    sname = io.read()
    io.write("age: ")
    agestr = io.read()
    age = tonumber(agestr)
    local person = PersonInfo.New(name, sname, age)
    table[name..' '..sname] = person
end
-- вывод значений таблицы
function find_record(table)
    name = io.read()
    if table[name] ~= nil then
        table[name]:Print()
    else
        print("\a\r")
    end    
end
-- Удаление значений из таблицы
function remove_record(table)
    name = io.read()
    if table[name] ~= nil then
        table[name] = nil
    else
        print("\a\r")
    end    
end
-- пустая таблица
table = {}
command = ""
while command ~= "quit" do
    io.write(">> ")
    command = io.read()
    if command == "quit" then
        break
    elseif command == "find" then
        find_record(table)
    elseif command == "add" then
        read_record(table)
    elseif command == "remove" then
        remove_record(table)
    else
        print("\a\r")
    end
end
return 0

Комментариев нет: