miércoles, 23 de marzo de 2016

La compilación según ZinjaI (parte I)

Para que al presionar F9 en ZinjaI podamos ver nuestro programa ejecutándose tienen que ocurrir unas cuantas cosas. La más importante de ellas, es la compilación de ese programa. ¿Cómo construye ZinjaI las llamadas a GCC necesarias para compilar un programa? La respuesta varía según el modo de trabajo. Para programas simple (cpp único sin proyecto) pasa una cosa, para un proyecto pasa otra. En ambos casos la respuesta es compleja e involucra información de muchas fuentes diferentes. En esta serie de posts voy a documentar qué ocurre exactamente al presionar F9 para entender de dónde sale cada partecita de cada paso, y especialmente cómo se llega a la llamada final a GCC.


En esta primera parte vamos a analizar solamente el modo que suelo llamar "programa simple". ZinjaI trabaja de este modo cuando no hay proyecto. En este modo, cada archivo (pestaña de código) se considera un programa completo e independiente. Por esto, esta compilación se realiza en un solo paso. Y además, en este modo, el cuadro de diálogo de opciones de compilación ofrece en realidad muy pocas opciones. Si se necesita algo más complejo, conviene por varios motivos pasar a un proyecto.

Al presionar F9, lo primero que ZinjaI hace es ver si el fuente tenía modificaciones y, en caso afirmativo, guardarlo (si el archivo aún no tenía nombre, se guarda siempre, utilizando una ruta temporal que se puede cambiar desde el cuadro de preferencias). Luego, pasa a analizar el fuente a compilar. Hay un detalle importante: el fuente que se guarda es siempre el actual, pero el fuente a compilar puede ser otro. Si hacen click derecho en una pestaña, verán una opción "Compilar siempre este fuente", que permite fijar el fuente a compilar. Es útil cuando lo que estamos editando es en realidad un header que otro cpp incluye. Pero en la mayoría de los casos, será el fuente actual (la pestaña seleccionada). Como efecto colateral del guardado, el archivo se re-parsea actualizando el árbol de símbolos.


Luego se determina si efectivamente hay que recompilar. Para ello primero se compara la fecha del último ejecutable (si existe) con la de modificación del fuente. Si el fuente es más reciente, o el ejecutable no existe, se compila. Si el ejecutable ya existe y es más reciente que el fuente a compilar se pasa a analizar el contenido del fuente en busca de #includes, para averiguar si alguno de esos headers incluidos (o lo que estos incluyen, el análisis es recursivo) es más reciente que el ejecutable. Si alguno lo es, hay que recompilar.

Suponiendo que ya determinamos la necesidad de compilar, el siguiente paso es construir la linea de compilación. Es decir, la llamada a GCC. El comando para invocar al compilador está dado por el toolchain. Cada toolchain define un comando para compilar fuentes C y otro para C++. Cada fuente está marcado como C o C++, según la plantilla utilizada o de la extensión del nombre de archivo. El comando empieza con ese valor, que por defecto será "g++" (en GNU/Linux) o "mingw32-g++" (en Windows). A continuación se le agregan argumentos que define el toolchain. El toolchain puede definir argumentos para la compilación y para el enlazado. Aquí se agregan ambos porque se van a realizar ambos procesos con una sola llamada. En GNU/Linux, por defecto estos campos están vacíos. En Windows, por ejemplo, aquí es donde se agrega "-static-libstdc++ -static-libgcc" para forzar el enlazado estático de la biblioteca estándar. Esto se puede alterar reconfigurando el toolchain desde el cuadro de preferencias.


Luego, ZinjaI agrega dos sets de argumentos propios que no pueden modificarse. Primero agrega (si es necesario, dependiendo de la versión de GCC) argumentos como "-fshow-column" y "-fno-diagnostics-show-caret" para controlar el formato de los mensajes de errores y warnings. Esto es necesario para que ZinjaI luego parsee correctamente esa salida, y por eso no es modificable. En segundo lugar, cuando se trabaja en modo programa simple, siempre se agrega información de depuración. Siempre se agrega "-g", y opcionalmente se puede agregar algún argumento más, pensado para definir el formato de la información de depuración (forzar por ejemplo una versión de dwarf). Por defecto, no se especifica formato, pero se podría configurar ZinjaI para que lo haga (no desde las preferencias, sino editando manualmente su archivo de configuración).

A continuación, si el nombre del fuente no tiene una extensión reconocible, ZinjaI agrega "-x c++" o "-x c" para indicarle al compilador el lenguaje que debe utilizar. Algunos alumnos usan nombres de archivo como "Ejercicio 3.5", donde el "5" será tomado como extensión, y entonces no se podrá reconocer a partir de la misma el lenguaje del fuente. Para resolver este problema ZinjaI agrega automáticamente el "-x", pero solo cuando es necesario.


Finalmente llegamos a las opciones de compilación que especifica el usuario mediante la plantilla, o desde "Ejecución->Opciones...". Al tomar estos argumentos, se reemplazan las variables como "${MINGW_DIR}" por sus valores, y se ejecutan los sub-comandos como "`pkg-config...`". Es importante hacer tres aclaraciones aclaraciones: 1) por defecto, cada subcomando se ejecuta una sola vez, y su salida se cachea para las próximas compilaciones, 2) las plantillas usualmente incluyen en su lista de argumentos una variable "${DEFAULT}" que representa el set básico de argumentos que se define en las preferencias de ZinjaI, y se utiliza para modificar los argumentos por defecto de todos los programas simples sin cambiar el toolchain (de aquí salen argumentos como "-Wall" y "-pedantic-errors"), y 3) ZinjaI puede aplicar cambios adicionales cuando se utiliza un argumento que requiere cierta versión de GCC mayor a la disponible (por ejemplo, si se utiliza "-std=c++14" con gcc 4.8 o inferior, será reemplazado por "-std=c++1y"). Los puntos 1) y 2) se pueden configurar desde el cuadro de preferencias.

Para cerrar, solo resta añadir la ruta del archivo fuente,e indicar dónde debe ir a parar el ejecutable. Si es un archivo sin nombre, el ejecutable va a la carpeta de temporales; si es un archivo con nombre, el ejecutable va a la misma carpeta que el fuente. Se genera con el mismo nombre que el fuente pero con extensión diferente (".bin" en GNU/Linux, y ".exe" en Windows). Las rutas serán siempre rutas completas/absolutas y entre comillas para evitar problemas.


El comando de compilación resultante se ejecuta utilizando como directorio de trabajo el directorio del fuente (o el home del usuario si el fuente no está guardado). Las salidas std y err de la ejecución se procesan para rellenar el árbol de resultados de la compilación, y el código de retorno para saber si se logró generar con éxito un ejecutable. Si el código es 0, indica que por fin tenemos nuestro ejecutable listo para comenzar a trabajar.

Como vieron, pasan muchas cosas detrás de la tecla F9, y hay muchos puntos donde se puede tocar y cambiar. Y todo esto en el modo "programa simple". Cuando junte muuucha paciencia, me pondré a documentar de forma similar lo que ocurre detrás de F9 cuando hay un proyecto.

No hay comentarios:

Publicar un comentario