Introducción al depurador de Delphi 6. ¿Para qué sirve?

No es ninguna novedad que la palabra “bug” significa bicho en inglés. Además de no ser ninguna novedad tampoco tiene mucha importancia, pero siempre queda bien comenzar un artículo con apostillas históricas. Allá vamos.
Allá por los años 40 del siglo pasado (sí, del siglo pasado), cuando los circuitos integrados no eran aún ni ciencia ficción, las computadoras utilizaban bulbos, lámparas, de ésas que generaban luz y calor, atrayendo a los insectos. Cuenta la leyenda que una adorable señora llamada Grace Murray Hooper anotó en su libreta la causa de un fallo de la computadora Mark II: “Relé nº70 Panel F bug en Relé”. Aunque desconocemos si se trataba de polilla o araña, lo cierto es que había un bug en el programa.

 

 

La adorable señora (la viejecita de la foto, sí) pasó a la posteridad, pero no por andar matando bichitos sino por ser la precursora del lenguaje COBOL. Además, es posible que no la vea de manera tan adorable si le cuento que se trataba de una almirante de la Marina Estadounidense y que la Mark I se utilizaba en ese entonces para calcular el ángulo en el que debían apuntarse los nuevos cañones de la Marina Estadounidense.
Lo cierto es que los bulbos han quedado en la prehistoria siendo hoy reemplazados por lo que conocemos como circuitos integrados. El término bug ha perdurado y hoy se sigue empleando para indicar problemas en circuitos internos y en archivos. Claro que la traducción desbichar no hubiera quedado elegante, así que nos referiremos al Debugger como depurador, el depurador de Delphi 6.
¿Acaso tiene sentido hoy un artículo sobre el depurador de Delphi 6? Buena pregunta.
Podría decirse que la política de Borland respecto a actualizar a Delphi .NET está jugando de manera decisiva respecto a este tema. Actualmente son muy pocas las empresas que estan desarrollando en Delphi.NET que no sea para “migrar” sus anteriores desarrollos al nuevo paradigma del señor de gafas. Pero es difícil asegurar que, para un nuevo desarrollo, Delphi.NET sea indudablemente la mejor elección.
A a ello puede sumarse la realidad del parque de Hardware de algunos países donde las computadoras de última generación son una muy difícil inversión, por ello a Borland le está costando mucho convencer a sus clientes que programen para .NET ya que siempre es mucho más difícil vender software que obligue a cambios de hardware. ¿Hay Win32 para rato?.

El depurador integrado

Las personas que deciden casarse y los desarrolladores de software saben que no siempre las cosas salen como uno espera. Haya leído o no en la entrega anterior de Síntesis el artículo sobre el compilador, le resultará obvio que no es suficiente con que el código sea compilado sin errores. Un código compilado puede contener errores mucho mas difíciles de encontrar. La tarea puede resultar ardua y frustrante.
El depurador de Delphi puede servirnos para interceptar excepciones, colocar puntos de ruptura, evaluar expresiones e inspeccionar el código fuente compilado, entre otras cosas. Se dice que el depurador está integrado porque está disponible cuando trabajamos con el código fuente dentro del entorno de desarrollo.
Por defecto, el depurador queda activado cuando se instala Delphi. Si desea desactivarlo puede desmarcar la casilla de verificación Integrated debugging en la la pestaña General de la ventana Debugger Options.

 

Si lo tiene habilitado, puede verlo en acción cuando una excepción ocurre o es elevada1 en un programa ejecutado desde el entorno de desarrollo. Lo primero que verá será un mensaje avisándole sobre la excepción (como se ve en la figura 1). Luego el depurador integrado entra en acción permitiéndole acceder a todas sus prestaciones, que intentaré detallar a continuación.

 

Tooltip Expression Evaluation

 

No existe una traducción única para “Tooltip Expression Evaluation” pero podemos decir que hace referencia a esa herramienta que sirve para mostrar en tiempo de depuración el valor de de una expresión en el código fuente en el momento en el que la ejecución se detiene en un punto de ruptura. Para verlo en acción puede colocar el cursor del ratón sobre una expresión con el depurador cargado y la ejecución detenida. Podrá ver en una ventana emergente, similar a una pista (Hint) el valor de la expresión, como se ve en la figura 2.
En el caso de que el valor de la expresión no esté disponible por las optimizaciones que realiza el compilador, la ventanita dirá algo como Inaccesible value.
Esta herramienta se puede habilitar/deshabilitar desde la ventana de configuración del editor (Tools | Editor Options | Code Insight).

La pila de llamadas

No es nada raro que durante la ejecución de un programa desde el IDE de Delphi nos encontremos con una excepción inesperada. Posiblemente una excepción que nosotros mismos hemos escrito, pero que no esperábamos encontrar “en este momento”. ¿Qué ha pasado? ¿Y a tí quién te ha llamado?
Atrás quedaron los días de la programación funcional que tanto juego hacía con D.O.S. y su mecánica unidireccional. Las famosas ventanitas que el señor Gates registró como suyas se apoyan en la programación orientada a eventos. Y la VCL de Delphi es -podríamos decir- puro evento3.
Luego del error cometido en las figuras 1 y 2 comprendí que la tabla Animals.DB que viene con Delphi no contiene un campo llamado Edad y que a veces es mejor utilizar campos estáticos. A continuación creé todos los objetos TField de dicha tabla en tiempo de diseño y me propuse validar el ingreso de datos. Como sé que las los nombres de animales no pueden comenzar con números, escribí rápidamente una función que me diga si la cadena pasada como parámetro lo hace.

function TForm1.ComienzaConDigitos(Str: String): Boolean;
begin
Result := Str[1] in [‘0’..’9′] ;
end;

Una maravilla de la programación, vea. Esta función es llamada desde el evento OnValidate del campo NAME de la tabla Animals.

procedure TForm1.tbAnimalsNAMEValidate(Sender: TField);
begin
if ComienzaConDigitos(Sender.AsString) then
Raise Exception.Create(‘Los nombres no pueden comenzar con dígitos’) ;
end;

Probé mi maravilla desde un TDBGrid asociado a la tabla y funcionó. Comprobé que funcionaba como yo esperaba, al primer intento de ingresar algo como “5 Ornitorrincos” obtuve la excepción esperada y tomé la foto que puede ver en la figura 3. ¡Perfecto!, todo funciona como se espera. Luego escribí la función que ingresa datos a la tabla desde otra Tabla y me propuse utilizarla. Allí comenzaron los problemas (Figura 4).
El fantasma del Access Violation, que recorre mi código fuente, apareció sin avisar. Inmediatamente se cargó en memoria el depurador y la ejecución se detuvo en la línea

Result := Str[1] in [‘0’..’9′] ;

La causa era obvia, el torpe que escribió ese código no previó que una cadena puede estar vacía, y Str[1]  apunta -en ese caso- a… ¿ »hacia dónde » apunta?. Como sea, lo que se llama una violación de acceso. Como en general no me doy cuenta de lo obvio, eché mano de la ventana de la pila de llamadas (Call Stack). Puede hacerla visible desde el menú View | Debug Windows | Call Stack. Con la ejecución detenida pude ver qué fué lo que pasó: La función que ingresaba datos desde otra tabla intentó copiar un registro donde el campo NAME no estaba inicializado, la temible cadena nula que mi función no esperaba.
La ventana Call Stack le permite inspeccionar la cadena de rutinas y subrutinas que resultan en la ejecución de la línea de código donde el depurador se haya detenido. En condiciones normales encontrará en esta ventana sólo las rutinas definidas en su aplicación para las cuales haya definido que la información de depuración (debug symbol information en la pestaña Compiler de la ventana Project Options) esté disponible.

 

En la figura 5 puede ver que a la función ComienzaConDigitos() se le ha pasado una cadena nula. Esta ha sido llamada desde el manejador del evento denominado tbAnimalsNAMEValidate y éste a su vez por el procedimiento CopiarNombresATabla. ¿Y quién ha llamado a CopiarNombresATabla? Pues un maldito botón: al fin tenemos a quién echarle la culpa de todo.
Le comentaba que en Windows la programación es bidireccional. Si ha comenzado a programar con herramientas como Delphi es posible que pase por alto que lo que llamamos “la VCL” es fruto del trabajo de la gente de Borland, que ha encapsulado las llamadas al API Win32 en clases. Por un lado existen llamadas al API para darle órdenes al OS. Por otro lado, el OS envía mensajes que la VCL debe saber “escuchar” para luego traducirlas en -por ejemplo- eventos, eso significa que la cadena de llamadas es mucho más larga que lo que se ve en la figura 5, para ver la cadena completa necesita utilizar una versión de la librería VCL compilada con información de depuración (debug information). Vamos a ello.
Puede ejecutar la ventana de diálogo Project Options desde el menú principal en Project | Options, allí habilite la casilla de verificación Use Debug DCUs en la pestaña Compiler. Ahora puede tener más información acerca del mismo punto en el código fuente, asegúrese de haber configurado además el valor Search Path en la pestaña Directories/Conditionals de la misma ventana. Dicho valor especifica la ubicación de los archivos de su código fuente. El valor por defecto para el Search Path de cada proyecto es $(DELPHI)\Lib\Debug, que a su vez es tomado del valor que figure en Debug DCU Path en la pestaña General de la ventana Debugger Options.
Lo que veíamos en la figura 5 era -por decirlo de alguna manera- sólo código propio. Su código. Ahora, con más información de depuración puede ver que la cadena de llamadas es mucho mas larga (figura 6), que el procedimiento BitBtn1Click() (el manejador del evento OnClick del botón de marras) fue disparado por el método Click de una clase llamada TControl (un ancestro de nuestro botón) y que la dicha clase tiene un procedimiento de ventana WndProc() que es el encargado de “escuchar” los mensajes recibidos. En realidad el primer enterado del asunto ha sido el objeto Application, que dentro de su método Run() llama a HandleMessage(). Puede utilizar el menú contextual de la ventana de la pila de llamadas para inspeccionar directamente el código del método seleccionado eligiendo View Source. El IDE se posicionará directamente sobre el código en cuestión, trátese del código de los señores de Borland (la VCL), del suyo propio o de cualquier biblioteca de terceros.

Hablando de la famosa VCL, si desde la ventana de la pila de llamadas inspecciona su ejecutable con la información de depuración denominada “debug DCUs”, o simplemente echa un vistazo a la figura 6, podrá interiorizarse de la forma y el orden en que todas las llamadas fueron hechas. Para este ejemplo, al hacer clic en un control de la VCL y debido al propio diseño de la VCL, la pila de llamadas va recorriendo los métodos de los antecesores del control, de abajo hacia arriba. En algunos casos se tratará de métodos sobreescritos (overriding) en la jerarquía de las clases: puede ver que -con un sólo clic del usuario- hay varias llamadas al método Click pero de diferentes clases.
Esto es lo que llamamos la pila de llamadas. A diferencia de lo que ocurre con los gobiernos, aquí sí podemos investigar hasta las últimas consecuencias hasta saber quién originó todo. Para el caso que nos ocupa, recuerde que el culpable de todo no es el objeto Application ni el Botón, sino el torpe que escribió:

Result := Str[1] in [‘0’..’9′] ;

La ventana Watch List

Si me pide que le traduzca semejante cosa le diría algo como “Ventana de inspección de expresiones”  digamos que sirve para inspeccionar -siempre en modo depuración o ejecución detenida- una lista de valores, que pueden ser variables o expresiones. Puede ponerla en acción desde el menú  View | Debug Windows | Watch List. Para utilizarla he reemplazado la función ComienzaConDigitos() por un procedimiento denominado VerificaNombre(). Por un lado ahora podremos verla (watch) en ejecución, por otro, obtenemos un ejemplo menos estúpido que el anterior. Además es el mismo procedimiento el que eleva la excepción cuando no se verifica la condición necesaria. Repasando, desde el evento OnValidate se llama a VerificaNombre().

procedure TForm1.tbAnimalsNAMEValidate(Sender: TField);
begin
VerificaNombre(Sender.AsString) ;
end;

Y VerificaNombre() se encarga de elevar una excepción si el nombre contiene un dígito:

procedure TForm1.VerificaNombre(Str: String);
var I : Integer ;
HayDigito : Boolean ;
begin

HayDigito := False ;
if Str <>  » then
for I := Length(S) downto 1 do
HayDigito := HayDigito or (S[I] in [‘0’..’9′]);

If HayDigito then
Raise Exception.Create(‘Un nombre no puede contener dígitos’) ;

end;

Luego de asegurarme de tener habilitado el depurador puse el cursor sobre la primera línea del método y presioné F4, que es lo mismo que elegir Run | Run to Cursor en el menú, algo así como “Ejecutar hasta esta línea donde he situado el cursor”. Luego ejecuté el programa de marras hasta que llegó el momento de grabar el registro en la tablita de los animalitos. Allí el programa mismo fue tomado del cuello y tirado hacia atrás: el depurador quería aparecer en pantalla. Apareció, obviamente, con la ejecución detenida en la línea de código elegida.

Aquí puede verse la utilidad de la ventana Watch List. Puede ir formando la lista de expresiones que quiere vigilar paso a paso mediante la opción Add Watch del menú contextual de dicha ventana. No hay límite para la cantidad de expresiones a ingresar, lo cual es una ventaja indudable sobre el precitado Tooltip Expression Evaluation y el aún no citado comando Evaluate/Modify. En la figura 7 pueden verse los valores de las expresiones vigiladas. Observará que antes de entrar la ejecución en el bucle for, la variable I aún no tiene valor asignado, esto es obra del optimizador del compilador.

Archivo de registro de eventos (Event Log)

Existen ciertas ocasiones en las que no deseamos recorrer todo el programa paso a paso, imagínese un bucle for de miles de elementos. Avanzar una a una miles de líneas de código para encontrar un problema o estudiar un caso puede ocasionar caída del cabello, vejez prematura o divorcios. Tenga en cuenta que tiene una alternativa que consiste en decirle al depurador que siga al programa sin perderlo de vista y que nos entregue un informe detallado a su vuelta. ¿Por qué líneas de código anduvo? ¿Con quién se encontró? ¿Qué Dll’s cargó? ¿Cómo fueron modificándose esas variables? ¿Cuánto valían esas variables antes de cargar determinada Dll?
Para ver ese informe debe seleccionar View|Debug Windows|Event Log en el menú. Le aparecerá la ventana que contiene los mensajes de control. Estos se dividen en 5 categorías detalladas mas abajo. Utilizando su menú contextual puede limpiar el contenido (clear events), guardar a un archivo de texto los mensajes, agregar comentarios a los mensajes9, y configurar las propiedades de la ventana (properties).
Al configurar las propiedades de la ventana puede elegir qué tipo de mensajes su agregan al “log”:

  • Breakpoint messages: Si se habilita, el depurador agregará un mensaje al registro (log) cada vez que se encuentre un punto de ruptura o una excepción sea elevada. El mensaje incluye la dirección de memoria del punto del programa que está siendo depurado donde se ha colocado el punto de ruptura, además de información adicional sobre éste, como la unidad donde se encuentra, número de línea, condición, etc.
  • Process messages:  Si se habilita, el depurador agregará un mensaje al registro (log) cada vez que un módulo (dll, bpl) se cargue o descargue de memoria por el proceso principal.
  • Thread messages: Si se habilita, el depurador agregará un mensaje al registro o “log” cada vez  que un hilo de ejecución sea creado o destruido mientras dure el proceso principal10.
  • Output messages: Si se habilita, el depurador agregará un mensaje al registro o “log” cada vez  que su programa o uno de sus módulos haga una llamada a OutputDebugString.
  • Window messages: Si se habilita, el depurador agregará un mensaje al registro o “log” cada vez  que un mensaje de Windows sea emitido o recibido por el programa (o una de sus ventanas11) que está siendo depurado. La entrada en el registro o “log” contendrá información acerca del mensaje, como su nombre y sus parámetros. Tenga en cuenta que estos mensajes no serán escritos al log inmediatamente si su programa se encuentra en ejecución. Ni bien detenga el proceso en el depurador los mensajes serán escritos al log.

 

A veces los bucles son largos, o están dentro de otros, y la búsqueda se hace tediosa ¿verdad?. ¿Ha sentido la necesidad de escribir algo de código para agregar datos a un TMemo durante la ejecución y evaluarlos posteriormente? Le sugiero consultar en la ayuda la función del API OutputDebugString(), con ella puede -desde el código fuente- enviar mensajes al depurador y pasarle una cadena como parámetro. En dicha cadena puede introducir los valores que desee evaluar luego, desde la ventana “Event log”. Sólo debe asegurarse de agregar Windows.pas en la cláusula uses y luego simplemente escribir algo como:

S1 := ‘La Variable X1 vale: ‘ + IntToStr(X1) ;
Windows.OutputDebugString(PChar(S1)) ;

Donde S1 es de tipo String y X1 -obviamente- un entero. Ocurre que OutputDebugString() recibe un parámetro de tipo PChar así que deberá hacer la conversión desde String para poder utilizarla.

Conclusión

Siempre queda bien colocar la palabra conclusión al final, aunque no haya ninguna posible, como en este caso. No se puede esperar mucho de una introducción tan básica. Recuerde que aún nos quedan por recorrer los puntos de ruptura, además de responder la pregunta inicial: ¿Para qué sirve un depurador?. Una respuesta simple sería “Para encontrar y eliminar bugs” Pero eso nos lleva a otra pregunta: ¿Qué es un bug?
Los bichos aún están ahí, y hay quien dice que nos van a sobrevivir.