miércoles, 20 de noviembre de 2013

Autotools (3): Integración de libtool en autotools para la creación de una biblioteca a instalar en el sistema.

En otros post anteriores vimos la escencia de las bibliotecas así como de libtool, si además haz realizado aunque sea un “hello world” con autotools entonces ahora veamos como integramos a libtool con autotool para la creación de una biblioteca.

#configure.ac
LT_INIT: Es la forma en que actualmente se define LIBTOOL.

# Checks for programs.
AC_PROG_CXX: Chequea por un compilador para c++.
AC_PROG_AWK: Chequea gawk, mawk, nawk, and awk, en ese orden y lo establece como variable.
AC_PROG_CC: Chequea por un compilador para c.
AC_PROG_CPP: Muetra el valor de la variable CPP que contiene el preprocesador.
AC_PROG_INSTALL: Muetra el valor de la variable INSTALL para instalación.
AC_PROG_LN_S: Chequea que ls sistema en que te encuentras soporta links simbólicos(ln -s), en caso que este no esté disponible lo establece a ln, en otro caso a cp -pR.
AC_PROG_MAKE_SET: Establece el comando make.

#lib/Makefile.am:
CLEANFILES: Para que make clean borre los archivos que coincidan.

AM_CPPFLAGS: El contenido de esta variable es pasado a todas las reglas de compilación que invoquen el preprocesador de C.

Definiciones para el uso de pkg-config una vez creada la lib.
pkgconfigdir = @libdir@/pkgconfig
pkgconfig_DATA = libmatX.pc

A diferencia del primer ejemplo donde poniamos bin_PROGRMAS para crear ejecutables aquí usamos esta macro a la cual le decimo el nombre de la lib a crear y en el source debe transformar el nombre a su forma canónica.
lib_LTLIBRARIES = libmatX.la
libmatX_la_SOURCES = \
matX.cc

libmatX_la_LDFLAGS: Flags para el elnlazador.


“#Headers/Makefie.am
includedir: Referencia al directorio include donde se pondran los headers
nobase_include_HEADERS: Para que no se instale dos veces con una invocación del programa.
Una vez configurado el proyecto procedemos a la contrucción:
1. mkdir -p m4
2. libtoolize --copy –force
3. aclocal -I m4
4. autoheader
5. autoconf
6. automake -a
7. ./configure
.8 make
Ya podrás ver que se ha creado lib/libmatX.la, incluso lo podrías leer(cat lib/libmatX.la) ya que es texto plano, en este punto la biblioteca ya es usable, esto sería en nuestro progrma usar la macro LDADD (tambien con el nombre del ejecutable como prefijo y con _ al final) para esto puedes consultar GNU Automake aunque este ejemplo cuneta con su *.pc.
9. sudo make install
Ya deberías poder verificar como instalada la biblioteca en tu sistema
10. pkg-config --list-all | grep hello_libtool

Ya teniendo esta infraestructura básica del proyecto veamos como crear las bibliotecas estática y dinámica correspondiente.

Biblioteca estática:
make clean
./configure --disable-shared
make
Biblioteca dinámica:
make clean
./configure --disable-static
make

NOTA: En caso de no especificar se crea la dinámica y la estática.

Desde aquí puedes descargar el ejemplo:

git clone https://github.com/denisacostaq/EmbeLinux --branch hello-libtool

martes, 19 de noviembre de 2013

Creación de bibliotecas mediante el uso de libtool (Parte 2).

Si ya revisaste el post anterior sobre construcción de bibliotecas podremos proseguir con el hecho de que ya tenemos algunos conceptos básicos de nuestro lado, esta vez relizaremos el mismo ejemplo lo que cambia es las herramientas. Primero construiremos la lib estática, después compartida y por último de ejecución (que no es más que la misma biblioteca compartida lo que se carga en tiempo de ejecución).

Libtool:
Según su propia documentación (apt-get install libtool-doc) fue diseñado desde sus inicios para soportar un número arbitrario de tipos de bibliotecas, más tarde se ha ido portando a otras plataformas y gradualmente se han ido desarrollando nuevos paradigmas que describen la relación entre las bibliotecas y los programas.

Consideremos el sigiente código con el cual crearemos la biblioteca estática y compartida:
//module.c
float mult (float fac1, float fac2)
{
return fac1 * fac2;
}

Biblioteca estática:
1. Creamos un directorio donde “instalaremos” la lib.
mkdir -p /tmp/install
2. Compiamos el código.
libtool --verbose --mode=compile gcc -g -O -c module.c -static
3. Enlazamos el código, si no ponemos -rpath /tmp/install al comando anterior tendremos que ejecutar
libtool --finish /usr/local/lib después para que se complete la instalación correctamente.
libtool --verbose --mode=link gcc -module -o module.la module.lo -rpath /tmp/install -static
4. Instalamos
libtool --mode=install install module.la /tmp/install
Ya tenemos lista la lib en /tmp/install que puedes usar como en el post anteriorque mencionaba al inicio y además notar que se encuentra en el direcorio oculto (ls .libs/).

Biblioteca dinámica:
Recomendable eliminar los archivos anteriores para ver el nuevo resultado y repitiendo los pasos para este caso tenemos:
1. mkdir -p /tmp/install
2. libtool --verbose --mode=compile gcc -g -O -c module.c -shared
3. libtool --verbose --mode=link gcc -module -o module.la module.lo -rpath /tmp/install -shared
4. libtool --mode=install install module.la /tmp/install

Biblioteca de ejecución:

Para ver como usamos una lib en tiempo de ejecución usaremos el siguiente código:

#include <stdio.h>
#include <ltdl.h>
#include <stdlib.h>

//declaramos una variable(puntero a la función así que
//debe coincidir en los tipos)
float (*mult)(float fac1, float fac2);

int main (int argc, char *argv[])
{
lt_dlhandle handle;
lt_dlinit ();
lt_dladdsearchdir ("./");

handle=lt_dlopenext ("module");
if (!handle)
{
printf ("Error, módulo no cargado: %s\n",
                   lt_dlerror());
exit (EXIT_FAILURE);
}

const lt_dlinfo *info;
info = lt_dlgetinfo (handle);
if (!info)
{
fprintf (stderr, "No se pudo obtener la información  del módulo: %s\n", lt_dlerror());
lt_dlclose (handle);
exit (EXIT_FAILURE);
}
if (info->name)
{
printf ("Nombre del módulo: %s\n", info->name);
}
else
{
printf ("No es un módulo libtool\n");
}
//casteo al tipo de la función
mult = (float(*)(float, float))lt_dlsym(handle, "mult");  
if (mult)
{
printf ("Resultado desde el módulo: %f\n", mult(5.0, 10.0));
}
else
{
perror ("No se pudo cargar la función\n");
lt_dlclose (handle);
exit (EXIT_FAILURE);
}
lt_dlclose (handle);
return 0;
}


Recomendable eliminar los archivos anteriores para ver el nuevo resultado y repitiendo los pasos para este caso tenemos:
1. libtool --verbose --mode=compile gcc -g -O -c module.c -shared
2. libtool --verbose --mode=link gcc -module -o module.la module.lo -rpath /tmp/install -shared
3. libtool --mode=link gcc -export-dynamic -o programdl program.c -lltdl

Un detalle importante es notar que la lib se contruye de la misma manera que la compartida, el programa es el que se contruye de forma diferente a la convencional y además con ldd como en el post anterior puedes ver como program no depende de module por lo que los pasos 2 y 3 se pueden cambiar de orden ya que program no necesita saber nada acerca de module en tiempo de compilación.
NOTA: Para tener más referencia consulte man libtool o /usr-sha-doc-libtool-doc.

jueves, 14 de noviembre de 2013

Implementando mi propio shell usando una gramática LL1.

Lo primero que veremos, qué es un shell; después, qué es una gramática, cuando una gramática se clasifica como LL1 (y si no lo es como llevarla) y por último veremos una pequeña implementación en C de un shell.
Shell: Shell (del inglés cascarón, coraza, concha, etc) toma este nombre por ser el shell una interfáz entre el nucleo del Sistema Operativo y el usuario, es un intérprete a través del cual este le da órdenes(ver Fig 1).

Fig 1.


El shell es un programa capaz de interpretar comandos, hacer una petición al sistema y devolver un resultado. Exixsten varias implementaciones: sh, bash, csh, Tcsh, …, etc... Cada usuario tiene una shell predeterminada ej: adacosta:x:1008:1008:PEPSI,,,:/home/adacosta:/bin/bash el cual es ejcutado al autenticarse el usuario en una terminal, o al abrir una emulador de terminal en una sesión X11, lo primero que hace el shell es leer sus configuración de sistema (/etc) y después leer la configuración del usuario (/home/user-name/.*) pudiendo llegar a ser uno o varios archivos, en mi caso:
( /home/adacosta/.bashrc, /home/adacosta/.profile /home/adacosta/.bash_*)

Gramática: Intuitivamente, una gramática es un conjunto de reglas para formar correctamente las frases de un lenguaje; así ́ tenemos la gramática del español, del francés, etc. Según N. Chomsky existen 4 tipos fundamentales de gramáticas:


1. Gramáticas regulares, o de tipo 3: Las reglas son de la forma A → α B o bien A → α, donde A y B son variables (no terminales) y α es una constante (terminal). Estas gramáticas son capaces de describir los lenguajes regulares.
́
2. Gramáticas Libres de Contexto (GLC), o de tipo 2: Las reglas son de la forma X → α, donde X es una variable y α es una cadena que puede contener variables y constantes. Estas gramáticas producen los lenguajes Libres de Contexto.
́
3. Gramáticas sensitivas al contexto (dependientes del contexto) o de tipo 1: Las reglas son de la forma αAβ → αΓβ, donde A es una variable y α, β y Γ son cadenas cualesquiera que pueden contener variables y constantes.
4. Gramáticas no restringidas, o de tipo 0: Con reglas de la forma α → β, donde α no a puede ser vacío, que generan los lenguajes llamados “recursivamente enumerables”.

Los objetivos de una gramática son: definir las sentencias que pertenece a un lenguaje, así como describir estructuralmente dichas sentencias. Las gramáticas generan sentencias mediante secuencias de derivaciónes directas




Como ejemplo veamos una gramática subconjunto del idioma español:
G = ( {<frase>, <sujeto>, <predicado>, <artículo>, <sustantivo>, <verbo>},
{el, la, perro, luna, brilla, corre}, P, <frase>)
P:
<frase> → <sujeto> <predicado>
<sujeto> → <artículo> <sustantivo>
<artículo> → el | la
<sustantivo> → perro | luna
<predicado> → <verbo>
<verbo> → brilla | corre

En correspondencia con esta gramática podriamos generar frases como las que siguen.
{el perro corre , el perro brilla, la perro brilla, ...} estando todas ellas correctas sintácticamente aunque haciendo un análisis semántico (en otra etapa) podriamos detectar que algunas de las frases carecen de sentido.

Gramáticas LL1:
Un analizador LL es llamado un analizador LL (k) si usa k tokens cuando el analizador ve hacia delante de la sentencia. Si existe tal analizador para cierta gramática y puede analizar sentencias de esta gramática sin marcha atrás, entonces es llamada una gramática LL (k). De estas gramáticas, la gramática LL(1), aunque es bastante restrictiva, esta es muy popular (principalmente en actividades docentes) porque los analizadores LL correspondientes sólo necesitan ver el siguiente token para hacer el análisis de sus decisiones. Lenguajes mal diseñados usualmente suelen tener gramáticas con un alto nivel de k, y requieren un esfuerzo considerable al analizar.

Una gramática ambigua es aquella que produce más de una derivación más a la izquierda o más de una derivación más a la derecha para la misma sentencia. En general existen tres estrategias iniciales para eliminar la ambiguedad de las gramáticas, ellas son:
1. Definir la precedencia y asociatividad de los operadores.
2. Eliminar la recursividad izquierda o derecha.
3. Factorizar.
Veamos un ejemplo:
G = ({<Expresión>}, {id, +, - , *, / , (, )}, P, <Expresión>)
P:
(1) <Expresión> → <Expresión> + <Expresión> | <Expresión> - <Expresión>
(2) <Expresión> → <Expresión> * <Expresión> | <Expresión> / <Expresión>
(3) <Expresión> → id | ( <Expresión> )
Precedencia y asociatividad:
Definamos un nivel de prioridad para cada operador como sigue:
1. (), id
2. *, /
3. +, -
Nivel 1 Se introduce un nuevo símbolo No Terminal, que llamaremos <Factor> para describir una expresión indivisible y con máxima precedencia.
<Factor> → id | ( <Expresión> ) .
Nivel 2 Se introduce un nuevo símbolo No Terminal, que llamaremos <Término>, que generará una secuencia de uno o más términos generados por el No Terminal <Factor>, conectados con los operadores de nivel precedencia 2.
<Término> → <Término> * <Factor> | <Término> / <Factor> | <Factor>
Nivel 3 Se mantiene el símbolo No Terminal <Expresión> para describir secuencias de uno o más términos generados por el No Terminal <Término>, conectados con los operadores de nivel precedencia 3.
<Expresión> → <Expresión> + <Término> | <Expresión> - <Término> | <Término>
Aplicando las reglas anteriores la gramática definida anteriormente queda re definida como sigue:
G = ({<Expresión>, <Término>, <Factor>}, {id, +, - , *, / , (, )}, P, <Expresión>)
P:
<Expresión> → <Expresión> + <Término> | <Expresión> - <Término> | <Término>
<Término> → <Término> * <Factor> | <Término> / <Factor> | <Factor>
<Factor> → id | ( <Expresión> )

Recursividad Izquierda :
Una producción de la forma A → Aβ se dice que es recursiva inmediata. Para eliminar las recursividades inmediatas podemos seguir el procedimiento siguiente:
1. Se agrupan las A-producciones:
A→ Aα1 | Aα2 | . . .| Aαm | β1 | β2 . . .| βn
donde ningún βi comienza con A.
2. Se reemplazan las A-producciones por:
A→ β1 A’| β2 A’| . . .| βn A’
A’→ α1 A’| α2 A’| . . .| αm A’| ǫ
Aplicamos el procedimiento anterior para eliminar la recursividad directa en la gramática:
G = ({<Expresión>, <Término>, <Factor>}, {id, +, - , *, / , (, )}, P, <Expresión>)
P:
<Expresión> → <Expresión> + <Término> | <Expresión> - <Término> | <Término>
<Término> → <Término> * <Factor> | <Término> / <Factor> | <Factor>
<Factor> → id | ( <Expresión> )

Para ellos seleccionamos las reglas que son recursivas directa a la izquierda estas son:
(I) <Expresión> → <Expresión> + <Término> | <Expresión> - <Término> | <Término>
(II) <Término> → <Término> * <Factor> | <Término> / <Factor> | <Factor>

Aplicando la regla para la eliminación de ambiguedad 2 sobre (I) y (II) nos queda:

<Expresión> → <Término> <MasExpresión>
<MasExpresión> → + <Término> <MasExpresión> | - <Término> <MasExpresión> | E

<Término> → <Factor> <MasTérmino>
<MasTérmino> → * <Factor> <MasTérmino> | / <Factor> <MasTérmino> | E


Factorización por la izquierda.
La Factorización por la izquierda tiene el objetivo de reescribir las producciones de la gramática con igual comienzo para retrasar la decisión hasta haber visto lo suficiente de la entrada como para elegir la opción correcta. El procedimiento es simple. Si tenemos una regla de producción de la forma:
́ A → αβ1 | αβ2 | . . . | αβn | δi
esta se transforma en las regla.
A → α A′ | δi
A ′ → β1 | . . . | βn

A manera de ejemplo consideremos la regla de producción siguiente:
<Instruciones > → <Instrucción> ; <Instruciones > | <Instrución>

Aplicando la Factorización por la izquierda nos quedan las reglas equivalentes:

<Instruciones > → <Instrucción> <MasInstrucciones >
<MasInstrucciones > → ; <Instruciones > | E

A modo de conclusión podemos decir que las gramáticas constituyen la mejor vía para la descripción sintáctica de los lenguajes de programación. Existen diversas razones que justifican tal afirmación:
Las gramáticas brindan una especificacion sintáctica precisa de los lenguajes de programación.

Para ciertas clases de gramáticas pueden construirse analizadores sintácticos eficientes que determinan si el programa fuente está bien escrito. El proceso de construcción del parser puede además, revelar ambiguedades sintácticas no detectadas en la fase de diseño del lenguaje .

Una gramática bien diseñada puede ayudar a la traducción del programa fuente en código objeto y a la detección de errores.

Se pueden añadir nuevas construcciones al lenguaje de forma fácil.



Un ejemplo de implementación:
Un ejemplo de implementación de un intérprete basado en un gramática LL1 lo puede tomar clonando denishell(git clone https://github.com/denisacostaq/denishell), toda la documentación relevante forma parte del código fuente y/o de la documentación(valga la redundancia) generada con doxygen, en especial fijarse en el comando cd, en la manera que se “simula” la implementació de un pipe (char** run_pipe_on_node (struct ast_node *ast);), en la pesatña examples de dicha documentación puedes aprender cómo comunicar dos procesos con un pipe real, y además puedes ver algunos de los comandos a ejecutar, tambié revisar char** cat_args(); y char** ls_args (); para tener detalles sobre la internacionalización (deni-sh_i18n.h).