… o el padre de todos los bugs (de momento).
Una aclaración (añadido posterior)
Por la serie de comentarios recibidos pudiera parecer que mi aplicativo no está funcionando correctamente, y lo cierto es que tras releer detenidamente la entrada completa me he dado cuenta de que efectivamente así lo parece. Quizás haya pecado de ser demasiado obtuso y/o abstruso a la hora de contar el tema, pero la aplicación YA ESTÁ FUNCIONANDO correctamente sin ningún problema. Diciéndolo en pocas palabras, yo estaba cometiendo un montón de errores en los constructores, errores que generaban excepciones que no se lanzaban donde debían, con el apoteósico final del tema del Orcas. Y todo el comentario viene por eso: parece ser que la máquina virtual .NET no es capaz de lanzar bien las excepciones en esos lugares, y nada más, cosa que al final he terminado confirmando al compilarlo con el Orcas, me ha saltado la excepción que faltaba en el lugar exacto, he visto el fallo cometido por mi, y santas pascuas. 🙂
Y ahora el post original:
Voy a intentar explicar mi última aventura dentro del C++/CLI. Y digo última porque abandono el lenguaje y de momento siempre que pueda haré las cosas en C#, y cuando necesite interop IJW, lo meteré todo en un ensamblado hecho en C++/CLI y lo usaré desde C#. Espero así tener menos problemas, que el IDE se pueda manejar sin que se arrastre mucho, y creo que dejaré de padecer problemas como el que voy a contar ahora. Si el C# fuera tan buggy como el C++/CLI, lo más seguro es que abandone el .NET definitivamente para mis proyectos profesionales, aunque todavía no lo tengo decidido del todo.
La aplicación
Mi último proyecto es una aplicación que irá en un quiosco, completamente autónoma. Aunque no es muy grande, sí que es compleja, pues controla varios dispositivos que yo llamo exóticos (aunque para mí un dispositivo exótico sea un servidor de bases de datos) a través de USB y de canales serie, emplea sockets para conectarse al mundo exterior, y procesa unos 100 megas en elementos gráficos y sonidos.
Este tipo de desarrollos requieren variables globales, pero por desgracia el .NET no las permite, y realmente no entiendo por qué, puesto que tampoco es un entorno Orientado a Objetos puro, y menos aún con las novedades que trae el C# 3.0. Por lo tanto tengo que utilizar clases globales estáticas para realizar el mismo trabajo, pero este tipo de clases tienen una contrapartida, y es que no sabes cuándo se van a instanciar realmente, y en teoría deberían estarlo justo antes de entrar en main, aunque el C++/CLI retarda a veces dicha creación hasta que se llama a cualquier método de la misma, y entonces ejecuta el constructor estático, cosa que para un sistema en tiempo real es inaceptable, por lo que al final lo he tenido que agrupar todo en una clase global que he llamado, ya lo habrán adivinado, Globals. Dentro de esta clase instancio objetos miembro estáticos, que llamo en tiempo de ejecución mediante propiedades:
if(Globals::Devices->InPowerFail) …
Otros detalles de implementación
Una de las cosas que hace la aplicación es consultar periódicamente si tiene actualizaciones para bajárselas e instalarlas, al más puro estilo Windows Update. Todo ese código está en una pareja de ficheros llamada UpgradeEngine.h/UpgradeEngine.cpp. Ahí dentro hay clases para consultar el estado del sistema remoto, para bajarse lo que quiera que tenga que bajarse y para realizar la descompresión y la pertinente instalación, todo ello cuelga de un método global que llamo dentro de main antes de instanciar la ficha principal. La implementación del método global está al final del fichero, y todas las clases por encima de él, y este método global abre un fichero local, mira si le toca y sólo entonces instancia los objetos pertinentes.
Hay muchas más clases que son globales y estáticas, algunas de ellas crean hilos que van a monitorizar los periféricos, que deben estar disponibles lo antes posible para evitar efectos indeseados, otras simplemente precargan parte de los gráficos y de los sonidos, y otras son meros elementos de soporte.
El primer problema
Todos los programadores somos humanos y nos equivocamos. Partiendo de ese principio, y teniendo en cuenta que aproximadamente el 20% de la aplicación está en los constructores globales estáticos, vamos a tener un 20% del total de los bugs en dichos constructores. Y esos bugs, a veces, van a lanzar excepciones, excepciones que deberían ser manejadas primero por un controlador local si lo hubiera y luego por uno global, terminando en todo caso en el de la máquina virtual.
Eso si no hay bugs dentro de esos controladores. Porque si los hay, podría ocurrirte lo que a mi. Haces una pifia, lanzas la aplicación y ¡tachán!, excepción al canto. Bien, en la primera línea del primer método que haya en UpgradeEngine.cpp. Pero si esa clase ni siquiera se está instanciando ahora, y ahí no he tocado nada, y ni siquiera se ha llamado al código que decide si se instancia o no esa clase (recordemos, estamos en un constructor estático global, antes de entrar en main).
Miras el código que has modificado y descubres la pifia, pero te sigue rondando por la cabeza el hecho de que no se haya disparado ahí la excepción. Pero sigues picando código, y vuelves a cagarla. Nueva excepción, que termina en el mismo sitio que antes, excepción generada en otro constructor estático en la otra punta de la aplicación. La cosa ya te mosquea un poco.
Cambias el orden de las clases en UpgradeEngine.cpp, y resulta que las excepciones se siguen capturando en la primera línea del primer método de la primera clase que haya en dicho fichero, se produzcan en el constructor estático que sea. Y entonces empieza a subir la presión sanguínea.
Ya puestos a probar cosas, haces una construcción en release que sabes va a lanzar una excepción en algún lugar, y si ejecutas desde el IDE el comportamiento es el mismo, pero si lanzas la aplicación por sí sola, peta la máquina virtual entera y se cierra todo. No es que te diga que se ha generado la excepción tal o cual, no, sino que lo que se rompe es la máquina virtual completa, el programa nativo que es el propio .NET.
Y entonces decides visitar ese gimnasio que pagas pero que casi nunca usas, y que ahorita mismo te viene al pelo.
El segundo problema
Ya repuesto de lo anterior, estás terminando el aplicativo, sólo te quedan un par de cosas y que te lo revisen y te saquen bugs. Estás contento con el trabajo bien hecho. Te traen el quiosco para que metas tu aplicación en él.
Y comienza el baile de nuevo.
La máquina virtual se cae sola. En tu PC todo va bien, en la placa final ni siquiera obtienes excepciones, sino que la propia VM .NET se cae miserablemente. Bueno, son cosas que pasan. Intentas depurar de forma remota y no puedes porque el ordenador remoto es un XP Home… Los compis que están haciendo la integración en la cadena de montaje también te vienen con pegas, en algunas instalaciones (todas ellas clonadas), la VM salta con excepciones, en otras simplemente se cae. Me traen una que genera casi siempre excepciones y las excepciones tienen el mismo sentido que cuando me pasaba lo que he explicado en el punto anterior, o sea, ninguno. Y de diez máquinas idénticas en pruebas, sólo dos generan excepciones, y las ocho restantes simplemente se caen. Y no son problemas de hardware, porque intercambias componentes y no tienen relación alguna con los fallos…
El intermedio
Hora del kitkat. O de mandarlo todo a tomar por c*l*. Se ha bajado la máquina virtual del Orcas, y tras los problemas con la clave comentados en otro post, me pongo a jugar con él. Una de las pruebas es recompilar mi proyecto C++/CLI actual con este entorno, que por cierto es idéntico al Visual Studio 2005.
Lanzo la aplicación y… y… y… ¡Joder, una excepción en pleno código del UpgradeEngine.cpp! ¡Pero no en la primera línea del primer método de la primera clase, si no en otro sitio! Y hay una traza a un fichero que ni existe ni tiene una ruta válida si no es en mi máquina de desarollo, y ocurre siempre que la máquina NO tenga que actualizar…
¡Alabados sean los chicos-chapuza de Microsoft!
Las preguntas sin resolver
Está claro que el problema estaba en esa línea, pero el asunto no queda lo suficientemente explicado, y aunque en un principio la culpa sea mía por olvidar quitar esa traza, el compilador del Visual Studio 2005 (versión 14.00.50727.762) debería haberse comportado como el del Orcas (versión 15.00.11209).
Otra pregunta sin resolver es por qué no saltaba la excepción en esa línea en mi placa de pruebas (diferente de todas la demás, es una VIA EPOC de esas tiny).
Otra más es por qué en release la máquina virtual .NET se cae entera.
Otra es por qué en ciertas máquinas daba una excepción sin sentido, en otras no y en otras más se caía la VM siendo estas máquinas completamente idénticas.
Más: ¿Por qué cuando hacía una pifia en otro lugar, en lugar de saltar la excepción en ese lugar y con el tipo concreto, saltaba la otra y encima sin sentido alguno?
Posible explicación
Voy a intentar dar una explicación del comportamiento observado, ya que explicar todos los pasos dados antes de compilar la aplicación con el Orcas sería demasiado largo, y encima está el hecho de que al poner trazas interfería en la secuencia, pero creo que mis conclusiones son correctas.
Una aplicación .NET tiene al menos 8 hilos aunque nosotros no creemos ninguno. Supongo que esos hilos están ahí para el recolector de basura, el precompilador, el scheduler de código, el optimizador, el intérprete de la máquina virtual y otros menesteres similares.
Por lo tanto, mientras se está ejecutando código por un lado, seguro que por el otro se está precompilando, optimizando o simplemente realizando otras tareas de mantenimiento, por lo que aunque pensemos en una máquina de von Newman secuencial, realmente no es así (bueno, sí que es así, pero a efectos prácticos no).
Imagino el siguiente escenario: por un lado un hilo está ejecutando los constructores estáticos, y por el otro está preparando el entorno para entrar en main, por un lado se produce una excepción en un constructor estático y por el otro en el código con la traza mal puesta. Entran en acción los manejadores de excepciones, mientras uno busca un manejador, el otro también lo hace, o es entonces cuando mientras una parte de la vm está buscando un manejador la otra ejectua el código que genera una nueva excepción, de forma que se arma la picha un lío y sale por donde puede.
Como en debug hay más código oculto que en release, los tiempos son diferentes, así como las comprobaciones, y dado que la máquina virtual .NET es algo vivo, en determinadas condiciones se produce ciertas secuencias que llevan a su total caída, en otras a salir por donde puede, y finalmente en alguna más a poder lanzar una excepción más o menos válida.
Creo no equivocarme, pero simplemente toda esta explicación podría ser una mera paja mental, así que lo único que puedo afirmar y reafirmar es que
Sigo pensando que hay algo podrido dentro de la máquina virtual .NET