Sobre delegados, closures, punteros y la falacia del puntero loco

Leía esta mañana –mientras se carga una serie de gráficos en mi actual proyecto- el blog de Marino Posadas que, aunque publica poco, publica bien, y me he quedado más que estupefacto con lo que nos ha contado. He de matizar que en ningún momento dudo de la palabra de Marino, y es por eso por lo que me ha llamado tanto la atención de lo que nos ha contado.

Antes de seguir aquí lo mejor es que leáis su entrada, y volváis a esta.

***

El primer malentendido sobre el texto nos lo ha solucionado el propio Marino en los comentarios: no, Anders no se tiró seis meses para inventar los delegados, sino que estuvo tal tiempo creando el .NET, y durante el cual salió la idea del delegado. Haremos la vista gorda e ignoraremos el hecho de que en Delphi (y C++ Builder) el propio Anders ya tenía algo así.

Además, siempre he dicho que el delegado es el paso siguiente al puntero a función y/o función de retrollamada (o callback, como le se suele citar habitualmente). Eso también queda fuera de toda discusión.

Cuenta Marino que Anders primero analizó que una mayoría de las BSOD en Windows venían a cuenta de drivers defectuosos, y que otra mayoría venía causada por una variación sobre un mismo tema: el moldeo o casting incorrecto de punteros a función, algo que es bastante común en el desarrollo de sistemas.

Por lo tanto, para acabar con el segundo problema, decidió implementar los delegados en .NET. Aquí tenemos la primera falacia de todas, y encima autocontenida. Es decir, los delegados están en .NET, pero no en la creación de drivers, ni en la de software de sistemas, que se sigue haciendo en C++, así que solución, nada de nada.

Claro, a no ser que pensara que el mundo™ iba a abandonar sus millones de líneas de código™ y se iba a pasar derechito al .NET™, acabando en un par de meses con el infierno de los punteros.

Espero que ya se haya dado cuenta de que no, que el .NET está y sirve para lo que está y sirve, y para nada más. Por lo tanto, está claro que no ha acabado con el problema de los punteros locos.

Lo que ha hecho ha sido crear una plataforma de desarrollo en la que se minimiza el problema, pero el código nativo (nuevo y viejo) sigue adoleciendo del mismo. Además, en .NET un delegado vacío, hasta donde llego, lanza una excepción, de igual forma que la ejecución de un puntero a función nulo lanza, también, una excepción, que si eres buen programador, habrás capturado.

***

Vamos ahora a por la segunda falacia. ¿Sabéis qué es en realidad un delegado? Pues un delegado es una clase normal y corriente, pero que está cableada en cierta medida dentro del compilador de C#. De hecho, en C++/CLI podemos usarla como una clase más. Podéis leer algo sobre esto aquí.

¿Y qué hace esa clase? Pues tiene un constructor que viene a recibir un puntero a método o función global. Tiene un método que es el que ejecutará ese puntero recibido. Y tiene una serie de eventos y sobrecarga de operadores que nos permiten la semántica de utilizar “+=” y “-=” para añadir/quitar punteros a función.

Y cuando mandamos ejecutar el delegado, lo que hace es comprobar si el puntero es válido, y si lo es, lo ejecuta. Y si se trata de un delegado multicast, entonces contiene una bolsa de punteros a función que va comprobando que sean válidos y ejecutando uno detrás de otro.

Ni más ni menos.

***

Ahora vamos a lo que vamos, que es la tercera falacia y la más importante de todas. ¿Sabíais que con C++ hacer eso mismo es casi trivial? ¿Sabíais que ya está hecho en Boost (no lo he mirado, pero me apuesto un gallifante a que los hay, y seguro que de varios tipos)? ¿Realmente quería Anders acabar con el tema de los problemas con las funciones de retrollamada? No lo creo, ya que si eso realmente hubiera estado en su agenda, lo que se habría hecho es modificar Win32 para que aceptara un objeto de tipo delegado creado con alguna clase de C++, con un operador puntero para C.

Porque niños, hacer eso mismo en C++ no sólo es trivial, sino mucho más directo, ya que podemos sobrecargar el operador de llamada a función y el de puntero, como nos lo demuestran los predicados de la STL… Pero claro, Anders tiene que barrer para casa y expresar las genialidades de .NET y C#…

En fin…

Hemos leído: Coders at Work

Curioso, cuando menos, este libro, ya que recoge 15 entrevistas hechas a otros tantos próceres del desarrollo, algunos más conocidos que otros. Peter Seibel es el entrevistador, y a partir de un par de preguntas iguales, las conversaciones derivan hacia los temas más variados que puedas imaginarte, todos ellos, claro está, en relación al desarrollo.

Cada entrevista trae una breve reseña biográfica de quién es el entrevistado, e indefectiblemente comienza con la misma pregunta: “¿Cómo aprendiste a programar?”. Otra que también suele hacer a lo largo de la charla es el uso que le da a los libros de Knuth “El arte de programar ordenadores”. Aparte de esos dos puntos comunes, las conversaciones suelen variar ampliamente, pero siempre alrededor del trabajo de cada uno de ellos, cómo contratan y qué tal les va ahora.

Una de las cosas que menos me han gustado del libro es el hecho de que todos los entrevistados son dinosaurios del desarrollo, que empezaron en esto allá por los años cincuenta/sesenta, y eso se nota mucho en las conversaciones. Casi todos empezaron o bien robando tiempo a un mainframe de la época o bien haciendo programas para ellos (hablamos de la época de los teletipos e impresoras, e incluso del uso de tarjetas perforadas).

Luego se va adelantando en el tiempo y se van comentando los logros de cada uno, hasta el punto de encontrarnos opiniones tan dispares y tan políticamente incorrectas hoy en día como el rechazo hacia las pruebas unitarias o a la depuración organizada, e incluso a la programación orientada a objetos, sin contar un odio cerval a C y nombrar, de pasada y con malos modos, a C++.

Es decir, la mayoría de los entrevistados rechazan de plano alguno de los paradigmas más actuales del desarrollo. Evidentemente no todos lo rechazan todo, pero sí que, siempre, rechazan algo.

Yo aquí veo dos lecturas diferentes. La primera está en que, por desgracia, los entrevistados son eso: dinosaurios del desarrollo listos para la extinción. La otra interpretación, quizás más plausible, es el hecho de que la elección de personas venga de la mano del entrevistador o de quién la haya hecho, y muestre, más que otra cosa, las preferencias personales del mismo más que la realización de un muestreo dentro de los grandes del desarrollo.

Personalmente creo en la segunda, más que nada porque falta alguno de estos tres: Ritchie, Kernighan o Stroustrup. Nadie va a negar aquí que la invención de C realizó un cambio de paradigma en el desarrollo, haciendo más fácil lo difícil (y aquí pensemos que por cada PC hay mil dispositivos embebidos que, ciertamente, tienen su firmware escrito en C). Y sin embargo en el libro se denosta C y C++ (y no es porque yo sea un fan del lenguaje, de hecho odio C, pero no C++).

Por lo tanto creo que el libro, pese a ser bueno, está un poco sesgado.

No obstante resulta muy interesante de leer. Es increíble lo que estos señores saben, y su experiencia en el campo que nos ocupa. De hecho, la pregunta que más me ha gustado de todas es la de que expliquen su mayor pesadilla en depurar un bug… aunque alguno se vaya por las ramas al responder.

Ciertamente vale la pena leerlo, no para aprender a programar, sino para conocer otros puntos de vista y para leer sobre la experiencia de esa gente. Además, el libro no es muy técnico en cuanto a terminología aunque a veces hay que releer alguna frase de los entrevistados debido a las peculiaridades de cada uno.

Los entrevistados son: Jamie Zawinski, Brad Fitzpatrick, Douglas Crockford, Brendan Eich, Joshua Bloch, Joe Armstrong, Simon Peyton Jones, Peter Norving, Guy Steele, Dan Ingalls, L. Peter Deutsch, Ken Thompson, Fran Allen, Bernie Kosell y Donald Knuth. Deciros como palabras finales que de la lista sólo conocía a Thompson por ser el creador de Unix y al insigne Knuth.

Detectando fugas de memoria en Visual C++

A veces nos encontramos con que nuestro programa “chorrea” memoria, y hasta hace poco tiempo no había nada tan terrible para un programador como encontrar dónde se producía el problema, máxime cuando se trataba de aplicaciones con múltiples hilos.

Hubo una época en la que incluso había herramientas de terceros y analizadores de código para encontrar este tipo de problemas. Pero recientemente (creo que a partir de la versión 2005 de Visual C++), Microsoft añadió una serie de utilidades a la compilación Debug para detectar estos errores de forma casi inmediata.

Visual C++ 2005 creó la técnica, y Visual C++ 2008 la perfeccionó. Y por supuesto Visual C++ la mantiene.

Veamos un ejemplo. ¿Qué tiene de malo este código fuente tan sencillo?

class Elemento
{
    public:
    char A,B,C,D;

};

int _tmain(int argc, _TCHAR* argv[])
{
    Elemento *e=new Elemento();

    return 0;
}

Es evidente más allá de toda duda que no hemos liberado e. Este es un caso trivial, porque cuando salgamos del programa el espacio de direcciones se limpiará y no pasará nada. Pero imaginaros esa asignación en un bucle dentro de un programa destinado a estar horas y horas en ejecución.

Llega un momento en que ocupa toda la memoria disponible y revienta. Y lo peor es que el error que nos dé será tan críptico y tan extraño que, si no miramos el programa corriendo en el administrador de tareas no nos daremos cuenta. Y a veces ni así si el crecimiento del uso de la memoria es lento.

Además, en cada máquina el error será diferente, e incluso en la misma máquina dará diferentes errores dependiendo del uso del programa y del propio sistema (hablamos de excepciones extrañas con mensajes extraños e incluso mostrando texto corrupto).

Imaginaros ahora que os dan un programa de cien mil líneas que haya pasado por cinco o seis programadores, algunos de ellos malos y otros peores, que el programa tenga fugas de memoria y que te lo asignen a ti para solucionarlos.

No, no vale pegarse un tiro ni prenderle fuego al ordenador o a la empresa.

***

Es muchísimo más fácil que todo eso. Si nuestro programa está hecho en MFC tan sólo tendremos que recompilarlo con Visual Studio 2008 o siguiente, ejecutarlo un rato desde el IDE y cerrarlo.

Si no usa MFC y sólo usa Win32 u otras librerías, debemos añadir varias cosas al mismo. Abajo el nuevo código.

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

class Elemento
{
    public:
    char A,B,C,D;

};

int _tmain(int argc, _TCHAR* argv[])
{
    Elemento *e=new Elemento();

    _CrtDumpMemoryLeaks();
    return 0;
}

Hemos incluido unos ficheros cabecera y en el punto de salida hemos hecho una llamada a la función _CrtDumpMemoryLeaks(). La salida de la ventana Output (from Debug) tras una ejecución es la siguiente:

[… texto omitido …]
‘fugas_memoria.exe’: Loaded ‘C:WindowsSysWOW64msvcr100d.dll’, Symbols loaded.
Detected memory leaks!
Dumping objects –>
{107} normal block at 0x00695E30, 4 bytes long.
Data: < > 00 00 00 00
Object dump complete.
The program ‘[716] fugas_memoria.exe: Native’ has exited with code 0 (0x0).

Nos fijamos en algunas líneas que nos dicen que ha habido fugas de memoria en un bloque de 4 bytes (que se corresponden a los cuatro char que hemos indicado en la clase). Mola, ¿no?

Bueno, ya sabemos que tenemos una fuga de memoria, pero no sabemos quién la está generando. Vamos a averiguarlo.

Lo primero es fijarnos en la línea

{107} normal block at 0x00695E30, 4 bytes long

y en el número que hay entre corchetes: 107. Ese es el número de bloque de memoria que no está siendo liberado. Una vez que sabemos qué bloque es, debemos relanzar la aplicación pero esta vez en lugar de ejecutarla con F5 o con Debug -> Run, ejecutamos un solo paso para que se nos quede en el punto de entrada inicial pulsando F11 o Debug -> Step Into. También podemos poner un punto de interrupción.

Una vez hecho esto, abrimos una ventana de Watch (Inspección) y añadimos la variable

{,,msvcr100d.dll}_crtBreakAlloc

a inspeccionar. La parte “msvcr100d.dll” dependerá de la versión del compilador que estemos usando. En general será “msvcr71d.dll” para 2003, “msvcr80d.dll” para 2005 y “msvcr80d.dll” para 2008 , aparte de la expuesta para 2010.

Ahora ponemos 107 como valor, ejecutamos el programa y… ¡Nos saltará una excepción! Le damos a “break” y se nos abre un fichero de código fuente de las tripas del runtime de C/C++. Para ir a donde nos interesa, abrimos la ventana de “Call Stack” y buscamos nuestro código fuente entre la pila de llamadas.

¡Y allí estará el new o el malloc() que nunca se libera! ¡Hemos encontrado la fuga!

***

A simple vista parece algo lioso, pero os puedo asegurar que cada vez que explico esto en alguna de las empresas por las que voy se notan los suspiros de alivio de todo el departamento de desarrollo y vuelven las sonrisas.

Si nuestra aplicación está hecha en MFC todavía es más fácil porque la mitad de cosas ya vienen predefinidas (de ahí esa extraña definición de “DEBUG_NEW” que hay por los ficheros de código fuente).

Las apuestas pueden subir cuando el error es aleatorio, pero siempre será más fácil así que sin ayuda. Además, existen trucos como poner periódicamente llamadas al volcado de las fugas, aparte de que hay más opciones para la detección, algunas de ellas no documentadas porque vienen de la propia experiencia personal.

Todo esto que he explicado aquí está contado en varios lugares de la MSDN, y hay más opciones y variación, de hecho una búsqueda de “memory leak” arroja bastantes resultados.