Bienvenidos sean a este post, para esta ocasion usaremos una version mas simplificada de lxp, un enlace entre Lua y Expat v. 1.2, Expat es un XML 1.0 parser de tipo Open source escrito en C, implementa SAX (API Simple para XML por sus siglas en ingles) y es un API basada en evento, esto significa que el SAX parser lee un documento XML y lo reporta a la aplicacion que encuentra atraves de las callbacks, por ejemplo si instruimos a Expat que analice una cadena como la siguiente:
<tag cap="5">Hola</tag>
Esto generara tres eventos:
- evento de elemento de inicio, cuando lee la subcadena «<tag cap=»5″>»
- evento de texto, cuando lee «hola»
- evento de elemento final, cuando lee «</tag>»
Nota: el evento de texto tambien se puede llamar evento de dato de caracter.
Cada uno de estos eventos llama a su manejador de callback apropiado en la aplicacion, aqui no cubriremos toda la libreria Expat, nos concentraremos en las tres partes que ilustran nuevas tecnicas para interactuar con Lua.
A pesar de que Expat maneja mas de una docena de eventos, consideraremos solo los tres eventos que describimos anteriormente (elementos de inicio, elemento final y texto).
La parte que necesitamos de Expat para este ejemplo es pequeña, primero necesitamos las funciones para crear y destruir un Expat parser:
XML_Parser XML_ParserCreate(const char *encoding);
void XML_ParserFree(XML_Parser P);
El argumento encoding es opcional, para nuestro enlace usaremos NULL, despues tenemos un analizador (parser), debemos registrar su manejador de callback:
XML_SetElementHandler (XML_Parser P,
XML_StartElementHandler start,
XML_EndElementHandler end);
XML_SetCharacterDataHandler (XML_Parser p,
XML_CharacterDataHandler hndl);
La primera funcion registra los manejadores para los elementos de inicio y final, la segunda funcion registra el manejador para el texto, todos los manejadores de callback reciben datos del usuario como su primer parametro, el manejador del elemento inicial recibe tambien el nombre del tag y el atributo:
typedef void (*XML_StartElementHandler)(void *uData,
const char *name,
const char **atts):
Los atributos vienen como un array terminado en NULL de cadenas, donde cada par consecutivo de cadenas posee un nombre de atributo y su valor, el manejador de elemento final solo tiene un parametro extra, el nombre del tag:
typedef void (*XML_EndElementHandler)(void *uData,
const char *name);
Por ultimo un manejador de texto solo recibe … exacto! texto como parametro extra, este texto no es terminado en NULL, en su lugar tiene un tamaño explicito:
typedef void (*XML_CharacterDataHandler)(void *uData,
const char *s,
int len);
Para pasar el texto a Expat usamos la siguiente funcion:
int XML_Parse (XML_Parser p, const char *s, int len, int isLast);
Expat recibe el documento a ser analizado en partes, a traves de sucesivas llamadas a XML_Parse, el ultimo argumento a XML_Parse es isLast, el cual le informa a Expat si esa parte es la ultima de un documento, observen que cada parte del texto no necesita ser terminado en cero, en su lugar pasamos una longitud explicita, la funcion XML_Parse devuelve cero si detecta un error de analisis, Expat tambien provee funciones para recuperar informacion de error pero en este caso los ignorara en aras de la simplicidad, la ultima funcion que necesitamos de Expat nos permite configurar los datos del usuario que sera pasado a los manejadores:
void XML_SetUserData (XML_Parser p, void *uData);
Con todo esto explicado pasemos a ver como utilizaremos esta libreria en Lua, un primer enfoque es un enfoque directo: simplemente exportar todas estas funciones a Lua, un mejor enfoque es adaptar esta funcionalidad a Lua, por ejemplo debido a que Lua no tiene tipo no necesitamos distintas funciones para setear cada tipo de callback, mejor aun podemos evitar todas las funciones de registracion de callback, en su lugar cuando creamos un analizador (parser), le damos una tabla de callback que contiene todos los manejadores de callback, cada uno con una clave apropiada, vamos a suponer que queremos imprimir un esquema de un documento, para ello podemos usar la siguiente tabla de callback:
local contar = 0
callbacks = {
ElementoInicial = function (parser, tagnombre)
io.write("+ ", string.rep(" ", contar), tagnombre, "\n")
contar = contar + 1
end,
ElementoFinal = function (parser, tagnombre)
contar = contar - 1
io.write("- ", string.rep(" ", contar), tagnombre, "\n")
end,
}
Si lo alimentamos con la siguiente entrada:
<hacia> <si /> </hacia>
Con los manejadores obtendremos la siguiente salida:
+ hacia + si - si - hacia
Con esta API no necesitaremos funciones para manipular los callbacks, las manipulamos directamente en la tabla de callback, por esto la API solo necesita tres funciones: una para crear los analizadores, otra para analizar un trozo del texto y la ultima para cerrar un analizador, en realidad implementaremos las ultimas dos funciones como metodos del objetos analizadores, un tipico uso de la API podria ser como este:
p = lxp.new(callbacks)
for l in io.lines() do
assert(p:parse(l))
assert(p:parser("\n"))
end
assert(p:parse())
p:close()
Este codigo es bien simple, la primer linea crea el parser, nuestro siguiente paso sera una iteracion sobre las lineas de entrada, la primera linea del bloque analiza la linea, la segunda agrega una nueva linea, una vez terminado el bloque, la penultima linea cierra el documento y la ultima cierra el parser.
Una vez terminada la explicacion prestemos atencion a la implementacion, la primera decision es como representar un analizador en Lua, es natural para usar un userdata pero que necesitamos poner dentro? Al menos debemos mantener el analizador de Expat actual y la tabla de callback, ya que no podemos almacenar una tabla de Lua dentro de un userdata (o dentro de cualquier estructura de C), en Lua 5.0 deberiamos usar una referencia para la tabla, en Lua 5.1 podemos configurar la tabla como el entorno de userdata, tambien debemos almacenar un estado de Lua dentro de un objeto parser porque estos objetos parser es todo lo que un callback de Expat recibe y los callbacks necesarios para llamar a Lua, por lo tanto la definicion para un objeto parser es la siguiente:
#include <stdlib.h>
#include "xmlparse.h"
#include "lua.h"
#include "laxulib.h"
typedef struct lxp_userdata {
lua_State *L;
XML_Paser *parser;
} lxp_userdata;
El proximo paso es la funcion que crea los objetos parser, lxp_make_parser, veamos su codigo:
static void f_StartElement(void *ud,
const char *name,
const char **atts);
static void f_CharData(void *ud, const char *s, int len);
static void f_EndElement(void *ud, const char *name);
static int lxp_make_parser(lua_State *L)
{
XML_Parser p;
lxp_userdata *xpu;
xpu = (lxp_userdata *)lua_newuserdata(L,
sizeof(lxp_userdata));
xpu->parser = NULL;
p = xpu->parser = XML_ParserCreate(NULL);
if (!p)
luaL_error(L, "Fallo el XML_ParserCreate");
luaL_checktype(L, 1, LUA_TTABLE);
lua_pushvalue(L, 1);
lua_setfenv(L, -2);
XML_SetUserDate(p, xpu);
XML_SetElementHandler(p, f_StartElement, f_EndElement);
XML_SetCharacterDataHandler(p, f_CharData);
return 1;
}
La funcion del codigo anterior tiene cuatro pasos:
- Su primer paso sigue un patron comun, primero crea un userdata luego preinicializa el userdata con valores consistentes y finalmente setea su metatabla, la razon para la preinicializacion es sutil porque si hay cualquier error durante la inicializacion debemos asegurarnos que el finalizador (el metametodo __gc) encontrara el userdata en un estado consistente.
- En el segundo paso, la funcion crea un parser de Expat la almacena en el userdata y chequea por errores.
- En el tercer paso, nos aseguramos que el primer argumento para la funcion es una tabla (la tabla de callback), y la setea como el entorno para el nuevo userdata.
- En el ultimo paso, inicia el parser de Expat setea el userdata como el objeto para ser pasado a las funciones de callback y setea la funcion de callback, observemos que estas funciones de callback son las mismas para todos los parsers, despues de todo es imposible crear dinamicamente nuevas funciones en C, en su lugar estas funciones fijas de C usaran la tabla de callback para decidir cual funcion de Lua debera ser llamada cada vez.
El proximo paso es el metodo parse, lxp_parse, veamos el siguiente codigo:
static int lxp_parse(lua_State *L)
{
int status;
size_t len;
const char *s;
lxp_userdata *xpu;
xpu = (lxp_userdata *)luaL_checkdata(L, 1, "Expat");
s = luaL_optlstring(L, 2, NULL, %len);
lua_settop(L, 2);
lua_getfenv(L, 1);
xpu->L = L;
status = XML_Parse(xpu->parser, s, (int)len, s == NULL);
lua_pushboolean(L, status);
return 1;
}
Este se encarga de analizar una pieza de datos de XML, consigue dos argumentos: el objeto parser (el self del metodo) y una parte opcional de datos del XML, cuando es llamado sin datos le informa a Expat que el documento no tiene mas partes.
Cuando lxp_parse llama a XML_Parse esta ultima funcion llamara los manejadores para cada elemento relevante que encuentra en la parte informada del documento, por lo tanto lxp_parse primero prepara un entorno para estos manejadores, hay un detalle mas en la llamada a XML_Parse: recuerden que el ultimo argumento para esta funcion le dice a Expat si la parte informada del texto es la ultima, cuando llamamos a parse sin ningun argumento s sera NULL, asi que el ultimo argumento sera true.
Ahora nos centraremos en las funciones de callback f_StartElement, f_EndElement y f_CharData, estas tres funciones tienen una estructura similar: cada uno chequea si la tabla de callbacks define un manejador de Lua para su evento especifico y si se prepara los argumentos luego llamada a estos manejadores de Lua, veamos primero el manejador f_CharData en el siguiente codigo:
static void f_CharData(void *ud, const char *s, int len)
{
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_getfield(L, 3, "CharacterData");
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1);
lua_pushlstring(L, s, len);
lua_call(L, 2, 0);
}
Su codigo es bien simple, este manejador (y los otros tambien) reciben una estructura de lxp_userdata como su primer argumento, debido a nuestra llamada a XML_SetUserData cuando creamos el parser, despues de recuperado el estado de Lua el manejador puede acceder al entorno establecido por lxp_parse:
- La tabla de callback en el indice 3 de la pila
- El parser mismo en el indice 1 de la pila.
Luego llama al manejador correspondiente en Lua (cuando esta presente) con dos argumentos, el parser y dato de caracter (una cadena), veamos el codigo para f_EndElement:
static void f_EndElement(void *ud, const char *name)
{
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_getfield(L, 3, "EndElement");
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1);
lua_pushlstring(L, name);
lua_call(L, 2, 0);
}
Esta funcion es similar a f_CharData porque tambien llama a su correspondiente manejador de Lua con dos argumentos, el parser y el nombre del tag (otra vez una cadena pero ahora null-terminado), veamos el siguiente codigo:
static void f_StartElement(void *ud, const char *name, const char **atts)
{
lxp_userdata *xpu = (lxp_userdata *)ud;
lua_State *L = xpu->L;
lua_getfield(L, 3, "StartElement");
if (lua_isnil(L, -1))
{
lua_pop(L, 1);
return;
}
lua_pushvalue(L, 1);
lua_pushstring(L, name);
lua_newtable(L);
for (; *atts; atts == 2)
{
lua_pushstring(L, *(atts + 1));
lua_setfield(L, -2, *atts);
}
lua_call(L, 2, 0);
}
Este es el codigo de nuestro ultimo manejador, f_StartElement, llama a Lua con tres argumentos: el parser, el nombre del tag y una lista de atributos, este manejador es un poco mas complejo que los otros porque necesita trasladar la lista de los atributos del tag dentro de Lua, usa una traslacion natural construyendo una tabla que asocia los nombres del atributo a sus valores, por ejemplo un tag de inicio como:
<to method="post" priority="high">
Genera la siguiente tabla de atributos:
{method = "post", priority = "high"}
Nuestro ultimo metodo para parsers es close, para ello veamos el siguiente codigo:
static int lxp_close(lua_State *L)
{
lxp_userdata *xpu = (lxp_userdata *)luaL_checkdata(L, 1, "Expat");
if (xpu->parser)
XML_ParserFree(xpu->parser);
xpu->parser = NULL;
return 0;
}
Cuando cerramos un parser tenemos que liberar sus recursos, a saber la estructura de Expat, recuerden que debido a errores ocasionales durante su creacion, un parser podria no tener este recurso, observen como mantenemos al parser en estado consistente a medida que lo cerramos, asi que no hay problema si intentamos cerrarlo de nuevo o cuando el recolector de basura lo finaliza, realmente usaremos esta funcion como el finalizador, esto asegura que cada parser eventualmente libera sus recursos incluso si el programador no lo cierra, veamos el siguiente codigo:
static const struct luaL_Reg lxp_meths[] = {
{"parse", lxp_parse},
{"close", lxp_close},
{"__gc", lxp_close},
{NULL, NULL}
}
static const struct luaL_Reg lxp_funcs[] = {
{"nuevo", lxp_make_parser},
{NULL, NULL}
}
int luaopen_lxp(lua_State *L)
{
luaL_newmetatable(L, "Expat");
lua_pushvalue(L, 1);
lua_setfield(L, -2, "__index");
luaL_Register(L, NULL, lxp_meths);
luaL_Register(L, "lxp", lxp_funcs);
return 1;
}
Este codigo es el verdadero paso final, abre la libreria, pone todas las partes previas juntas, usamos aqui el mismo esquema que usamos en el ejemplo del array booleano orientado a objetos de los posts anteriores, creamos una metatabla, ponemos todos los metodos dentro de la misma y hacemos que el campo __index apunte a si mismo, por eso necesitamos una lista con los metodos del parser (lxp_meths), tambien necesitamos una lista con las funciones de la libreria (lxp_funcs), como es comun con las librerias orientadas a objetos esta lista tiene una sola funcion, la cual es crear nuevos parsers, por ultimo la funcion de apertura luaopen_lxp debe crear la metatabla, hacerla apuntar a si misma (por medio de __index) y registrar metodos y funciones.
En resumen, hoy hemos visto como trabajar con una libreria externa, con ella conocimos a SAX, algunos de las fases de trabajo sobre el XML, como podemos utilizarla, como hacerla trabajar en Lua, como podemos adaptarla a C, cual son las fases minimas y necesarias para poder trabajar, un codigo de ejemplo para cada uno de ellos, espero les haya sido util sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.
Tengo un Patreon donde podes acceder de manera exclusiva a material para este blog antes de ser publicado, sigue los pasos del link para saber como.


Tambien podes donar
Es para mantenimiento del sitio, gracias!
$1.50