Archive for category Pigmeo Compiler

Reescribiendo el Frontend y el Backend

Tras mucho pelearme con la estructura interna del compilador durante meses, he decidido reescribir gran parte de Pigmeo Compiler.

Hasta ahora, el proceso de compilación seguía más o menos el siguiente esquema:
Se leía el programa del usuario (.exe que contiene CIL bytecodes) utilizando Mono.Cecil (una capa de abstracción sobre System.Reflection), se buscaban todas sus referencias, se creaba un nuevo ejecutable de .NET (.exe) desde cero, al que llamamos bundle, que contenía solamente las partes útiles del programa del usuario junto con las partes útiles de las librerías a las que se hacía referencia (es decir, las partes necesarias por el programa del usuario), después se añadía un CustomAttribute al bundle para indicar la arquitectura para la que estábamos compilando, el bundle era modificado directamente mediante reflexión para ser optimizado, y finalmente el Backend procesaba el código CIL contenido en el bundle buscando “conjuntos de instrucciones” que representasen operaciones conocidas (por ejemplo la instrucción ldc.i4 seguida de stsfld indicaría que se está almacenando un valor constante en una variable estática) para generar finalmente el código en lenguaje ensamblador.

Este proceso había sido diseñado de esta manera, centrándose en trabajar sobre ejecutables de .NET y siendo CIL el código intermedio utilizado internamente por el compilador, para ser simple y corto, sin necesidad de inventar nuevos lenguajes intermedios. No obstante, generar ejecutables de .NET desde cero y modificarlos es mucho más complicado de lo que parece, incluso utilizando librerías como Cecil que facilitan la tarea. Casi haría falta un MBA en .NET y C#, aunque con paciencia también se puede lograr.

Por lo tanto, durante los últimos meses he estado reescribiendo grandes partes de Pigmeo Compiler y, sobre todo, añadiendo capas de abstracción y pasos intermedios.

La aplicación del usuario sigue siendo leída utilizando Cecil, pero ahora existe una capa de abstracción sobre Cecil que se encuentra en el namespace Pigmeo.Internal.Reflection, y permite reflejar ejecutables de .NET (sólo lectura) de manera sencilla e incluye características no soportadas por System.Reflection ni por Mono.Cecil, además de más características especializadas que, aunque no tienen relación directa con .NET, son útiles de cara al proceso de compilación. Por ejemplo, podemos reflejar un archivo y una propiedad nos devuelve una referencia al objeto que representa la librería de dispositivo que ese programa está usando; también podemos saber fácilmente la arquitectura o microcontrolador para los que está diseñada la aplicación, o podemos desensamblar de manera sencilla todo el programa, o clases o métodos individuales (se utiliza especialmente para depurar el compilador).

Tras reflejar el programa del usuario (usando Pigmeo.Internal.Reflection), en lugar de generar el bundle con CIL bytecodes como se hacía antes, ahora se convierte el programa a PIR (Pigmeo Intermediate Representation) que, como su nombre indica, es una representación intermedia. Normalmente esto se hace con lenguajes intermedios, pero realmente no necesitamos un lenguaje, sólo una jerarquía de clases que representen el programa, así que no existe una sintaxis “oficial” para convertir PIR a string, aunque sí que están implementadas todas las funciones para poder mostrar por pantalla el programa una vez convertido a PIR (y, como en el caso de P.I.Reflection, se utiliza sobre todo para depurar el compilador).

PIR está diseñado para ser fácilmente manipulable (y fácil de comprender al mostrarlo en pantalla), de manera que podamos hacer múltiples cambios al programa y, sobre todo, diversas optimizaciones sin romper la lógica original del programa ni complicarnos con Cecil ni System.Reflection.

Una vez convertido el programa a PIR, éste es optimizado de la misma manera para todas las arquitecturas, tras lo cual se envía el PIR optimizado al Backend. El Backend estaba diseñado para “recibir” CIL bytecodes (del bundle) por lo que también ha sido reescrito para poder manejar PIR. En la primera fase del Backend se lee la arquitectura de destino del programa y se envía el PIR al Backend especializado en dicha arquitectura. Allí, el PIR vuelve a ser optimizado y modificado específicamente para la arquitectura de destino, y una vez preparado, ya puede convertirse el PIR a lenguaje ensamblador.

, , , , ,

2 Comments


Obteniendo información del ejecutable

Antes de compilar un archivo .exe para convertirlo a lenguaje ensamblador, podemos obtener información sobre el archivo, tanto desde la consola como desde la interfaz WinForms (pulsando en el botón que tiene un icono de información).

Cuando se soporten más interfaces gráficas será muy simple hacer que desde ellas también se pueda mostrar esta información.

Obteniendo información del archivo .exe desde la interfaz WinForms

Obteniendo información del archivo .exe desde la consola
Obteniendo información del archivo .exe desde la consola

, , , , ,

No Comments


Compilando desde WinForms y desde la consola

Compilando desde Linux usando la interfaz de WinForms:

Compilando desde la consola en Linux:

Este último vídeo también incluye algunos ejemplos de los parámetros que se le pueden pasar al compilador, y se ve cómo automáticamente detecta el idioma del sistema (que en la consola se configura mediante variables de entorno).

, , , , ,

No Comments


Pigmeo Compiler compilando

Compilando el siguiente programa escrito en C#: ejemplo001.cs

El compilador de C# que más nos guste nos genera el .exe (que hace referencia a PIC16F716.dll) y Pigmeo Compiler lo convierte a lenguaje ensamblador: ejemplo001.asm. Podemos fijarnos en que se compiló para el PIC16F716 (sin especificarlo a la hora de utilizar Pigmeo Compiler), ya que el compilador de pigmeo automáticamente detecta la “librería de dispositivo” que se utilizó cuando el programa en C# fue compilador a .exe y sin intervención del usuario sabe para qué arquitectura y qué rama/dispositivo estamos compilando, en este caso la arquitectura es PIC14 y el dispositivo es PIC16F716.

Y con mpasm, gpasm o cualquier otro programa ensamblador (que sea compatible) para PICs de 8 bits generamos el archivo binario (.hex) que podemos grabar en nuestro microcontrolador.

, , , , ,

No Comments


Editor de lenguaje ensamblador integrado

Para facilitar la depuración o para cualquiera que quiera ver el código fuente generado por Pigmeo Compiler he implementado un sencillo editor de texto con resaltado de sintaxis para lenguaje ensamblador, que abre automáticamente el archivo con el que estemos trabajando.

Vídeo de ejemplo del editor de lenguaje ensamblador [ogg + theora]
Vídeo de ejemplo del editor de lenguaje ensamblador [avi + xvid]

, , , , , , , , , , ,

No Comments


Video de ejemplo del compilador

La GUI de WinForms corriendo en Gentoo Linux, sobre Mono 1.2.6. Versión 0.0.9999-SVN de Pigmeo Compiler:
Vídeo de ejemplo de Pigmeo Compiler [ogg + theora]
Vídeo de ejemplo de Pigmeo Compiler [avi + xvid]

Se ve cómo se elige un archivo a ejecutar, automáticamente se configuran las rutas para los archivos de destino; luego configuramos para que se muestren los mensajes de depuración y cambiamos entre español e inglés un par de veces para que se vea cómo los controles se adaptan a cada idioma. Se vuelve al panel de compilación y compilamos.

Algunos mensajes de salida están en inglés y otros en español. Esto es así porque todo lo relacionado con los desarrolladores de pigmeo (incluyendo los mensajes de depuración) solamente está en inglés, ya que los usuarios finales no tienen por qué ver estos mensajes, en cambio los textos verbose (los precedidos por “INFO:”) se muestran en español (salvo que aún no estén traducidos).

Creo que para ser una versión de desarrollo (aún no hay ninguna release estable) no está nada mal.

, , , ,

No Comments


Interfaz WinForms del compilador actualizada

Ya he añadido todas las opciones de configuración del compilador a la interfaz WinForms.

El resultado (corriendo en linux):
Opciones de configuración del compilador

Opciones de configuración del compilador en inglés

Como se ve, lo he traducido todo al español. El idioma puede elegirse desde esta misma ventana (está al final del panel, se ve en la segunda imagen) y los controles cambian de posición y se redimensionan automáticamente según el idioma, así que la propia interfaz gráfica se adapta y no se necesitan distintas versiones para cada idioma.

, , , , ,

No Comments


El compilador de Pigmeo ya tiene cara

La primera interfaz gráfica que voy a implementar es la de WinForms, que aunque ni es estándar ni libre, sí es la más fácil de portar ya que Mono la soporta casi perfectamente en linux.

Una imagen del compilador utilizando la interfaz de WinForms en Gentoo Linux:
pantallazo de pigmeo compiler corriendo con winforms en linux

La misma versión del compilador, usando exactamente la misma interfaz y utilizando el mismo binario, sin necesidad de recompilarlo siquiera, corriendo en Windows XP con estilo clásico:
pantallazo de pigmeo compiler corriendo con winforms en windows

Y de nuevo en Windows XP pero esta vez utilizando el estilo de Windows XP:
pantallazo de pigmeo compiler corriendo con winforms en windows xp

Además el compilador automáticamente ha detectado que mi sistema está configurado en español, así que la interfaz se muestra en español (el idioma puede configurarse en tiempo de ejecución desde el panel de configuración del compilador), como puede leerse en el menú “Archivo”, pero como todos los demás strings aún no están traducidos entonces se muestran en inglés.

, , , , , , , , ,

No Comments


Console.WriteLine(), salida estántar (stdout) y salida de error (stderr)

Los que programamos en C# estamos tan acostumbrados a usar Console.WriteLine() para todo que a veces se nos olvida que existen las entradas y salidas estándares.

Console.WriteLine() y Console.Out.WriteLine() escriben un string a la salida estándar seguido de un caracter de fin de línea. Console.In es un objeto TextReader que nos permite leer fácilmente la entrada estándar. Console.Error.WriteLine() es quien nos permite mostrar un string en una línea de la salida de error.

He cambiado el código necesario para que los mensajes de error que se muestren por consola se redirijan a la salida de error, y tanto los warnings como los mensajes de aviso y depuración se muestren en la salida estándar. Cuando estamos en una interfaz gráfica todos estos mensajes se muestran mezclados en un solo TextBox, y además se envían inevitablemente (por elección de diseño, no por culpa de la implementación) a las salidas estándar y de error. De paso he agrupado los métodos que gestionan el envío de mensajes entre la lógica de compilación y las interfaces gráficas, de manera que esté todo bien organizado y sólo haya un sitio desde el que realmente se llamen a las entradas y salida estándar.

Por otro lado, y siguiendo mi intención de hacer el compilador tan portable como sea posible, lo he implementado todo de manera que si en el futuro se añaden al compilador interfaz ncurses o para el framebuffer, los mensajes que vayan a la salida estándar y de error no se muestren en la consola.

, , , , , , , ,

No Comments


Detectar automáticamente la consola, e ignorar la interfaz gráfica

Aunque he mantenido la lógica del compilador bien separada de las interfaces gráficas, lo que sí he hecho es integrar todas las interfaces dentro del mismo ejecutable. Al principio mi intención era generar una librería para cada interfaz gráfica y que ésta se cargase según los parámetros pasados desde la consola, o bien crear una librería que incluyese la lógica del compilador y además un ejecutable por cada interfaz existente, debiendo llamar a ejecutables distintos según la interfaz deseada. El problema es que para mostrar los avisos y errores generados por el compilador, y mostrar claramente el porcentaje de progreso de la compilación me vi obligado a integrar tanto la lógica de compilación como las interfaces en el mismo ejecutable, a no ser que quisiese emplear temporizadores para ir actualizando el progreso de compilación y mostrarlo de manera actualizada en la pantalla.

Entonces surgió otro problema: ¿qué ocurre cuando ejecutamos el compilador y en el sistema no hay soporte para cierta librería gráfica, o estamos en una consola, sin X11 ni Windows?

Por defecto la máquina virtual muestra una excepcion de inicialización de una clase perteneciente a la librería gráfica utilizada, pero es una solución muy poco elegante. Por lo tanto, Pigmeo Compiler detecta la plataforma de ejecución automáticamente (utilizando Environment.OSVersion.Platform) y se ejecuta por defecto utilizando la interfaz de GTK# cuando se encuentra en sistemas derivados de Unix y la interfaz WinForms cuando está en Windows, pero además se detecta cuándo una librería gráfica no está disponible, por lo que si ejecutándose desde Linux (por ejemplo) se intenta utilizar GTK# y no está disponible, automáticamente se cambiará a la interfaz basada en WinForms, que también funciona en Linux. Además, si ninguna interfaz gráfica está disponible (porque la máquina virtual no encuentre las librerías necesarias o porque nos encontremos en una interfaz sólo texto) se muestra un aviso y se comienza la compilación desde la consola.

Por supuesto, esto solamente es el comportamiento por defecto, y podemos elegir manualmente la interfaz que queremos utilizar con el parámetro –ui de Pigmeo Compiler.

, , , ,

4 Comments


monomerge

Al poco tiempo de haber comenzado a programar Pigmeo Compiler encontré monomerge, una aplicación que agrupa varios assemblies de .NET (ejecutables y librerías) en un solo ejecutable. Aparentemente me iba a ahorrar gran parte del trabajo, lo que viene a ser casi todo el frontend de Pigmeo Compiler.

Tras varios intentos fallidos y tras darme cuenta de que gran parte del código tendría que reescribirlo de todas formas, desistí y comencé a trabajar en mi propio “mezclador” de assemblies. Con esto me refiero a que monomerge agrupa las librerías y los ejecutables con todos los tipos que incluyen, es decir, todas las clases y estructuras que existen en los assemblies. Al principio parece una buena idea, es bastante cómodo de implementar y tenemos lo que queremos: un solo ejecutable con todas las librerías necesarias, sólo necesitamos una máquina virtual (el CLR de .NET) y ya tenemos nuestra aplicación funcionando sin un montón de librerías molestando. Realmente esto no es tan sencillo, porque los compiladores que generan estos assemblies incluyen en ellos absolutamente todos los tipos (clases y estructuras) que estén definidos en el código fuente, y eso es un problema a la hora de compilar para un microcontrolador, con recursos muy limitados, ya que la mayoría de propiedades y métodos implementados en las clases y estructuras nunca llegan a usarse.

Por lo tanto monomerge descartado, y estoy escribiendo el frontend del compilador desde cero, procesando instrucción a instrucción (en CIL) empezando por el EntryPoint de la aplicación (normalmente la función estática main(), pero no necesariamente), y detectando exactamente qué propiedades (variables) y métodos (funciones) y de qué tipos (clases y estructuras) realmente se utilizan en la aplicación, para lograr que en la aplicación compilada para microcontroladores sólo se implementen las cosas utilizadas y realmente necesarias, y no haya una sobrecarga innecesaria y completamente ineficiente.

, , , , , ,

No Comments


AOP, reflexión (reflection), Cecil

El proyecto pigmeo se divide en varios subproyectos. La piedra angular del compilador (pigmeo-compiler uno de los subproyectos) es su paradigma: la programación orientada a aspectos. Utilizando las clases del namespace System.Reflection de .NET podemos modificar directamente los binarios de .NET. Como System.Reflection es algo engorroso utilizaré un librería intermedia: Cecil. Es GPL así que no hay ningún problema en utilizarla. El usuario final obviamente no tendrá que preocuparse por Cecil, reflexión ni AOP ya que sólo se utiliza desde pigmeo-compiler, y no desde pigmeo-framework (el framework utilizado por los usuarios finales).

Cecil está muy bien organizado y permite acceder a todas las clases, métodos, instrucciones en lenguaje intermedio (CIL)… mediante una jerarquía bien estructurada. El gran problema de Cecil es que no tiene absolutamente nada de documentación, solo unos pocos ejemplos, y ni siquiera está documentado el código fuente mediante XML (muy útil para ver la descripción de clases y objectos en el autocompletado del entorno de desarrollo), por lo que te encuentras con cientos o incluso miles de clases y funciones con nombres extraños y sin ningún tipo de explicación, así que acceder a algunas partes del ejecutable que se está intentando modificar resulta prácticamente imposible.

Cecil se utilizará para la primera parte del compilador: el frontend. El frontend lee el ejecutable de .NET y genera OTRO ejecutable con el mismo programa pero organizado de una manera totalmente diferente, parecida al bajo nivel (o más bien “desorganización”) del lenguaje ensamblador. Por ejemplo las clases estáticas con sus propiedades también estáticas funcionan como variables globales en C y C++, son accesibles desde cualquier parte del programa. Estas propiedades estáticas en el frontend se cambian de nombre a uno válido para el programa ensamblador, y se agrupan en una sola clase que más tarde en el backend específico para la arquitectura de destino se convertirán de golpe a variables globales en lenguaje ensamblador. La idea es que a partir del ejecutable de .NET generado por el compilador de C#, VB.NET o el lenguaje que sea, se genere un segundo ejecutable de .NET tan válido como el primero pero estructurado de manera que sea mucho más fácil convertirlo a lenguaje ensamblador, eliminando la complicada estructura original del ejecutable y además optimizando el código para todas las arquitecturas, es decir, optimizaciones que son válidas para cualquier arquitectura de destino, y no dependen de ninguna en particular. Las optimizaciones específicas para cada arquitectura se procesarán en el backend.

Por si a alguien le parece extraño que diga “ejecutable de .NET” que sepa que el compilador del lenguaje de alto nivel utilizado (C#, VB.NET,C++/CLI, nemerle…) lo que genera no es código máquina, sino un “.exe” que aunque comparte la extensión de los ejecutables nativos de Windows NO lo es, sino que es un archivo que contiene, entre otras cosas, el código en lenguaje intermedio (CIL) que para ser ejecutado debe ser interpretado por una máquina virtual o CLR (Microsoft .NET, mono, Portable .NET…).

No me extiendo más, porque para eso está la documentación para desarrolladores.

, ,

1 Comment