lunes, 22 de octubre de 2012

Proyectos C/C++ de Eclipse a Autotools

La problemática de preparar proyectos de C/C++ para que se compilen en cualquier sistema compatible con el estándar POSIX, con las herramientas recomendadas por GNU conocidas como Autotools (autoconf, autoheader, automake, autoreconf, aclocal, libtoolize) es un procedimiento que puede llegar a ser exhaustivo, confuso y agobiante si no se cuenta con sugerencias prácticas y aspectos básicos sobre estas herramientas. Como Eclipse es uno de los ambientes de desarrollo más populares sobre todo en Linux, cuenta con un tipo de proyecto llamado GNU Autotools que da un buen nivel de integración con dichos programas e incluye un eficaz resaltado de sintaxis. Sin embargo, por defecto no permite ejecución o depuración, además de dar un cascarón vacío que carece de una información que bien pudiera deducirse automáticamente a partir de los proyectos originales de C/C++. Este artículo trata de cómo crear proyectos GNU Autotools en Eclipse que reutilicen los proyectos Executable, Shared library y Static library sin perder la posibilidad de depurarlos, ejecutarlos y mantenerlos. Supongamos que nuestro proyecto original se llame ProjectX.
  1. Creamos un directorio llamado gnu-ProjectX (puede ser otro nombre) al mismo nivel que nuestro proyecto en el espacio de trabajo de Eclipse.
  2. Movemos la carpeta del proyecto original hacia dentro gnu-ProjectX. Es importante aclarar que con un enlace simbólico FUNCIONAN ERRÓNEAMENTE las Autotools, por lo que desechamos esta variante.
  3. Desde Eclipse, eliminamos el proyecto ProjectX del workspace. ¡Es importante haber realizado el paso 2 antes de hacer esto!
  4. Creamos un nuevo proyecto de C/C++, en el wizard especificamos el nombre gnu-ProjectX (que coincide con el directorio que creamos anteriormente) como proyecto GNU Autotools, y según nuestro objetivo seleccionamos:
    • Hello World C++ Autotools Project, si lo que vamos a generar es un ejecutable.
    • Autotools Shared Library Project, si lo que vamos a generar es una biblioteca (estática o dinámica).
  5. Seguidamente, especificamos los datos de autor, copyright y demás. En el campo Source, entramos el nombre del proyecto original, ProjectX.
  6. Una vez creado el proyecto, podemos ver nuestro proyecto original dentro del nuevo proyecto. Podemos eliminar el archivo .cpp que se crea nuevo, pues no lo utilizaremos. ¡Cuidado no eliminar un archivo del proyecto original!
  7. Si lo que se ha creado es una biblioteca, deberá especificarse en el archivo configure.ac lo siguiente:
    AC_CONFIG_MACRO_DIR([m4])
    después del comando AC_PROG_LIBTOOL y en Makefile.am de la raíz del proyecto, especificar:
    ACLOCAL_AMFLAGS=-I m4
    para no recibir unas warnings cuando se ejecuten las Autotools.
  8. Verificamos en Makefile.am de la raíz que se incluya la línea:
    SUBDIRS=ProjectX
  9. con el nombre del directorio del proyecto original.
  10. La parte más complicada es modificar Makefile.am del directorio del proyecto ProjectX, por lo que brindaremos una secuencia especial de pasos:
    1. Si su código no está en la raíz del directorio del proyecto, deberá crear tantos Makefile.am como directorios anidados tenga, en los que debe especificar la variable SUBDIRS con los nombres de los directorios hijos necesarios. Luego, debe buscar la directiva AC_CONFIG_FILES en el archivo configure.ac y adicionar un Makefile por cada Makefile.am. Ejemplo:
      AC_CONFIG_FILES(Makefile 
                      ProjectX/Makefile 
                      ProjectX/src/Makefile)
    2. Buscamos una variable llamada bin_PROGRAMS y modificamos su valor al nombre del ejecutable que deseamos generar. Por ejemplo:
      bin_PROGRAMS=projectx.bin
      Si es una biblioteca, el prefijo deberá ser lib en vez de bin.
    3. Usando ese nombre, creamos (o modificamos si existe) una con el sufijo _SOURCES. Por ejemplo:
      projectx_bin_SOURCES=main.cpp tool.cpp write.cpp tools.h
      Para dar el valor de esta variable, podemos buscar el archivo subdir.mk que debe aparecer dentro de la carpeta Debug del proyecto ProjectX original. En este archivo debe aparecer una variable C_SRCS o CPP_SRCS en la que se enumeran los archivos de código fuente a compilar, por lo general uno por línea con \ al final. Copie y pegue el o los archivos aquí enumerados en en Makefile.am y asegúrese de que de ellos quede solo el nombre y extensión, es decir, que no tengan elementos de caminos relativos, como ../ . Al final del backslash que indica cambio de línea NO debe aparecer ningún otro caracter, incluido el espacio.
    4. Si tiene los archivos .c y/o .cpp distribuidos en varios directorios, entonces deberá poner los caminos relativos:
      projectx_bin_SOURCES=dir1/main.cpp dir2/tools.cpp
    5. Si usa alguna biblioteca, deberá definir una variable con el nombre del binario con sufijo _LDADD (o _LIBADD si es una biblioteca), por ejemplo:
      projectx_bin_LDADD=-ldaemon -lMyLib
      Para obtener estos valores, puede buscar el archivo objects.mk que debe aparecer dentro de la carpeta Debug del proyecto ProjectX. En este archivo debe aparecer una variable LIBS cuyo valor puede usar.
  11. Ahora debemos correr las Autotools en orden:
    • aclocal
    • autoheader (solo si se usa la directiva AC_CONFIG_HEADERS en configure.ac)
    • autoconf
    • libtoolize (solo para bibliotecas)
    • automake --add-missing
    • autoreconf (para reconfigurarlo todo, por si acaso)
    Esto se puede hacer desde la línea de comandos en el directorio del proyecto gnu-ProjectX o desde Eclipse, seleccionando el proyecto en el Project Explorer, y luego ir al menú Project / Invoke Autotools. Estas herramientas deben estar instaladas, desde luego. En Debian/Ubuntu por ejemplo, los paquetes autoconf, automake y libtool tienen todo lo necesario. En caso de que Eclipse emita un mensaje de error como:
    sh autoconf
    
    sh: 0: Can't open autoconf
    abra las propiedades del proyecto, busque la sección Autotools / General y en la pestaña Tool Settings especifique cada herramienta con su camino absoluto, por ejemplo: /usr/bin/autoconf. Consulte los manuales de las herramientas automake y autoconf para más información.
  12. Una vez corridas todas las Autotools sin error, deberán haberse creado muchos archivos nuevos, como son: aclocal.m4 config.guess config.status config.sub configure INSTALL install-sh Makefile Makefile.in missing.
  13. Supongamos que en nuestro código usamos encabezados (headers) o bibliotecas no estándares, es decir, que no aparecen por defecto bajo el directorio /usr/include o /lib, /usr/lib que son incluidos por defecto a la hora de compilar. Si es así, podemos optar por la alternativa de definir (o aumentar) las variables de entorno:
    • CPATH para los headers, caminos absolutos separados por dos puntos (:).
    • LIBRARY_PATH para las bibliotecas, caminos absolutos separados por dos puntos (:).
  14. Desde la línea de comandos, en el directorio gnu-ProjectX ejecute:
    ./configure
    
    make
    Chequee si se produjo o no algún error. En caso afirmativo, deberá comprender el mensaje de error para poder darle solución. Deberá determinar si el fallo fue en los archivos del configuración de Autotools, o faltan prerrequisitos, variables de entorno sin definición o incompletas, o errores de compilación. Según sea el caso, consulte los manuales o la Internet.
  15. Si todo terminó felizmente, pruebe a instalar (correr desde una cuenta con permisos de administración):
    make install
    Chequee el o los binarios en /usr/local/bin (por defecto) y las bibliotecas en /usr/local/lib.
  16. Si la instalación se realizó con éxito, trate de probar lo instalado. Una vez comprobado, puede ejecutar el comando:
    make dist
    para crear un paquete .tar.gz, el cual puede distribuir.
  17. Solo falta que volvamos a tener la posibilidad de ejecutar y depurar el proyecto. Esto es sencillo: desde el Project Explorer en Eclipse, haga clic derecho en algún lugar, debe aparecer la opción Import..., busque en General / Existing projects into Worspace, busque la carpeta de ProjectX dentro de gnu-ProjectX y haga clic en el botón Finish y listo.
En conclusión, ya estamos listos para empaquetar, desplegar y compilar nuestro código de C o C++, sin perder la posibilidad de mantenerlo actualizado, depurarlo y probarlo en Eclipse.

jueves, 16 de agosto de 2012

C++0x + Debian 6

¿Es posible aprovechar algún nuevo recurso de C++, discutido e incluido en el nuevo estándar conocido inicialmente como C++0x y luego rebautizado como C++11, usando gcc/g++ 4.4.5, que es la predeterminada en Debian 6? Responder esta pregunta es el objetivo de este artículo.

Primero, la respuesta corta: sí, es posible.

Luego, la respuesta larga. En este sitio, podrán encontrar las características implementadas en la versión 4.4 del compilador GNU gcc. Pero ver sólo la página no dice mucho, por lo que nos entretuvimos en identificar, comprender y verificar las más atractivas.

  1. Variables con declaración automática de tipo (auto).
  2. Inicialización de colecciones.
  3. enum ahora fuertemente tipado.
  4. Permitidos varios caracteres > seguidos.
  5. Referencias a rvalue.
  6. Manejo de funciones predeterminadas.

Para ello construimos un proyecto de pruebas unitarias usando el framework lite UnitTest++. La compilación con g++ debe realizarse con la opción -std=c++0x para habilitar las nuevas extensiones del lenguaje.

Variables con declaración automática de tipo

Cuando necesitamos declarar una variable, ya sea en C o C++ es necesario especificar su tipo y de manera opcional su valor inicial. Una variable declarada como auto deduce su tipo a partir del tipo de la expresión con que se inicializa. Veamos la prueba:

#include <UnitTest++.h>
#include <string>
#include <iostream>
#include <vector>

using namespace std;

struct A
{
 int p;
};

TEST(Use_of_auto_type_supported)
{
 auto i = 5;
 int a = i + 5;
 CHECK_EQUAL(10, a);
 
 auto s = string("a");
 string b = s + "b";
 CHECK_EQUAL(0, b.compare("ab"));
 
 auto t = new A;
 t->p = 5;
 A sta;
 sta = *t;
 CHECK_EQUAL(5, sta.p);
 
 auto d = 5.6;
 double dd = 4.4 + d;
 CHECK_CLOSE(10, dd, 0.01);
 
 auto c = 'a';
 char cc[1];
 cc[0] = c;
 CHECK_EQUAL('a', cc[0]);
}

Como se puede ver, la variable i es de tipo int, s es string, t es un apuntador a la estructura A, d es un double y c un char. Cada tipo ha sido deducido a partir de la expresión de inicialización. Sin embargo, una expresión como la siguiente:

 auto p = null;

es inválida pues null no es expresión que defina un tipo particular. Con este recurso resolvemos los tediosos bloques como el siguiente:

for (vector<a_big_name_type>::const_iterator iter = collection.begin();
   iter != biometricTypes.end();
   ++iter)
{
 ...
}

Ahora podemos escribir:

for (auto iter = collection.begin(); 
   iter != biometricTypes.end();
   ++iter)
{
 ...
}

Para más información, ver este documento.

Inicialización de colecciones

Desde C# 3.0 se tiene algo como esto. Pues ya en C++ es perfectamente posible:

TEST(Init_lists)
{
 vector<int> a {1, 2, 3, 4 ,5};
 vector<string> s {"1", "2", "3"};
 CHECK_EQUAL(3, a[2]);
 CHECK_EQUAL(0, s[1].compare("2"));
}

El vector a contiene 5 números, desde el 1 al 5, mientras que s tres cadenas. Para más información, ver este link.

enum ahora fuertemente tipado

Desde tiempos immemoriales, los nombres de los campos de dos enumeraciones no podían coincidir. Es decir, que esto era ilegal:

 enum A { A1, A2 };
 enum B { A1, B1 }; // Ilegal, nombre A1 repetido.

Por lo general se resolvía el problema poniéndoles prefijos a los nombres de los campos, y así no "colisionaran" unos con otros. Pero dicha técnica no era otra cosa que un disimulado parche. Para resolver problemas de nombres, asignación/conversión directa a enteros, tamaño en memoria de los enum y otros, se tiene en C++ 11 lo siguiente:

enum class E1 : char { Vale1, Vale2 };     // No hay error de sintaxis
enum class E2 : uint32_t { Vale1, Vale2 }; // No hay error de sintaxis

TEST(Strongly_typed_enums)
{
 E1 a = E1::Vale1;
 E2 b = E2::Vale1;
 // int c = E1::Vale1; // Ilegal, imposible asignar enum a int.
 CHECK_EQUAL(1, sizeof(a));
 CHECK_EQUAL(4, sizeof(b));
}

Para que sea fuertemente tipada, enum debe sucederse con la palabra reservada class, seguida de dos puntos y el tipo cuyo tamaño en memoria tendrá la nueva enum. Para más información, ver este documento.

Permitidos varios caracteres > seguidos

Esta posibilidad es bien sencilla, pero que molestaba un poco a los que trabajamos con tipos genéricos, STL y demás. Y es que ya se puede hacer:

TEST(Right_angle_brackets)
{
 vector<vector<int>> v;
 v.push_back(vector<int>());
 v.push_back(vector<int>());
 v.push_back(vector<int>());
 CHECK_EQUAL(3, v.size());
}

En la declaración del vector v no hay error de sintaxis, pues el doble angular >> ya no provoca error en tiempo de compilación al no ser detectado como si fuera operador binario. Este operador de redirección se usa por ejemplo en la segunda sentencia del listado siguiente:

 char a;
 cin >> a;

Para indicarle al objeto cin contenido en el namespace std y parte de la STL que la entrada estándar será almacenada en la variable a. Para más información, ver este link.

Referencias a rvalue

Con la concepción del lenguaje C++, se introdujo el uso de refencias para permitir usar un nombre de variable por otro sin necesidad de recurrir a la indirección (apuntadores). Tomemos el siguiente ejemplo:

 void a(int* p) { *p = 5; }
 void b(int& p) { p = 5; }
 ...
 int myp = 0;
 a(&myp);
 b(p); 

Ambas funciones, a y b producen el mismo efecto, sin embargo a es mucho más ineficiente. Para poder cambiar el valor de p a 5, la función a necesita un apuntador a una variable int y especificar que se cambiará el valor apuntado. Por otro lado, cuando se pasa myp a a debe aplicarse una indirección (operador &). Sin embargo, la función b solo le da un "alias" a la variable myp en su parámetro formal p y lo modifica sin tanto trauma.

Sin embargo, no es posible pasar por referencia un rvalue (expresión a la que no puede asignársele nada). Es decir, lo siguiente es ilegal:

 void b(int& p) { p = 5; }
 int myp() { return 6; }
 ...
 b(myp());

Con las referencias a rvalue, denotadas con el operador &&, es posible hacerlo:

string foo() { return "a"; }

TEST(Rvalue_reference)
{
 // string& f1 = foo();  // Error de sintaxis
 string&& f2 = foo(); // Se mueven los campos del string devuelto por foo, no se copian!
 string f3 = foo();   // Se copian los campos del string devuelto por foo
 CHECK_EQUAL('a', f2[0]);
}

Aunque la lógica sea similar, por debajo lo que se realiza es "mover" la memoria generada por lo que devolvió foo() hacia f2. En el caso de f3 se hace una copia campo por campo del string devuelto, por lo que es ineficiente. Para más información, ver este link.

Manejo de funciones predeterminadas

C++ por defecto brinda cuatro funciones miembro de clases/estructuras:

  • destructor
  • constructor por defecto
  • constructor de copia
  • operador de asignación por copia =

Además, también proporciona otros operadores como new y delete, por ejemplo. Sin embargo, cuando el usuario define un constructor propio, el por defecto desaparece, por lo que habría que implementarlo. Esta implementación es muy posible sea menos eficiente que la que brinda el compilador. Es por ello que se propone lo siguiente:

struct type
{
    int a;
    type() = default; // la reduncia ahora es eficiente
    type(int a) { this->a = a; } // Otro constructor
    virtual ~type(); // virtual requiere una declaración
    type( const type & ); // una simple declaration
};
inline type::type( const type & ) = default; // ahora eficiente
type::~type() = default; // una definición por defecto no inline

struct type2
{
    type2 & operator =( const type2 & ) = delete;
    type2( const type2 & ) = delete;
    type2() = default;
};

TEST(Default_And_Delete_Func)
{
 type t1;
 t1.a = 5;
 type2 t2, t3;
 // t3 = t2; // Inválido, el operador = se ha eliminado
 // type2 t4(t2) // Inválido, el constructor copia se ha eliminado
 type t5(t1); 
 CHECK_EQUAL(5, t5.a);
}

Como se puede ver en caso del tipo type, se han mantenido los constructores y el destructor por defecto, de manera que se puede crear otros constructores sin problemas.

El caso de type2 es otra nueva funcionalidad: eliminar las definiciones del constructor copia y de la asignación por copia (aunque se puedan eliminar otras funciones y sobrecargas de operadores), de manera que las operaciones con el tipo quedan prohibidas. Para más información, ver este link.

Conclusiones

Con estos recursos de nuestro lado, ganamos mucha del azúcar sintáctica y capacidades de las que hoy gozan C# y Java, nada más y nada menos que en C++. Existen otras nuevas mejoras, como las expresiones lambda, tan populares hoy en ambientes .NET, pero vienen en versiones más modernas del compilador g++ (ver aquí), por lo que no han sido abordadas.

También brindamos el vínculo para conocer cuál es el estado del desarrollo de las nuevas funcionalidades en la STL y una referencia online actualizada de los lenguajes C/C++.