Интеграция скриптов на Lua с С++.

Решив использовать в своей программе скриптовый язык Lua разработчик сталкивается с необходимостью обеспечения интеграции скриптов на Lua с основным кодом на С/С++.

В основном эта интеграция состоит из двух частей -

Во-первых, вызов функций, написанных на Lua, из C/C++.

Во-вторых, передача функций и структур/классов из С++ в Lua.

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

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

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

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

Использование template'ов.

Этот подход основан на мощной поддержке template'ов в языке С++ (фактически они образуют свой полноценный язык программирования). Наиболее известной библиотекой, активно использующей эти возможности (IMHO более правильно было сказать abusing) является boost. В частности в boost входит модуль luabind, служащий для интеграции С++ и Lua.

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

Тем не менее за счет использования template'ов можно получить вполне работающие решения, достаточные в целом ряде случаев.

Ниже предлагается мой подход, в его основу легли примеры с сайта www.ilovelua.narod.ru.

Основная идея заключается в предоставлении template-ных функций-преобразователей из типов языка С++ и STL в типы Lua и обратно. Использование для этих целей template-ов позволяет легко добавлять преобразователи для своих типов. В качестве таких функций преобразователей выступают функции tolua и fromLua.

template<typename T> bool fromLua ( lua_State * lua, int index, T& ret );
template<typename T> void toLua   ( lua_State * lua, T& arg );

Функция fromLua берет значение из стека по заданному индексу и записывает его значение по ссылке в параметр ret.

Аналогично функция tolua берет ссылку на значение и помещает его на стек Lua.

Легко добавляется поддержка ряда стандартных типов STL, таких как std::string, std::map и др.

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

Задачей такого template'а является получение вызываемой функции по имени (и помещение ее на стек), последовательность вызовов toLua, помещающих все входные аргументы на стек, сам вызов функции и получение возвращаемого значения со стека и его запись в соответствующий параметр.

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

Ниже приводится полный листинг файла lua-templ.h, содержащий все необходимые template'ы и используемый в дальнейших примерах.

//
// Simple templated Lua wrapper.
// Based on code from http://www.ilovelua.narod.ru
//

#ifndef __LUA_TEMPLATES__
#define __LUA_TEMPLATES__

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    <string>
#include    <map>

template<typename T> bool fromLua ( lua_State * lua, int index, T& ret );
template<typename T> void toLua   ( lua_State * lua, T& arg );

template<> bool fromLua ( lua_State * lua, int index, bool& ret )
{
    if( !lua_isboolean ( lua, index ) )
        return false;
        
    ret = lua_toboolean ( lua, index) != 0;
    return true;
}

template<> bool fromLua ( lua_State * lua, int index, float& ret)
{
    if( !lua_isnumber ( lua, index ) )
        return false;
        
    ret = (float) lua_tonumber ( lua, index );
    return true;
}

template<> bool fromLua ( lua_State * lua, int index, double& ret)
{
    if(!lua_isnumber ( lua, index ))
        return false;
        
    ret = lua_tonumber ( lua, index );
    return true;
}

template<> bool fromLua ( lua_State * lua, int index, int& ret)
{
    if( !lua_isnumber ( lua, index ))
        return false;
 
    ret = (int)lua_tonumber ( lua, index );
    return true;
}

template<> bool fromLua(lua_State * lua, int index, std::string& ret)
{
    if( !lua_isstring ( lua, index))
        return false;
        
    ret = lua_tostring ( lua, index);
    return true;
}

template<typename Key, typename Value>
bool fromLua ( lua_State * lua, int index, std::map<Key, Value>& ret)
{
    if(!lua_istable ( lua, index))
        return false;

    lua_pushvalue ( lua, index );       // stack: map
    lua_pushnil   ( lua );              // stack: map nil
    
    while(lua_next ( lua, -2 ))         // stack: map key value
    {
        Key     key;
        Value   value;
        
        fromLua ( lua, -2, key );
        fromLua ( lua, -1, value);
        
        ret [key] = value;
        
        lua_pop ( lua, 1 );             // stack: map key
    }
    
    lua_pop ( lua, 1);                  // stack:

  return true;
}

template<typename T>
bool fromLuaTable ( lua_State * lua, int index, const char * key, T& ret)
{
    lua_getfield ( lua, index? index : LUA_GLOBALSINDEX, key ); // stack: table value

    bool res = fromLua ( lua, -1, ret);

    lua_pop ( lua, 1 ); // stack: table

    return res;
}

template<> void toLua ( lua_State * lua, const bool& arg )
{
    lua_pushboolean ( lua, arg ? 1 : 0 );
}

template<> void toLua ( lua_State * lua, bool& arg )
{
    lua_pushboolean ( lua, arg ? 1 : 0 );
}

template<> void toLua ( lua_State * lua, const float& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, float& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, const double& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, double& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, const int& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, int& arg )
{
    lua_pushnumber ( lua, arg );
}

template<> void toLua ( lua_State * lua, const std::string& arg )
{
    lua_pushstring ( lua, arg.c_str () );
}

template<> void toLua ( lua_State * lua, std::string& arg )
{
    lua_pushstring ( lua, arg.c_str () );
}

/////////////// templates to call Lua functions passsing arguments /////////////////////

template <typename R> 
static inline bool callLua0 ( lua_State * lua, const char * funcName, R& res )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    lua_pcall    ( lua, 0, 1, 0 );                      // call function taking no argsuments and getting one return value
    fromLua      ( lua, res );
	lua_pop      ( lua, 1 );
    
    return true;
}

template <typename R> 
static inline void callLua0 ( lua_State * lua, const char * funcName )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    lua_pcall    ( lua, 0, 1, 0 );                      // call function taking no argsuments and getting one return value
}

template <typename R, typename A> 
static inline bool    callLua1 ( lua_State * lua, const char * funcName, R& res, A arg )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg );                          // push first argument on stack
    lua_pcall    ( lua, 1, 1, 0 );                      // call function taking 1 argsuments and getting one return value
    fromLua      ( lua, -1, res );
	lua_pop      ( lua, 1 );
    
    return true;
}

template <typename A> 
static inline void    callLua1 ( lua_State * lua, const char * funcName, A arg )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg );                          // push first argument on stack
    lua_pcall    ( lua, 1, 1, 0 );                      // call function taking 1 argsuments and getting one return value
}

template <typename R, typename A1, typename A2> 
static inline bool    callLua2 ( lua_State * lua, const char * funcName, R& res, A1 arg1, A2 arg2 )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg1 );                         // push first argument on stack
    toLua        ( lua, arg2 );                         // push 2nd argument on stack
    lua_pcall    ( lua, 2, 1, 0 );                      // call function taking 2 argsuments and getting one return value
    fromLua      ( lua, -1, res );
	lua_pop      ( lua, 1 );
    
    return true;
}

template <typename A1, typename A2> 
static inline void    callLua2 ( lua_State * lua, const char * funcName, A1 arg1, A2 arg2 )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg1 );                         // push first argument on stack
    toLua        ( lua, arg2 );                         // push 2nd argument on stack
    lua_pcall    ( lua, 2, 1, 0 );                      // call function taking 2 argsuments and getting one return value
}

template <typename R, typename A1, typename A2, typename A3> 
static inline bool    callLua3 ( lua_State * lua, const char * funcName, R& res, A1 arg1, A2 arg2, A3 arg3 )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg1 );                         // push first argument on stack
    toLua        ( lua, arg2 );                         // push 2nd argument on stack
    toLua        ( lua, arg3 );                         // push 3rdt argument on stack
    lua_pcall    ( lua, 2, 1, 0 );                      // call function taking 2 argsuments and getting one return value
    fromLua      ( lua, -1, res );
	lua_pop      ( lua, 1 );
    
    return true;
}

template <typename A1, typename A2, typename A3> 
static inline void    callLua3 ( lua_State * lua, const char * funcName, A1 arg1, A2 arg2, A3 arg3 )
{
    lua_getfield ( lua, LUA_GLOBALSINDEX, funcName );   // push global function f on stack
    toLua        ( lua, arg1 );                         // push first argument on stack
    toLua        ( lua, arg2 );                         // push 2nd argument on stack
    toLua        ( lua, arg3 );                         // push 3rdt argument on stack
    lua_pcall    ( lua, 2, 1, 0 );                      // call function taking 2 argsuments and getting one return value
}

///////////////////////// template to wrap function for Lua /////////////////////

template <typename R, R (*func)()>
class   WrapFunc0
{
public:
    static int f ( lua_State * lua )
    {
        R   r = func ();
        
        toLua   ( lua, r );
        
        return 1;                                       // return one vaue
    }
    
    void    registerFunc ( lua_State * lua, const char * name )
    {
        lua_register ( lua, name, f );
    }
};

template <typename R, typename A, R (*func)(A)>
class   WrapFunc1
{
public:
    static int f ( lua_State * lua )
    {
        A   a;
        R   r;
        
        fromLua ( lua, 1, a );
        
        r = func ( a );
        
        toLua   ( lua, r );
        
        return 1;                                       // return one vaue
    }
    
    void    registerFunc ( lua_State * lua, const char * name )
    {
        lua_register ( lua, name, f );
    }
};

template <typename R, typename A1, typename A2, R (*func)(A1,A2)>
class   WrapFunc2
{
public:
    static int f ( lua_State * lua )
    {
        A1  a1;
        A2  a2;
        R   r;
        
        fromLua ( lua, 1, a1 );
        fromLua ( lua, 2, a2 );
        
        r = func ( a1, a2 );
        
        toLua   ( lua, r );
        
        return 1;                                       // return one vaue
    }
    
    void    registerFunc ( lua_State * lua, const char * name )
    {
        lua_register ( lua, name, f );
    }
};

template <typename R, typename A1, typename A2, typename A3, R (*func)(A1,A2,A3)>
class   WrapFunc3
{
public:
    static int f ( lua_State * lua )
    {
        A1  a1;
        A2  a2;
        A3  a3;
        R   r;
        
        fromLua ( lua, 1, a1 );
        fromLua ( lua, 2, a2 );
        fromLua ( lua, 3, a3 );
        
        r = func ( a1, a2, a3 );
        
        toLua   ( lua, r );
        
        return 1;                                       // return one vaue
    }
    
    void    registerFunc ( lua_State * lua, const char * name )
    {
        lua_register ( lua, name, f );
    }
};

#endif

Ниже приводится пример использования данного файла для вызова функции на Lua (данный пример взят из предыдущей статьи). Обратите внимание, что template'ы для вызова функции принимают (и возвращают) все значения по ссылке, именно поэтому для передачи строки "17" использована конструкция std::string("17") - ссылку на const char * передавать нельзя. Также данный подход не поддерживает работу с функциями, не возвращающими никаких значений (хотя легко можно написать специальные template'ы для этих случаев).

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    "lua-templ.h"

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 ); 

    int res;

    callLua2 ( lua, "foo", res, std::string("17"), 3 );

    
    printf ( "Result: %d\n", res );
    
    lua_close       ( lua );                            // close Lua context
    
    return 0;
}

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

Следующий пример (опять на основе предыдущей статьи) демонстрирует использование template'ов для экспорта функций из С++ в Lua. Как уже было замечено поддерживаются только функции возвращаются значение - в принципе функцию типа void легко изменить добавив в конец что-нибудь вроде return 0;.

#include    <stdio.h>

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    "lua-templ.h"

double  foo1 ( double x, double y )
{
    return x*y;
}

int foo2 ( int x )
{
    printf ( "x = %d\n", x );
    
    return x + 2;
}

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
    
    WrapFunc2<double,double,double,foo1> ().registerFunc ( lua, "foo1" );
    WrapFunc1<int,int,foo2>              ().registerFunc ( lua, "foo2" );
    
                                                // 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;
}

Как видно, весь код для экспорта одной функции занимает ровно одну строку.

Использование генераторов кода

Наиболее известными генераторами, позволяющими строить необходимые stub-функции для вызова функций С++ из Lua, являются toLua и toLua++.

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

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

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

usage: tolua++ [options] input_file

Ниже приводятся основные опции tolua++ (их полный список можно получить используя опцию -h).

  -v       : печать версии.
  -o  file : задать имя выходного файла. По умолчанию вывод осуществляется на консоль.
  -H  file : создать заголовочный файл (описывающий созданные stub'ы) с заданным именем.
  -n  name : задать имя для модуля. По умолчанию в качестве имени для модуля используется имя входного файла (без пути).

Ниже приводится фрагмент Makefile'а, вызывающего toLua++ для создания stub-функций.

ex_4_reg.cpp: ex_4.pkg ex_4.h
	tolua++ -n ex_4 -o ex_4_reg.cpp -H ex_4_reg.h ex_4.pkg

ex_5_reg.cpp: ex_5.pkg ex_5.h
	tolua++ -n ex_5 -o ex_5_reg.cpp -H ex_5_reg.h ex_5.pkg

В качестве входного файла для toLua++ используется специальный pkg-файл, описывающий константы, функции, структуры и классы, которые надо проэкспортировать в Lua. Данный файл имеет расширение .pkg и является по сути "почищенным" заголовочным файлом с добавлением некоторых дополнительных конструкций.

Ниже приводится пример заголовочного файла ex_4.pkg, задающий экспорт двух функций (из прошлой статьи).

$#include "ex_4.h"

double  foo1 ( double x, double y );
void    foo2 ( int x, char * str );

Как видно, это фактически обычное описание функций, только вначале стоит специальная директива $#include. Она служит для вставки вызова соответствующего заголовочного файла (ex_4.h) в файл со stub'ами.

Существует ряд ограничений на то, что может быть в pkg-файле. В частности плохо обрабатываются template'ы (хотя полностью поддерживается std::string). Есть полная поддержка перегрузки функций и операторов, а также возможность добавления заданного Lua-кода в сгенерированные stub'ы.

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

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

#include    <stdio.h>

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    "tolua++.h"
#include    "ex_4_reg.h"

double  foo1 ( double x, double y )
{
    return x*y;
}

void    foo2 ( int x, char * str )
{
    printf ( "x = %d, str = %s\n", x, str );
}

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
    tolua_ex_4_open ( lua );
                                            // load and execute a file
    if ( luaL_dofile ( lua, "test-4.lua" ) )
        printf ( "Error opening test-4.lua\n" );
        
    lua_close     ( lua );                  // close Lua context
    
    return 0;
}

Основное, что нам нужно сделать - это подключение заголовочного файла tolua++.h, библиотеки tolua.lib и вызов инициализирующей функции tolua_ex_4_open (обратите внимание, что ex_4 - это имя модуля, заданное при вызове tolua++).

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

$#include "ex_5.h"

class   A
{
public:
    int     type;
    double  x;
    char    name [128];
    
    A ( const char * theName );
    
    virtual double  foo ();
};

class   B : public A
{
public:
    B ( const char * theName );
    
    virtual double  foo ();
};

Экспорт классов осуществляется также просто - для этого достаточно вызвать инициализацию модуля - функцию tolua_ex_5_open.

#include    <stdio.h>
#include    <string.h>

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    "tolua++.h"
#include    "ex_5.h"
#include    "ex_5_reg.h"

A :: A ( const char * theName ) 
{
    strcpy ( name, theName );
    type = 1;
    x    = 1;
}

double  A :: foo ()
{
    printf ( "A::foo\n" );
    
    return type * x;
}

B :: B ( const char * theName ) : A ( theName )
{
    type = 2;
    x    = 1;
}

double  B :: foo ()
{
    printf ( "B::foo\n" );
    
    return type * x + 0.7;
}

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
    tolua_ex_5_open ( lua );
                                            // load and execute a file
    if ( luaL_dofile ( lua, "test-5.lua" ) )
        printf ( "Error opening test-5.lua\n" );
        
    lua_close     ( lua );                  // close Lua context
    
    return 0;
}

Ниже приводится файл test-5.lua, демонстрирующий работу с экспортированными классами.

print ( 'Test-5 - exporting classes to Lua' )

a = A:new ( "name 1" )
b = B:new ( "name 2" )

print ( a.type, a.x, a.name )
print ( b.type, b.x, b.name )

a:foo ()
b:foo ()

Следующий пример использует в классах тип std::string и также демонстрирует передачу объектов из С++ в скрипты на Lua, и возвращение объектов из Lua в основной код.

#include    <stdio.h>
#include    <string.h>

#ifdef  __cplusplus
extern "C" {
#endif

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

#ifdef  __cplusplus
};
#endif

#include    "tolua++.h"
#include    "ex_6.h"
#include    "ex_6_reg.h"

A :: A ( const std::string& theName ) 
{
    name = theName;
    type = 1;
    x    = 1;
}

double  A :: foo ()
{
    printf ( "A::foo\n" );
    
    return type * x;
}

B :: B ( const std::string& theName ) : A ( theName )
{
    type = 2;
    x    = 1;
}

double  B :: foo ()
{
    printf ( "B::foo\n" );
    
    return type * x + 0.7;
}

int main ()
{
    lua_State * lua = lua_open ();          // create Lua context
    
    if ( lua == NULL )
    {
        printf ( "Error creating Lua context.\n" );
        
        return 1;
    }
    
    A * a = new B ( "Test B object" );
    
    printf ( "type = %d, x = %g, name = \'%s\'\n", a -> type, a -> x, a -> name.c_str () );
    
    luaL_openlibs   ( lua );                                // open standart libraries
    tolua_ex_6_open ( lua );
    
    if ( luaL_loadfile ( lua, "test-7.lua" ) )
        printf ( "Error opening test-7.lua\n" );
        
    lua_pcall             ( lua, 0, LUA_MULTRET, 0 );       
    lua_getfield          ( lua, LUA_GLOBALSINDEX, "foo" ); // push global function f on stack
    tolua_pushusertype    ( lua, (void *)a, "A" );          // push first argument on stack
    lua_pcall             ( lua, 1, 1, 0 );                 // call function taking 1 argsuments and getting one return value
    
    A * ptr = (A *) tolua_tousertype ( lua, -1, NULL );
                                                            // get return value and print it
    printf ( "Returned:\ntype = %d, x = %g, name = \'%s\'\n", ptr -> type, ptr -> x, ptr -> name.c_str () );
        
    lua_close     ( lua );                                  // close Lua context
    
    return 0;
}

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

tolua_pushusertype    ( lua, (void *)a, "A" );

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

A * ptr = (A *) tolua_tousertype ( lua, -1, NULL );

Соответствующая функция на Lua, использованная в этом пример приведена ниже

function foo ( a )
    print ( a.type, a.x, a.name )
    a:foo ()
    a.name = 'xxx'
    a.x = 7777
    return a
end

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

Также доступны откомпилированные версии для M$ Windows и Linux.

Valid HTML 4.01 Transitional

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