El compilador de Delphi (Win 32)


Cuenta la leyenda que CA-Clipper fue un galeón de guerra muy respetado, con hordas de guerreros que, disparando aplicaciones de 16 bits, libraban batallas memorables y daban la vida por él. Todas fueron glorias hasta que un día, sin explicación, sus tripulantes lo dejaron a la deriva.
Algunos permanecieron en la embarcación intentando tripularla, pero no son pocos los que han sabido abandonar el barco desde ese entonces, hace ya varios años. Quien esto escribe, dada su cobardía, se encontraba entre los primeros que huyeron.
No bien abordó Delphi preguntó altaneramente: “¿Cómo se hace para que mi aplicación ejecute una instrucción, que previamente he guardado en un campo string de una base de datos?”
Para un veterano de Delphi, la respuesta es obvia: esto es imposible.

 

– Ah. Dije, -no sin cierta decepción- ¿no se puede?
Para un programador acostumbrado a lenguajes como Clipper, FoxPro, etc, esto puede parecer una carencia: ¿Cómo es que Delphi no es capaz de hacer lo que yo hacía con mi viejo Clipper? ¿Acaso un ejecutable Delphi no es capaz de entender una instrucción almacenada en una cadena?
– Pues no. Me dijeron, dándome una palmadita en la espalda, que no entendí.

¿Es una carencia del lenguaje?

Los lenguajes de programación no pueden ser comparados en esos términos, debemos ver la cuestión desde una distancia mayor: de lejos, la cosa cambia. Lenguajes como Clipper y Delphi no pueden compararse de esa manera.
Antes bien, habría que hacer una distinción ineludible: Clipper es un lenguaje “interpretado”, mientras que Delphi es compilado.
-¿Y esto qué significa?
Que Delphi, al compilar, genera sólo código ejecutable.
El “compilador” Clipper, al “compilar” genera un código que podemos denominar “intermedio”. Esto significa que parte del código que el programador Clipper escribe, no se compilará en el momento de generar el archivo ejecutable, sino cada vez que éste se ejecuta. Y esto es posible gracias a que cada ejecutable Clipper lleva consigo un intérprete de tiempo de ejecución, un compilador (comúnmente llamado runtime) que hace justamente eso: ¡Compilar!
No hace falta que le diga que cuando escribimos código estamos generando texto ¿verdad? Puro texto. Puede escribir cualquier cosa en el editor de Delphi. Una novela si lo desea, pues un editor de texto genera justamente eso, texto. ¿Quién es el que se “dará cuenta” que la novela que ha escrito no es justamente un programa? Pues este señor que nos ocupa: el compilador de Delphi.

El compilador de Delphi en el contexto actual….

El hecho de ser un lenguaje “compilado” fue una de las tantas características que, desde su primer versión, diferenció a Delphi de sus competidores, allá por la era glacial de los lenguajes visuales, 1995. Mientras Visual Basic necesitaba un “runtime” para sus pseudo-ejecutables, Delphi generaba directamente código ejecutable.
Este panorama, paradigma, está cambiando en estos días, con el advenimiento de la nueva arquitectura para el desarrollo de aplicaciones propuesta por el señor de anteojos que preside Microsoft.
.NET difiere indudablemente respecto al modelo tradicional de creación de aplicaciones sobre Windows, proponiéndonos un cambio radical en la forma de comunicarnos con el sistema operativo. El API Win32 fue pensado hace ya una década y no puede decirse que sea lo más moderno y robusto. De hecho lenguajes orientados a objetos como Delphi han tenido que desarrollar envoltorios o “wrappers” para convertir en objetos o componentes ciertas tareas que el API Win32 nos brinda pero a través de funciones y sin un serio control de excepciones.

 

  • BCL (Base Class Library).   .NET FrameWork
  • CTS (Commont Type System)
  • CLR (Common Language Runtime)  Entorno de ejecución.

Las tres capas de .NET

Si .NET nos propone ahora -entre otras cosas- un acceso más homogéneo a los recursos del sistema operativo, podemos deducir que en un futuro, cuando el de anteojos lo disponga, los sistemas operativos de las ventanitas dejarán de soportar  el API Win32.
Puede decirse que otra ventaja de utilizar la arquitectura .NET es el hecho de que cualquier entorno de desarrollo .NET puede “compartir” sus objetos .NET en la forma de IL Code (Intermediate Language). El “entorno común de ejecución” o CLR (Common Language Runtime) ejecutará el IL Code escrito en cualquiera de los lenguajes sea Visual C++, Visual Basic .NET, C#, Delphi, etc.
¿Cómo es esto posible? Pues el CLR es un entorno de ejecución, una máquina virtual –término acuñado por los mentores de Java-, un programa que interpreta, compila y ejecuta, el código intermedio. Esto significa que si utiliza Delphi 8 para generar aplicaciones .NET, estará generando un código “intermedio” que será compilado en tiempo de ejecución.
Ahora bien, si desarrolla aplicaciones tradicionales, utilizando la librería VCL o la joven CLX, generará  aplicaciones que, aparte de generar un código de más rápida ejecución -ya que no requieren el Runtime de .NET- serán en su totalidad código ejecutable, “código máquina”.
Pero volvamos al compilador de Delphi, que no escapa a la generalidad de los compiladores. Entonces..

¿Qué es un compilador?

Puede decirse que un compilador es un programa que transforma un programa fuente en un programa ejecutable. Un programa fuente está compuesto en su mayor parte, de texto. Es el entorno (IDE) el que hace que nuestro proyecto Delphi esté realmente integrado. Es decir, el texto escrito en Object Pascal por un lado, los recursos por el otro. Pero no hablaremos de los recursos aquí, para mayor información al respecto, puede ver el artículo de Sebastian Silva de este mismo número “Dentro del archivo ejecutable .EXE”.
Volvamos al IDE de Delphi, hemos tecleado en el editor de texto la siguiente cadena:

Coeficiente := Importe/1234

es el compilador el que se ocupará de trasformar dicha cadena en una instrucción ejecutable que divida la variable importe por 1234 y lo asigne a la variable coeficiente. Para ello, primero deberá convertir la cadena “1234” en el número 1.234.
Habíamos visto ya que en el editor de texto puedo escribir literalmente cualquier cosa ¿verdad? ¿Por qué el compilador se niega a compilar “cualquier cosa”?….

Coeficiente := “CualquierCosa”/1234

…es decir, las aberraciones que el viejo Clipper me dejaba hacer?
¿Le dije ya que Clipper no es un lenguaje compilado, verdad? Clipper no compila esa sentencia al momento de generar el ejecutable. Si Clipper le “permite” escribir semejante barbaridad es justamente porque no lo compila. El bochornoso error vendrá luego, en tiempo de ejecución, cuando realmente se intente compilar
Veamos más de cerca que hace un compilador.
Básicamente un compilador es un programa o aplicación que lee y procesa un segundo programa escrito en un lenguaje, el lenguaje fuente, para transformarlo en un programa equivalente en otro lenguaje de uso final. Este compilador puede estar o no integrado en un entorno de desarrollo. Nuestro conocido IDE de Delphi no es sólo un compilador sino que “contiene” además de muchas otras prestaciones, al compilador.
Un compilador es un proceso o programa, en el que pueden distinguirse dos subprogramas o fases principales: una fase de análisis y otra de síntesis. Antes de pasar a su descripción, puede echar un vistazo a la figura 1.

 

 

  • La fase de análisis lee el programa fuente, estudia su estructura y su “significado”. En esta parte, se divide al programa fuente en sus partes constitutivas y se crea una representacion intermedia de él. Esto es conocido como lenguaje objeto (los archivos *.dcu o los mas antiguos .obj).
  • La fase de síntesis construye el programa de uso final (*.exe, *.dll, etc) a partir de esa representacion intermedia.

 

Asimismo análisis y síntesis pueden ser divididos en distintas partes: Análisis léxico, análisis sintáctico y análisis semántico para la fase de análisis, y generación de código intermedio, optimización de código y generación de código final para la fase de síntesis.

Existen otras dos tareas que todo compilador debe realizar: control de errores y manejo de la tabla de simbolos. Estas dos tareas estarán presentes a lo largo de más de una fase de las previamente mencionadas.

Tabla de símbolos

El manejo de tabla de símbolos es esencial en el funcionamiento de un compilador. Una tabla de símbolos es una estructura de datos que contiene un registro por cada uno de los identificadores (en muchos manuales encontrará la palabra tokens) encontrados en el programa fuente. Se trata de almacenar en memoria los identificadores usados en el programa fuente con todos sus atributos. Estos atributos pueden proveer información (por ejemplo) acerca de: almacenamiento reservado, tipo, alcance, si se trata de un palabra reservada, etc.

Manejo de errores.

 

El manejo de errores es una parte muy importante de todo compilador que se precie de bueno. Luego de detectar un error durante una fase, el compilador debe “negociar” con el error para que la compilación pueda proseguir y permitir que posibles errores subsiguientes puedan ser detectados. Un compilador que se detiene al primer error encontrado puede ser muy molesto y generar pérdidas de tiempo excesivas, sobre todo para los que escribimos mucho código erróneo. Si echa un vistazo a la figura 2 verá que cualquiera de los 5 errores detectados por Delphi impide por sí mismo la creación del ejecutable final. Sin embargo el compilador de Delphi prosigue “leyendo” el código para poder presentar la mayor cantidad de errores detectados.

 

– Mimnio, athesa, eioioio….  eioi….

Es posible que no le encuentre sentido a esta frase. No se preocupe. Es que pertenece al léxico de los “manos”, unos extraterrestres que supieron invadir la tierra en una obra literaria de Héctor Oesterheld1

Le cuento que uno de los “manos” ha estado viviendo en mi casa, o eso creo recordar. La cosa es que me ha asegurado que la frase es sintáctica y semánticamente correcta, sólo que los terráqueos no llegamos a entenderla. ¿Por qué? Pues porque tenemos otro léxico.
Escriba “DbSeek()”, instrucción correcta del lenguaje Clipper, en el IDE Delphi. Ahora intente compilar. Verá que el compilador no lo reconoce como una instrucción. “DBSeek()” no pertenece al léxico de Object Pascal. Pero no confunda el léxico con el conjunto de palabras reservadas: Puede en Delphi escribir su propio procedimiento llamado DBSeek(). A partir de ese momento formará parte del léxico y el analizador léxico no protestará.
¿Qué es entonces lo que hace el analizador léxico? Pues debe detectar cuando los caracteres escritos no conforman ningun identificador del lenguaje.
Probemos ahora palabras de nuestro léxico…

Si llego a morirme, tendré más cuidado.

A no ser que crea en la reencarnación, esta frase no tiene mucho sentido ¿o si? Esta excelsa construcción literaria puede “pasar” exitosamente un análisis sintáctico, pero su semántica es dudosa. Con un compilador, la cosa es exactamente igual, el análisis sintáctico consiste en detectar donde el flujo de identificadores viola las reglas estructurales del lenguaje (sintaxis), no importa aquí si lo que hace el programa tiene “sentido”.
Suponga ahora las siguientes instrucciones:

if InicializaCorrectamente() then Application.Terminate ;

Aunque dudemos de la salud mental de su autor, si el procedimiento InicializaCorrectamente() está bien escrito el compilador no protestará. El análisis sintáctico trata de verificar que la sentencia esté bien “construída”.
Si el analizador “lexicográfico” ha tomado la siguiente sentencia…

Variable1 := variable2 + incremento*6

seguramente producirá (en líneas generales) código intermedio y simplificado para el análisis sintáctico posterior, por ejemplo…

<id1> <:=> <id2> <+> <id3> <*> <int>

…donde cada elemento encerrado entre <> representa un único identificador. Las abreviaturas id e int significan identificador y entero, respectivamente.
Con estos elementos, el analizador sintáctico comprueba que las sentencias que componen el texto escrito son correctas en Delphi, creando una representación interna que corresponde a la sentencia analizada. Así, el compilador nos garantiza que sólo serán procesadas las sentencias que pertenezcan  (que sean correctas) en Object Pascal. Durante el análisis sintáctico, así como en el análisi léxico anterior, se van “almacenando” los errores que se encuentran, para presentarlos luego en la ventana de mensajes que ha podido ver en la figura 2.
Si volvemos al código escrito, verá que corresponde a una sentencia de asignación de Object Pascal. Estas sentencias son de la forma:

<id> <:=> <EXPRESION>

y la parte que se denomina puede ser de distintas estructuras, todas correctas, por ejemplo:

<id> <+> <EXPRESION>
<id> <-> <EXPRESION>
<id> </> <EXPRESION>
<id> <*> <EXPRESION>
<real>

¿Tiene sentido?

Por último, el análisis semántico se ocupa de analizar si la sentencia tiene algún significado. Ya hemos visto que podemos encontrar sentencias que son sintácticamente correctas pero carecen de sentido. En algunos casos -por carecer de sentido- no se podrán ejecutar.
Por esto mismo, y por la ligera ambigüedad del concepto “sentido”, podría decirse que en algunos casos el análisis semántico se hace a la par que el análisis sintáctico introduciendo en éste unas rutinas semánticas.
Para un ejemplo: en la sentencia que hemos visto existe una constante de tipo entero. Sin embargo, las operaciones matemáticas realizadas y la asignación final se realizan entre identificadores de tipo real, por lo que un compilador general tendría dos alternativas: o emitir un mensaje de aviso del tipo « Discordancia de tipos », o realizar una conversión automática al tipo superior, mediante una función auxiliar interna. Esto último es lo que hace el compilador de Delphi.

El generador de código intermedio

¿Ha oído la expresión “lenguaje de bajo” nivel? No, no estoy hablando de Visual Basic sino todo lo contrario. Un lenguaje de programación de bajo nivel es aquel que es fácilmente trasladado a lenguaje de máquina. Lo que conocemos como ensamblador o “assembler” es justamente de bajo nivel. ¿Por qué? Porque está allá abajo, muy “cerca” del procesador. Si desea leer un poco más acerca de ensamblador y Delphi vuelvo a recomendarle el artículo de Sebastian Silva de este número.
Los lenguajes de bajo nivel tienen algunas ventajas, como la mayor adaptación al equipo y la posibilidad de obtener máxima velocidad con mínimo uso de memoria. Por ello son ideales para programar controladores (drivers). También tiene sus inconvenientes, como (justamente) la imposibilidad de escribir código independiente de la máquina y una mayor dificultad en la programación y en la comprensión de los programas.
Por esta razón, hace ya más de 40 años comenzaron a surgir nuevos tipos de lenguajes que evitaban esos inconvenientes a costa de ceder un poco en las ventajas. Se los suele llamar « de tercera generación » o « de alto nivel », en contraposición a los « de bajo nivel » o « de nivel próximo a la máquina ». Entre los lenguajes de alto nivel podemos encontrar a nuestro querido Delphi.
¿Y qué tiene que ver esto con el generador de código intermedio? Pues, el código intermedio es un código que podemos denominar “abstracto” ya que es independiente de la máquina para la que se generará el código objeto. Debe ser producible a partir del análisis léxico/sintáctico, y ser fácil de traducir al lenguaje objeto. Podríamos decir que el código objeto es justo el paso  intermedio entre el alto nivel original (lo que hayamos escrito en el editor de código) y el bajo nivel resultante.
En algunos compiladores esta fase puede no existir y generarse  directamente código máquina. No es así en el caso de Delphi.

El optimizador de código

A partir del código intermedio crea un nuevo código más compacto y por lo tanto más eficiente. Para ello deberá eliminar sentencias que no se ejecutan nunca, sentencias que se ejecutan inútilmente, simplificará expresiones aritméticas, etc…
Veamos un ejemplo simple: imagine las siguientes sentencias:

E := b + c + d + 1 ;
F := b + c + d + 2 ;
G := b + c + d + 3 ;
H := b + c + d + 4 ;

Casi ya no quedan especímenes que sean capaces de escribir este código, pero imaginemos por un momento que no se han extinguido aún y que el compilador de Delphi debe lidiar con ellos (un caso en que la máquina supera al hombre).
Para asignar estas cuatro variables, el procesador deberá realizar cuatro veces la misma operación de suma ¿verdad? Allí es donde el optimizador de código del compilador entra en acción y realiza algo como lo siguiente:

x1 := b + c + d ;
E := X1 + 1 ;
F := X1 + 2 ;
G := X1 + 3 ;
H := X1 + 4 ;

Donde la operación más “compleja” pasa a realizarse sólo una vez. Esto es conocido como “factorización de expresiones comunes”. Otra operación típica de los optimizadores de código es la “extracción de invariantes”, veamos:

repeat
i := 8 ;
Inc(n,i);
until n> 200 ;

Aquí el invariante es i. No confundir con una constante, se trata de una variable que no cambia su valor dentro del bucle. ¿Por qué ejecutar su asignación en cada iteración? El optimizador de código genera lo siguiente:

i := 8 ;
repeat
Inc(n,i);
until n> 200 ;

La calidad de los optimizadores de código varía en general de una versión a otra, intentando optimizar cada vez más el código generado.

Generador de código

A partir de los análisis anteriores y de las tablas que estos análisis van creando durante su ejecución produce un código o lenguaje objeto que es directamente ejecutable por la máquina. Es la fase final del compilador. Las instrucciones del código intermedio se traducen una a una en código máquina. A partir de la receta escrita en el IDE, Delphi nos ha horneado un hermoso .exe. A saborearlo.

Dependiente, independiente

A menudo las fases de la compilación son agrupadas de acuerdo a su dependencia (o no) de la plataforma utilizada. Estas dos “partes” o divisiones conceptuales son comúnmente llamadas Front-End y Back-End.

Front-end

Consiste en aquellas fases o partes de fases que dependen primariamente del lenguaje fuente, y son independientes de la plataforma final de uso de la aplicacion generada. Esto normalmente incluye análisis  léxico y sintáctico, creación de la tabla de símbolos, análisis semántico y generación de código intermedio. Parte de lo que conocemos como optimización de codigo puede ser realizado por el front-end, que además debe tener funcionalidad de manejo de errores en todas sus fases.

Back-end

El back-end incluye todas las tareas del compilador que dependen del plataforma final de uso de la aplicación. Generalmente estas partes no dependen del lenguaje fuente, sino del lenguaje intermedio (y el sistema operativo final).
Dentro del back-end encontraremos aspectos de la fase de optimización de codigo y de la generación de código, además de la omnipresente funcionalidad de manejo de errores.

Distintas versiones de un mismo ejecutable?

 

Algunas empresas desarrollan programas de libre distribución, comúnmente llamados shareware. Una vez probados por el posible cliente, éste abona cierta suma vía Internet, y el proveedor le proporciona un código para que pueda convertir la versión de evaluación en una versión definitiva o “registrada”.
Esto significa que el “demo” ejecutable, disfrazado de un humilde sapo, era en realidad, el príncipe, y que llevaba todo consigo, pero bajo sus ropas. Sólo hacía falta el beso ¿verdad?
Significa también que los señores de parche en el ojo, pueden intentar convertir al sapo en la versión “registrada” del mismo. Sólo es cuestión de una perseverancia, directamente proporcional a la complejidad de los intrincados mecanismos que ideemos para protegerlo.
Suponga ahora que necesita publicar una “demo” de su aplicación, para distribuirla por Inernet o a través de un CD ¿Acaso no podemos compilar de una manera fácil una versión con menos prestaciones que la definitiva, que no sea “crackeable”, que no lleve dentro de sí aquella funcionalidad que no queremos arriesgar? Si lo primero que le ha venido a la mente es crear otro proyecto, que comparta la mayoría de las unidades y se diferencie en otras, no se pierda lo que viene a continuación, verá que es mucho más fácil.
La idea es tener un sólo proyecto, y concretar esas diferencias al momento de compilar.
Decía que es el compilador el que procesa el texto escrito por el programador. Y es allí, en el texto, donde podemos comunicarnos con él. A través de…

Las directivas de compilación

Como su nombre lo indica, son órdenes dictadas al compilador, para controlar su comportamiento. En líneas generales, existen dos formas de utilizar las directivas de compilación. La primera, más accesible e intuitiva, es utilizar las casillas de verificación situadas en la pestaña “Compiler” del formulario de configuración que parece al ejecutar Project->Options. Puede ver una instantánea del susodicho en la figura 3.
Vamos ahora a la segunda forma de utilizar directivas. Podría decirse que todo proyecto Delphi lleva alguna directiva de compilación dentro de su código, incluso el proyecto que Delphi nos propone cuando elegimos File->New->Application en su menú principal tiene alguna directiva de compilación. Algunos libros dicen que parecen comentarios pero no lo son. Según dichas publicaciones, lucen similares a los comentarios dentro del código, pero sin serlo.
Ahora bien, a diferencia de las sentencias normales de cualquier lenguaje, lo que hacen las directivas de compilación es incidir sobre el comportamiento del compilador. Una directiva de compilación, si bien es texto procesado por el compilador, no se trata de una instrucción “compilable”. No se generará con ella ningún código intermedio. Desde ese punto de vista ¡es un comentario!.
Entonces, no son otra cosa que “comentarios” dentro del texto a compilar, que controlan la tarea a realizar por el compilador. Recuerde que esto no significa que todo cambio en ese comportamiento vaya  a tener incidencia sobre el ejecutable final. La directiva {$Warnings Off}, por darle un ejemplo, sólo le dice al compilador que no muestre los mensajes de aviso (warnings) durante la compilación.
Seguramente ya sabrá que los comentarios en Delphi pueden establecerse de tres modos diferentes:

  • // Esto es un comentario
  • (*Esto es otro comentario*)
  • {Uno más}

Las directivas de compilación son del tercer tipo, es decir, se encierran entre llaves. Las reconocerá fácilmente dentro del texto que comúnmente le propone Delphi: comienzan con el signo $, como tantos de mis pensamientos:

{$ IFDEF WINDOWS}  // Si el sistema operativo es Windows compilar…

// una función sólo válida para Windows
ShellExecute( Application.Handle, nil, PChar(Log.FileName),
nil, nil, SW_SHOWMAXIMIZED );

{$ ENDIF}

Para que el comentario funcione como una directiva de compilación, el primer carácter debe ser el signo $. Eso significa que puede desactivarse simplemente modificando esa condición:

{ $ IFDEF WIN32}  // Al insertar un espacio el comentario deja de tener
// efecto sobre el compilador (como cualquier comentario)

Por ejemplo, la directiva {$R} le ordena al compilador que enlace determinado archivo de recursos dentro del ejecutable final. Por defecto, cada unidad asociada con un formulario incluye al menos una directiva de compilación {$R}, que indica al compilador que incluya la versión binaria del formulario en el archivo de código intermedio *.dcu y luego en el ejecutable. Si desea más detalles de tan intricado asunto, sugiérole nuevamente el artículo de Sebastián Silva de este mismo número.

Los tipos de directivas

El compilador de Delphi tiene tres tipos de directivas de compilación:

  • Conmutadores (Switches). Es un interruptor que activa -o no- cierta funcionalidad.
  • Parámetros (Parameters). Proveen información, como el nombre de un archivo.
  • Condicionales (Conditionals). Permiten definir condiciones para, por ejemplo, compilar  selectivamente (o excluir de la compilación) parte del código.

Conmutadores

Los conmutadores habilitan o deshabilitan ciertas funcionalidades. Por ejemplo, la directiva {$B} es un conmutador. Puede activar o habilitar un conmutador escribiendo el signo de suma o la palabra ON luego del identificador de la directiva, para desactivar o deshabilitar necesitará el signo de resta o la palabra OFF:
La directiva {$B} le “dice” al compilador de Delphi cómo debe comportarse el ejecutable final: si continuar (o no) con la evaluación de la segunda parte de una expresión booleana de más de un argumento cuando dispone ya del resultado sin haber evaluado todos los argumentos. {$B-} (su valor por defecto) significa no continuar {$B+} significa continuar la evaluación.
Veamos un ejemplo, en su valor por defecto, con la expresión:

expresion1 and expresion2 ;

expresion2 no será evaluada si expresion1 vale false. Apliquemos la directiva dentro del código:

{B+}
a := expresion1 and expresion2 ;
{B-}

b := expresion3 and expresion4 ;

Con la directiva {$B+}, la evaluación continúa. Expresión2 se evaluará aunque expresión1 valga false. No ocurrirá lo mismo con expresion3 y expresion4.

Parámetros

Las directivas de tipo parámetro identifican un nombre de archivo, una extensión de archivo o algún tipo de configuración requerida por el compilador. La directiva {$R}, comentada anteriormente, es un típico ejemplo de directiva de tipo parámetro. Otro ejemplo es la directiva {$I}, que le indica al compilador que el archivo definido en el parámetro debe ser “incluído” en el texto como si hubiera sido escrito allí mismo. Veamos un ejemplo:

{$I Definic.inc}

Aquí la directiva le indica al compilador que debe leer el archivo suministrado (Definic.inc) e incluirlo enteramente allí donde la directiva haya sido escrita. Lo que contenga el archivo Definic.inc será procesado como código Delphi. Por lo tanto puede utilizarlo para separar definiciones condicionales, más directivas de compilación y declaraciones de rutinas y subrutinas. La extensión *.inc no es obligatoria, es una simple convención.

Condicionales

Las directivas condicionales nos permiten excluir o incluir selectivamente cierta parte del código escrito.
La directiva {$IfDef} define el inicio de una sección de código de compilación condicional. ¿Qué significa semejante cosa? Pues que dada esa condición, ese trozo de código se compilará o no. Y ya sabemos que lo que no se compila no “viaja” dentro del ejecutable.

{$IfDef DefName}

En la declaración anterior, IfDef es la directiva. La palabrita Defname no es un parámetro sino la definición de un símbolo condicional. Veamos un uso real. Supongamos que tiene alguna funcion que no desea “exponer” en su versión de demostración. Tampoco quiere que esté “escondida”, simplemente no debe compilarse dentro de la “demo”.

procedure TForm1.Item1Click(Sender: TObject);
begin

{$IfDef VERSION_FULL}
AlgunaFuncion() ;
{$Else}
MostrarAcercaDe() ;
{$EndIf}

end;

El código anterior le indica al compilador que, en el caso de estar presente el símbolo VERSION_FULL, se compile AlgunaFuncion(). En caso contrario se compilará lo que figura dentro de la cláusula {$Else}, en este caso una ventana “Acerca de”.
Recuerde que la directivas de compilación pueden utilizarse a lo largo de todo el código, esto significa que podrá encontrarlas también dentro de la cláusula uses. Por ejemplo

uses
Windows, Messages, SysUtils, Variants, Classes,
{$IFDEF VERSION_1}
AlgunaLibreria ,
{$ENDIF}
{$IFDEF VERSION_2}
OtraLibreria,
{$ENDIF}
Graphics ;

El código anterior compilará la unidad AlgunaLibreria.pas en el caso de estar presente el símbolo VERSION_1, en caso de estar presente VERSION_2 compilará la unidad OtraLibreria.pas.
Ahora bien. ¿Cómo hacer que esté o no presente el símbolo de marras? La forma más simple es ingresar directamente el símbolo en la casilla “Conditional defines” de la pestaña “Directories/conditionals” del antecitado formulario “opciones de proyecto”. Puede ver una instantánea alusiva en la figura 4 y acceder a él mediante el menú Project->Options. Recuerde que luego de cambiar los condicionales, deberá recompilar todo el proyecto.
Puede ingresar mas de un símbolo en el susodicho casillero, deberá separarlos por punto y coma. Tenga algo de cuidado con esto, para el ejemplo anterior podría compilar las dos unidades aparentemente contrapuestas con sólo ingresar  “VERSION_1;VERSION_2” en el casillero.

La otra forma de definir un símbolo condicional es a través de la directiva {$Define}. Por ejemplo:
{$Define VERSION_BETA}  // Ahora el símbolo está activado.
Esta forma de hacerlo sirve para trabajar en un ámbito local (local scope), por ejemplo sólo en una unidad.
En los anteriores ejemplos hemos trabajado con símbolos definidos por nosotros mismos, existen otros que podemos denominar estándar, son los definidos por los señores de Borland y puede hacer uso de ellos para mejorar la evolución de su código fuente, veamos algunos ejemplos:

Símbolo

Versión de delphi

WIN16 16-BIT
WIN32 32-BIT
VER80 Delphi 1.0
VER90 Delphi 2.0
VER100 Delphi 3.0
VER120 Delphi 4.0
VER130 Delphi 5.0
VER140 Delphi 6.0
CONSOLE Indica que se trata de una aplicación de consola
WINDOWS Si se trata de MS Windows
LINUX Si se trata de Linux

Por supuesto, no todos estos símbolos estarán presentes en todas las versiones, han sido agregados a medida que la gente de Borland ha ido necesitándolo. A partir de Delphi 6 disponemos de los dos últimos símbolos de la tabla (entre muchos más que han sido agregados) que son pilares de la tecnología que Borland anunció con bombos y platillos en su momento y que luego olvidó: la plataforma cruzada. Si compila un proyecto en Delphi el símbolo WINDOWS estará activado, si lo hace en Kylix se activará el símbolo LINUX.

Terminando con las directivas

El compilador entenderá perfectamente varias directivas combinadas en un solo comentario, por ejemplo:

{$R+,Q+,Align On}

Como se ve, deben separarse por comas, no dejando espacios. Si utiliza una directiva de nombre compuesto, debe ser la última.
Algunas directivas son locales para una parte del código, pueden habilitarse o deshabilitarse tantas veces como queramos a lo largo del código. Ya hemos visto ejemplos de esto. Las hay también con un ámbito de acción (scope) para todo el archivo allí donde aparecieren. Por último algunas directivas son globales a todo el proyecto, en general aparecen en el archivo de proyecto: *.dpr (aplicación), *.dpk (paquete).
La otra opción para las directivas globales es especificarlas, como le he comentado, en la pestaña “compiler” del formulario “Project Options” (figura 3). Lo que hará el IDE de delphi es crear un archivo con el mismo nombre de su proyecto y extensión .cfg. Si aún no ha echado un vistazo a ese archivo, cuando lo haga verá como texto las directivas de compilación ingresadas en el formulario del IDE.
No hemos visto aquí todas las directivas ni todos los símbolos, sólo ha sido una humilde introducción al tema. Recuerde que las directivas de compilación no son sensibles a mayúsculas/minúsculas (case-sensitive), como el mismo lenguaje Object Pascal. De todas formas le aconsejo siempre trabajar con cuidado, no todos los lenguajes son iguales y si mañana desea abordar otro barco, no le convendrá llevar consigo malos hábitos.

El programa compilador

Estaba listo para ir a visitar a mi cliente, iba a llevarle una actualización de mi revolucionario programa con las nuevas funcionalidades Tenía el coche ya en marcha, suficiente lugar en el baúl para el nuevo ejecutable y sobre todo, poco tiempo. De pronto recordé con espanto que había compilado una versión de evaluación en lugar de la versión definitiva, que es la que pensaba cobrar.
Con apuro, volví hasta mi viejo pentium 100 y lo encendí. Comprendí que cuando terminara de iniciar el sistema operativo debía llamar al IDE de Delphi y esperar que se cargue completo sólo ingresar el condicional de compilación y luego elegir la opción “Build All”. ¿No habría una forma rápida de compilar sin tener que pasar por IDE? ¿Dejaba el coche en marcha?
Si hay algo que me molesta es perder el tiempo, así que mientras iniciaba Windows 2000 sobre el pentium 100 tuve tiempo de leer hasta el capítulo 23 de un libro acerca del compilador. ¡Sorpresa! Había algo que desconocía, el compilador de Delphi también está presente en un ejecutable aparte que puede utilizarse independientemente del IDE. El archivo es dcc32.exe y puede ejecutarlo desde la línea de comandos. Recuerde que el compilador del IDE es el mismo que el de la línea de comandos, aunque no se trata del mismo ejecutable.
Seguramente ya sabe que la sigla “IDE” significa Interactive Development Environment o Entorno de desarrollo interactivo. Para cualquier programador que haya experimentado con lenguajes anteriores, que no poseían un entorno integrado, la llegada de estos entornos interactivos, visuales e integrados fue un gran alivio, aunque sólo sea por el hecho de no tener que “salir” del editor de código para ejecutar el compilador (un programa aparte).
Podemos decir entonces que el desarrollo interactivo es grandioso: diseño visual, persistencia, modelado, etc. Sin embargo, hay veces que necesitamos sólo simpleza y velocidad, sobre todo si tenemos el coche en marcha. Volvamos a la línea de comandos.
Para controlar el compilador, deberá suministrarle a dcc32.exe los parámetros necesarios en la forma adecuada. No haremos una descripción detallada de todos los parámetros que soporta, puede encontrarla llamando al ejecutable sin parámetro alguno.
Dcc32.exe lee antes de compilar todas las opciones ingresadas a través de los parámetros, de esa manera puede suministrarle opciones y nombres de archivo en cualquier orden. Los nombres de archivo pueden ser unidades, proyectos, paquetes, etc. De la misma manera que en el IDE, podrá compilar sólo unidades2 o proyectos enteros. Si omite la extensión del archivo, Dcc32.exe intentará con .pas, luego con .dpr. Para compilar un paquete deberá suministrarle explícitamente la extensión .dpk.
El compilador tomará sus “opciones” de cuatro fuentes distintas:

  • El archivo de configuración global
  • El archivo de configuración local
  • El archivo de configuración del proyecto
  • Las directivas suministradas a través de parámetros a dcc32.exe

Dcc32.exe tomará las opciones de estos cuatro lugares, en ese orden. De esa manera, puede sobrescribir las opciones: las últimas prevalecerán sobre las anteriores.
El archivo de configuración global dcc32.cfg reside en el directorio Bin de Delphi. Puede modificar este archivo para almacenar opciones que se apliquen a todos sus proyectos. El archivo de configuración local es dcc32.cfg y puede residir en el directorio del proyecto en cuestión. Su ámbito serán todos los proyectos almacenados en esa carpeta. El archivo de configuración del proyecto tiene el mismo nombre que el proyecto pero con extensión .cfg ya he comentado que el IDE de Delphi crea automáticamente este archivo cada vez que guarda su proyecto.
Por último, las directivas suministradas a través de parámetros a dcc32.exe nos permitirán compilar dos versiones distintas a través de dos archivos de procesos por lotes. Los viejos y conocidos .BAT
Creé dos archivos .bat. Dentro del primero, que llamé compila_demo.bat, coloqué:

dcc32.exe Project1.dpr /dVERSION_DEMO /b

dentro del segundo, que llamé compila_full.bat, coloqué:

dcc32.exe Project1.dpr /dVERSION_FULL /b

Note que -d (ó /d) es un parámetro utilizado para indicarle a dcc32.exe que lo que viene a continuación es una directiva del tipo {$DEFINE}, de esa manera puede definir un símbolo global a través de un archivo .bat sin abrir siquiera el IDE. En este archivo de proceso por lotes no necesité suministrar el path del proyecto, que está situado en la misma carpeta que el archivo .bat. A través de este método puede activar o definir más de un símbolo, sólo es necesario que los separe con punto y coma.
Ahora sí, dejé dos accesos directos, cada uno apuntando a los distintos archivos de procesos por lotes. Haciendo doble clic, podría compilar –sin tener que abrir el IDE- la versión definitva, o la versión de evaluación. No es cosa de andar perdiendo el tiempo.

NOTAS

Los manos de El Eternauta

El eternauta es la obra cumbre del comic argentino. La frase pertenece a la canción que los extraterrestres llamados « manos » cantaban cuando comenzaban a morirse, luego de haber sentido miedo por primera vez.

Compilar sólo una unidad .pas

Para compilar sólo una unidad desde el IDE es necesario abrirla como proyecto, es decir mediante la opción Open project del menú File.