Lua Utils
Biblioteca utilitária para facilitar a integração de Lua com C++
Usando a biblioteca LuaUtils

Motivação

A biblioteca LuaUtils foi desenvolvida para auxiliar na interface entre aplicações C++ e scripts Lua 5.1 . Esta biblioteca utiliza o frmework Qt e portanto seu uso é apropriado apenas para aplicações Qt.

A biblioteca não tem como objetivo suplantar o uso da API padrão de Lua 5.1, mas sim complementar esta API, facilitando a interface quando necessário. Seu uso não foi atualizado para a versão 5.2 e requer (pequenas) mudanças.

Seu desenvolvimento tem sido pautado pela necessidade de uso, assim algumas funcionalidades "que obviamente deveriam estar incluidas", não estão, apenas porque ainda não foram necessárias nos contextos em que a biblioteca já foi utilizada.

Além disso, algumas funcionalidades poderiam ter sua sintaxe um pouco modificada para unificar a sintaxe de algumas operações. Um trabalho a ser feito!

As seções a seguir apresentam exemplos de como utilizar a biblioteca em tarefas comuns de interface entre o universo C++ e o universo Lua. Cabe salientar que a descrição assume que o leitor já está familiarizado com a API de Lua e com o mecanismo de passagem de parâmetros entre os ambientes C e Lua através de uma pilha.

Exemplos de uso

Criando um novo ambiente Lua

A criação de um novo ambiente Lua é feita através da instanciação de um objeto LuaEnv, seguida pela chamada ao método newState() que cria o ambiente e abre as bibliotecas padrão. O ambiente NÃO é fechado quando o objeto LuaEnv for destruído. Para isto é necessário uma chamada a closeState().

Exemplo:

LuaEnv env;
env.newState();

Se for desejada a criação de um ambiente Lua temporário que será fechado quando o objeto LuaEnv sair de escopo, pode ser utilizado o padrão RAII através do uso de um objeto AutoLuaEnv que se encarrega de efetuar a chamada a newState() em sua construção e chamar closeState() em seu destrutor.

Para executar scripts no novo ambiente, utilize os métodos runScript() ou runScriptStr().

Trabalhando com um ambiente Lua existente

Para trabalhar com um ambiente Lua existente, basta instanciar um objeto LuaEnv passando como parâmetro o lua_State ao qual este embiente deverá estar associado. Isto é importante, por exemplo, para transformar o parâmetro lua_State recebido em todas as funções C chamadas de Lua para um LuaEnv.

Exemplo:

int LuaCalledFunction(lua_State* L)
{
LuaEnv env(L);
...
}

Lendo valores de variáveis globais

A leitura do valor de uma variável global Lua é feita simplesmente através da chamada do método LuaEnv::getGlobal() que recebe como parâmetro o nome da variável. O valor obtido é retornado como um objeto QVariant.

Esta função pode ser utilizada para obter dados dos tipos mais comuns de Lua, i.e. números, strings, booleans, tabelas, userdata, funções e nil. Demais tipos (threads e light userdata) não são suportados, e neste caso a função retorna um QVariant nulo.

Para variáveis do tipo numérico, string e booleano, basta utilizar as funções da classe QVariant para obtermos o valor desejado, verificar se o valor é nulo (QVariant::isNull()) ou mesmo descobrir seu tipo (QVariant::type() == QVariant::Double, QVariant::String ou QVariant::Bool).

Exemplos de uso:

LuaEnv env(L); // L é o estado Lua de trabalho
// Converte valores para int, double, QString, bool.
// Nao testa se o valor retornado é nil. Se for, variáveis vão
// receber o valor default para cada tipo. O mesmo ocorre
// se o tipo da variável global for diferente do tipo para
// o qual fizemos a conversão
int a = env.getGlobal("global_name").toInt();
double b = env.getGlobal("global_name").toDouble();
QString c = env.getGlobal("global_name").toString();
bool d = env.getGlobal("global_name").toBool();
// Testa se a variavel v é nula
QVariant v = env.getGlobal("global_name");
if(v.isNull())
printf("global contém o valor nil");
// Testa se a variavel é do tipo numérico
if(env.getGlobal("global_name").type() == QVariant::Double)
printf("global contém valor numérico");

Para objetos do tipo tabela, userdata e função, consulte as seções Trabalhando com tabelas, Trabalhando com userdata, Trabalhando com tipos registrados via tolua++ e Chamando funções Lua.

Por default, o objeto lido não é mantido na pilha de Lua, a não ser que o objeto seja uma tabela ou uma função. Este comportamento pode ser alterado com um segundo parâmetro passado para getGlobal(). Veja a documentação da função LuaEnv::getGlobal() para mais detalhes.

Escrevendo valores em variáveis globais

A escrita de um valor em uma variável global Lua é feita simplesmente através da chamada do método LuaEnv::setGlobal() que recebe como parâmetro o nome da variável e seu valor como um QVariant.

Esta função pode ser utilizada para escrever dados dos tipos mais comuns de Lua, i.e. números, strings, booleans, tabelas, userdata, funções e nil.

Valores numéricos, booleanos e strings são convertidos para um QVariant automaticamente. Tabelas, userdata e funções devem ser convertidos explicitamente via QVariant::setValue() (tabelas e valores do tipo tolua++ possuem funções auxiliares para isto - ver Trabalhando com tabelas e Trabalhando com tipos registrados via tolua++).

Exemplos de uso:

LuaEnv env(L); // L é o estado Lua de trabalho
env.setGlobal("global_name", 3.14);
env.setGlobal("global_name", true);
env.setGlobal("global_name", "LuaUtils é demais");
LuaTable tab(&env); // Nova tabela
// Armazena tabela criando um QVariant explicitamente
v.setValue(tab);
env.setGlobal("global_name", v);
// Armazena tabela utilizando função auxiliar de LuaTable
env.setGlobal("global_name", tab.toVariant());

Trabalhando com tabelas

Tabelas são representadas por objetos da classe LuaTable. Estes objetos podem representar uma tabela nova ou uma tabela existente e provêem diversos métodos para sua manipulação.

Para criar uma nova tabela e colocá-la no topo da pilha, utilize o construtor LuaTable(LuaEnv*). Já para criar um objeto LuaTable que se refere a uma tabela existente, utilize o construtor LuaTable(LuaEnv*, int, bool). Este construtor recebe como segundo parâmetro o indice da pilha de Lua que se refere à tabela, e como terceiro parâmetro um booleano que indica se o objeto LuaTable deve criar uma referência para a tabela (true) ou apenas armazenar o índice da pilha (false). Esta última opção (false) é mais eficiente, porém o objeto LuaTable se mantém válido apenas enquanto a tabela estiver neste indice da pilha.

Quando uma tabela é retornada via uma chamada a LuaEnv::getGlobal(), por default, a tabela é mantida na pilha de Lua e o objeto LuaTable construído simplesmente armazena o indice da tabela. Este comportamento pode ser mudado através de parâmetro da chamada a LuaEnv::getGlobal().

Dado um objeto LuaTable, os métodos getIndex(), getField(), setIndex() e setField() podem ser chamados para ler e escrever na tabela. Seu funcionamento é análogo ao dos métodos LuaEnv::getGlobal() e LuaEnv::setGlobal() (ver Lendo valores de variáveis globais e Escrevendo valores em variáveis globais). Consulte a documentação da classe para demais opções disponíveis.

Para converter um QVariant v em um LuaTable use:

LuaTable tab = v.value<LuaTable>();

Um exemplo mais completo, obtendo a tabela de uma variavel global e verificando o tipo do retorno antes da criação da tabela pode ser feito com:

QVariant v = env.getGlobal("nome_tabela");
if(!v.isNull() && v.canConvert<LuaTable>())
{
LuaTable tab = v.value<LuaTable>();
}

Para converter um LuaTable em um QVariant use o método toVariant(). Exemplo:

LuaEnv env(L); // L é o estado Lua de trabalho
// Cria uma tabela. Preenche campos 1 e 2 com strings e seta a tabela
// como valor de uma variável global
LuaTable tab(&env); // Cria nova tabela
tab.setIndex(1, "string 1");
tab.setIndex(2, "string 2");
env.setGlobal("global_name", tab.toVariant());

Para colocar uma tabela na pilha use o método push(). Exemplo:

LuaTable tab(&env); // Cria nova tabela
tab.push(); // Coloca tabela no topo da pilha de Lua

Para criar um LuaTable a partir de uma entrada da pilha use:

// Exemplo assume que a pilha está na posição 3 da pilha. Obs: indices
// relativos (negativos) também são aceitos.
// Opcao 1: LuaTable guarda apenas indice da tabela na pilha de Lua.
// Sua posição na pilha não pode mudar.
LuaTable tab(&env, 3);
// Opcao 2: LuaTable guarda referência para a tabela. Posição da tabela
// na pilha de Lua pode ser alterada.
LuaTable tab(&env, 3, true);

Um exemplo mais completo, obtendo uma tabela dos parâmetros passados por Lua para uma função C:

int LuaCalledFunction(lua_State* L)
{
LuaEnv env(L);
// Verifica se a função possui uma tabela como parâmetro
if(lua_type(L, 1) != LUA_TTABLE) return luaL_typerror(L, 1, "table");
LuaTable tab(&env, 1);
...
}

Trabalhando com userdata

A classe LuaProxy provê um mecanismo para facilitar a exportação de objetos C++ para o ambiente Lua. Sob este framework, para exportar uma classe C++ MyClass para o ambiente Lua, deve ser criada uma segunda classe MyClassProxy que herda de LuaProxy::Base e define os metametodos utilizados para acessar o conteúdo de um objeto no ambiente Lua.

A classe LuaSimpleProxy provê uma implementação que permite passar para o ambiente Lua e recuperar novamente em C++ objetos de qualquer tipo, sem possibilidade de operar sobre o valor em Lua.

Consulte a documentação das classes LuaProxy e LuaSimpleProxy para maiores detalhes. Nos exemplos abaixo, considere o cenário acima de um objeto C++ de tipo MyClass com um Proxy correspondente de tipo MyClassProxy.

Para converter um QVariant v em um objeto do tipo Proxy, use o método estático toObjectOfClass().

MyClassProxy* proxy = LuaProxy::toObjectOfClass<MyClassProxy>(v);

Para converter um Proxy em um QVariant, use o método QVariant::setValue()

MyClassProxy* ptr = ...;
v.setValue((void*)ptr);

Para colocar um Proxy na pilha, use o método estático pushObject. Exemplo:

MyClassProxy* ptr = ...;

Para criar um Proxy a partir de uma entrada da pilha use os métodos estáticos toObjectOfClass() ou checkObjectOfClass() que opera de maneira similar às funções luaL_checkXXXX():

MyClassProxy* ptr = LuaProxy::toObjectOfClass<MyClassProxy>(L, stack_index);

Um exemplo mais completo, obtendo um Proxy dos parâmetros passados por Lua para uma função C:

int LuaCalledFunction(lua_State* L)
{
LuaEnv env(L);
// Verifica se a função possui um Proxy como parâmetro
MyClassProxy* ptr = LuaProxy::checkObjectOfClass<MyClassProxy>(L, 1);
...
}

Veja também em Associando métodos a um userdata um exemplo completo de um objeto Proxy que exporta uma série de métodos para o ambiente Lua.

Para trabalhar com userdatas do tipo "light", a classe LuaLightUserdata fornece um framework similar àquele apresentado pela classe ToluaUserdata descrito na próxima seção.

Trabalhando com tipos registrados via tolua++

A ferramenta tolua++ é um gerador de código fonte utilizado para criar automaticamente um "binding" Lua para um conjunto de classes em C++. A biblioteca Sg (Smart Graphics) utiliza esta ferramenta para exportar seus objetos para Lua.

Para facilitar o trabalho com objetos exportados via tolua++, a biblioteca LuaUtils fornece a classe ToluaUserdata. Nos exemplos abaixo, considere uma classe chamada SgGraphic exportada para o ambiente lua via toloua++;

Para converter um QVariant v no objeto tolua++ contido por este, use a macro ToluaUserdataValue(). Exemplo:

SgGraphic* g = ToluaUserdataValue(v, SgGraphic);

Outra alternativa que fornece mais controle sobre verificação de tipos consiste em:

QVariant v = env.getGlobal("nome_global");
if(!v.isNull() && v.canConvert<ToluaUserdata>())
{
ToluaUserdata ud = v.value<ToluaUserdata>();
if(ud.inheritsType("SgGraphic"))
{
SgGraphic* g = ud.value<SgGraphic>();
}
}

Para criar um QVariant a partir de um objeto exportável para Lua via tolua++, crie um objeto do tipo ToluaUserdata e use o método toVariant() deste objeto. Exemplo:

LuaEnv env(L); // L é o estado Lua de trabalho
SgGraphic graph = ....
ToluaUserdata g(&env, graph, "SgGraphic");
env->setGlobal("global_name", g.toVariant());

De maneira análoga, para colocar na pilha um objeto exportado via tolua++, crie um objeto do tipo ToluaUserdata e use o método push() deste objeto.

Exemplo:

LuaEnv env(L); // L é o estado Lua de trabalho
SgGraphic graph = ....
ToluaUserdata g(&env, graph, "SgGraphic");
g.push();

Para criar um objeto a partir de um dado na pilha de Lua, podem ser utilzados tanto a função padrão tolua_tousertype() quanto a criação de um objeto ToluaUserdata que já integra uma verificação de tipos. Exemplos:

// Opcao 1. Nenhuma verificação de tipo
SgGraphic* g = (SgGraphic*)tolua_tousertype(L, stack_index, NULL);
// Opcao 2. Com verificação de tipos
ToluaUserdata ud(&env, stack_index, "SgGraphic");
if(!ud.isNull())
{
SgGraphic* g = ud.value<SgGraphic>();
}

Chamando funções Lua

A chamada de funções em Lua a partir de código em C++ pode ser feita através do uso da classe LuaFunction que modela uma função em Lua. Seu uso é análogo ao uso da classe LuaTable, na medida em que mantém internamente uma referência para uma função Lua (que pode ser uma referência real ou apenas um indice na pilha). Operações para transformar um LuaFunction de/para QVariant e para incluir ou retirar uma LuaFunction da pilha seguem os mesmos passos descritos para a classe LuaTable (ver Trabalhando com tabelas).

Dado um objeto do tipo LuaFunction, a chamada da função se dá através do método call() que recebe os parâmetros a serem passados como um QVariant. Todas as chamadas são efetuadas em modo protegido. Em caso de erro a chamada a call() retorna false e uma mensagem de erro. Os resultados da chamada são colocados na pilha de Lua e devem ser acessados a partir desta.

A classe provê ainda métodos estáticos LuaFunction::callFunction() que recebem o nome da função e efetuam sua chamada instanciando um objeto LuaFunction internamente. Em geral esta é a maneira mais simples de efetuar uma chamada. Exemplos:

// Chamando uma função de nome "GlobalFunctionName" que recebe como parâmetros
// um número, uma string e uma tabela e retorna 2 valores na pilha de Lua,
// um numero e uma string
LuaEnv env(L); // L é o estado Lua de trabalho
LuaTable tab = ... // Obtem a tabela a ser passada como parâmetro
QString err;
if(!LuaFunction::callFunction(&env, "GlobalFunctionName", 3.14, "uma string",
tab.toVariant(), 2, err))
{
printf("Erro chamando função: %s", qPrintable(err));
return;
}
// Obtem resultados da pilha. Observe a ordem dos resultados na mesma
double result1 = env.toVariant(LuaEnv::STACK_POP).toDouble();

A função LuaTable disponibiliza a função callMethod() para efetuar a chamada de uma função que reside como um campo da tabela. Estas chamadas são feitas da mesma maneira que uma chamada a LuaFunction::call(), porém adicionam automaticamente como primeiro parâmetro da chamada a própria tabela. Desta forma, seu uso é apropriado para a execução de funções em Lua que devem ser chamadas via a sintaxe de chamada de método tabela:metodo().

Cadastrando funções

O cadastro de funções em C para que estas possam ser chamadas a partir do ambiente Lua é feito através do uso da função LuaEnv::registerFunctions().

Para o cadastro de métodos, é importante lembrar que estes devem ser estáticos. Todas as funções cadastradas devem retornar um inteiro e receber como único parâmetro um lua_State*. Exemplo:

LuaEnv env(L);
// Cadastra função internalFunction1 e internalFunction2 para serem
// referenciadas no ambiente Lua como function1 e function2, respectivamente.
{"function1", internalFunction1},
{"function2", internalFunction1},
{NULL, NULL}
};
int internalFunction1(lua_State* L)
{
...
}
// Obs: MyClass::internalFunction2 deve ser um método estático!
int MyClass::internalFunction2(lua_State* L)
{
...
}

Associando métodos a um userdata

A classe auxiliar LuaMethod provê um mecanismo interessante para auxiliar na exportação de métodos para serem chamados através de um objeto exportado para o ambiente Lua. Para funcionar, o objeto deve ter sido exportado para Lua através do uso da classe LuaProxy, ou seja, os métodos exportados devem pertencer a um objeto que herda de LuaProxy::Base.

Dentro destas condições, LuaMethod permite que métodos NÃO estáticos sejam cadastrados e os objetos corretos sejam ativados sempre que um método for chamado em Lua via a sintaxe objeto:metodo().

O exemplo abaixo ilustra o processo completo de criação de uma classe LuaImage que encapsula um objeto do tipo Image (obviamente o exemplo foi reduzido para incluir apenas uma parte não repetitiva do código).

class Image
{
...
};
class LuaImage : public LuaProxy::Base
{
public:
// Construtor. Armazena copia da imagem recebida
LuaImage(const Image& obj) : _image(obj) {}
// Retorna identificador único para um LuaImage
void* getClassMetatableID() { static int classID = 0; return &classID; }
void populateMetatable(lua_State * L, int index);
// Retorna a imagem encapsulada pelo objeto
Image& image() { return _image; }
private:
int luaValue(lua_State* L);
static int luaGc(lua_State * L);
Image _image; // Imagem exportada para o ambiente Lua
};
// Preenche tabela de metadados de um LuaImage exportando o método
// luaValue() para o ambiente Lua, onde este será conhecidos como value().
void LuaImage::populateMetatable(lua_State* L, int index)
{
// Cadastra função para coleta de objetos
lua_pushstring(L, "__gc");
lua_pushcfunction(L, luaGc);
lua_settable(L, index);
// Coloca no topo da pilha a tabela associada a __index
lua_pushstring(L, "__index");
lua_gettable(L, index);
// Cadastra funções de acesso na tabela que está no topo da pilha
// Mais funções podem ser incluidas antes do par {NULL, NULL}
{"value", &LuaImage::luaValue},
{NULL, NULL }
};
lua_pop(L, 1);
}
// Retorna o valor da imagem na linha e coluna requisitadas.
// Parâmetros: linha, coluna
int LuaImage::luaValue(lua_State* L)
{
// Obs: Par. 1 é o próprio objeto (utilizado por LuaMethod)
int line = luaL_checkint(L, 2);
int col = luaL_checkint(L, 3);
double val = _image.value(lin, col);
lua_pushnumber(L, val);
return 1;
}
// Função chamada para coleta de objetos do tipo LuaImage
int LuaImage::luaGc(lua_State* L)
{
// Objeto está na primeira posição da pilha
LuaImage* proxy = LuaProxy::toObjectOfClass<LuaImage>(L, 1);
if(!proxy)
return luaL_error(L, "LuaImage garbage collector called on incorrect object type.");
delete proxy;
return 0;
}

Funções chamadas por Lua

Quando uma função registrada é chamada a aprtir do ambiente Lua, seus parâmetros residem na pilha. Para obter valores para tipos tradicionais, utilize as funções padrão luaL_checkXXX(). No caso de tipos especiais, tabelas, userdata ou funções, podem ser utilizados os procedimentos para converter um valor na pilha para um LuaTable, para um proxy, para um objeto exportado via tolua++ ou para um LuaFunction, conforme mostrado nas seções Trabalhando com tabelas, Trabalhando com userdata e Trabalhando com tipos registrados via tolua++.

Gerenciando a pilha

A classe LuaStackBalancer pode ser utilizada para garantir que o estado da pilha se mantenha igual (com o mesmo número de entradas) após uma sequência de operações sobre a mesma. Esta classe utiliza o padrão RAII, salvando o topo da pilha quando o objeto é criado e retaurando seu topo para a posição salva quando o objeto é destruido. Seu uso é particularmente útil em situações onde o gerenciamento de erro leva a saídas prematuras da função com objetos tendo sido colocados na pilha.

int DoSomeCalculation(LuaEnv* env)
{
LuaStackBalancer bal(env);
...
// Adiciona parâmetros na pilha
...
if(ocorreu um erro)
return;
...
// Ao término da função, todos os parâmetros adicionados na pilha serão
// retirados, mesmo que esta saída ocorra através do return para
// tratamento de erro.
}

As macros LUA_SAVE_TOP() e LUA_CHECK_TOP() podem ser utilizadas para verificar a posição do topo da pilha em ambiente de debug.