El pan nuestro de cada día…

… 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

14 comentarios sobre “El pan nuestro de cada día…”

  1. Hasta donde yo se, según he entendido en el libro CLR via C# (que es la mejor introducción a CLR que jamás he leido), suponiendo que las propiedades estáticas las cargaras en el constructor estático, el constructor estático no es llamado antes del Main. Nada es llamado antes del Main. El constructor estático es llamado cuando intentas acceder a cualquier miembro de la clase. Esto carga todas las propiedades que sea, y ya son accesibles por el resto de la aplicación.

    Acerca del resto, no me pronuncio sin ver el fuente.

    En cualquier caso, ¡¡¡suerte!!!.

  2. Pues dime tu a mi, cómo, poniendo un breakpoint en la apertura de main, y otro en el constructor estático principal, éste se ejecuta todo antes de entrar en main… Excepto en algunas otras que efectivamente, se instancian en la primera llamada y tengo que emplear «truquitos».

  3. Sobre los constructores estáticos, las únicas garantias que tienes son:
    Que solo se llaman una vez por tipo y por dominio de aplicación.
    Que son thread safe.
    Que se llaman antes de que se invoque al primer método de la clase.

    Si estás asumiendo alguna cosa más sobre el comportamiento de un constructor estático te estas saltando el standard.

    Solo como anotación, en C++ standard los objetos globlales tambien se inicializan antes de que se ejecute el Main, lo que hace que sea posible ejecutar código antes del Main.

    Exiten una regla no escrita que debes respetar: Nunca tocar un miembro estático de otra clase desde un constructor estático. Interesante sobre este temá es: http://odetocode.com/Blogs/scott/archive/2004/07/29/360.aspx

    Creo que muy problablemente (con el 20% de la aplicación en constructores estáticos es probable que te la hayas saltado).

    Sobre los que el 20% de la aplicación este en constructores estáticos, no puedo decir más que es un mal diseño. Existen muchas alternativas mejores, la más usada utilizar singletons con inicialización explicita (típico para menejo de dispositivos, un singleton por dispositivo).

    Sobre que la máquina virtual se caiga, ¿estás seguro de que todo tu código es ‘safe’? Si tienes código unsafe, como intuyo más que probable, un fallo en tu aplicación no solo puede tirar abajo la máquina virtual de .net, sino cualquier programa en ejecución (no es probable pero es posible).

    Por último ‘el printf funciona’: asume que los bugs son tuyos, es el primer paso para solucionarlos. Es mucho más probable que haya algo prodrido en tu código que en el de la máquina virtual de .Net. Más que nada por que al resto de los programadores del mundo nos funciona aceptablemente.

    Comparto contigo que C++/CLI es dificilmente usable con comodidad por lo mal que se comporta con VS.

    Salud!!!

  4. Acerca de C/C++ con CLI, no he trabajado. Pero he verificado que lo que te he dicho es cierto: Me creo un proyecto en C# (podría ser VB, pero es que tengo buen gusto ;)). Creo una clase que tiene un miembro estático público, y en el constructor estático lo inicializo. pongo un breakpoint en la primera linea de Main y en la primera linea del constructor estático, y veo que donde empieza es en el Main. Y al intentar acceder a un miembro de la clase, es cuando se llama al constructor estático.

    Se que con C/C++ CLI la relación no es exactamente la misma. Lo que genera es código nativo que interactua con el CLR. Que no es lo mismo.

    El código que he hecho:

    using System;

    class Program
    {
    static void Main(string[] args)
    {
    Console.WriteLine(miClase.MiCampo);
    }
    }
    class miClase
    {
    private static int miCampo;
    static miClase()
    {
    miCampo = 5;
    }
    public static int MiCampo
    {
    get
    {
    return miCampo;
    }
    }
    }

    Saludos…

  5. Q buen post !!!

    Sin ganas de ofenderte, espero que la VM de Ms no tenga problemas, porque eso significaría que más de 100000 desarrolladores tendríamos muchos problemas (algunos reportamos los errores y por suerte se han ido solucionando)

    Por otra parte, soy un fanático de los patterns y también de los antipatterns (q son algo así como un transformer con una motosierra) y uno de ellos es la utilización masiva de static (a mí me gusta Shared por VB), un ejemplo de ello lo puedes ver en «Java Antippaterns: Too much static» http://www.odi.ch/prog/design/newbies.php#16; si buscas AntiPatterns Static en Live Search veras muchas opciones para minimizar el uso de static. (+1 a la solución de Rodrigo http://twasink.net/blog/archives/2004/08/singleton_stati.html)

    Finalmente, yo siempre creo que el error es propio, porque es la mejor forma de hacer mío el ámbito del error; sin embargo asumo que muchas veces encontrarlo es complicado y nunca descarto la opción de un error de VS, pero por defecto es mío. Encontrarlo es complicado y más en entornos como el que comentas (debe ser una verdadera lucha), así que (espero que todavía te quede) un poco de paciencia, algún amigo para que revise la app contigo y suerte!!!

    Saludos

  6. «Exiten una regla no escrita que debes respetar: Nunca tocar un miembro estático de otra clase desde un constructor estático. Interesante sobre este temá es: http://odetocode.com/Blogs/scott/archive/2004/07/29/360.aspx

    Creo que muy problablemente (con el 20% de la aplicación en constructores estáticos es probable que te la hayas saltado).»

    Glups :-% [Cara de sonrojo]. Por chiripa no lo hago, pero por chiripa.

    Rodrigo, no es por tener una sola instancia de cada elemento, es por no estar pasando referencias entre todas la clases entre sí. Los dispositivos los tienen que conocer todos los demás objetos porque los usan, y la configuración de la máquina también la cargo ahí.

    Respecto al código no seguro, efectivamente, medio programa lo es porque el .NET no tiene ni la mitad de cosas que me hacen falta, pero ese código está más que controlado… ¿Cómo te explicas entonces que el compilador del Orcas me haya detectado el error?

    Respecto a lo que a la mayoría de los demás desarrolladores les funcione aceptablemente bien vamos a dejarlo estar. De buena fuente sé que las excepciones no se disparan bien en los constructores estáticos, el código AnyCPU (en C#) revienta de vez en cuando la VM, y cosas así. Haz un proyecto AnyCPU con un puerto serie recibiendo y enviando con un timer windows forms, con la resolución más alta que puedas (1 ms), mételo en un Windows de 64 bits, y verás como tras un buen rato, casca. Compilalo con x86 y verás como no. O el bug de la corrupción de la imagen de fondo. Otra cosa es que hagáis programitas de juguete (que aunque sean complejos y grandes, no dejan de ser un «envio comando por red, recibo una cadena y la enseño por pantalla»).

    Rodrigo, cuando pongo aquí un post de estos estoy bien seguro de lo que hablo…

  7. Rafael, sólo una pregunta: ¿Conoces Delphi?

    Hubiese sido la solución a todos tus problemas (por ejemplo, te hubiese dejado declarar tranquilamente esas variables globales 😉 aunque digan que es tan de «mal programador» como usar el viejo y querido GOTO).

    No hemos de olvidar que Delphi es el «hermano mayor» de C#, igual que la VCL de Borland lo es de la BCL de .NET (que para algo todos ellos son hijos del mismo padre).

    Y en cuestion de accesos a bases de datos… pues… lo mismo 🙂

    Saludos

  8. PabloNetrix: conozco algo mejor: Borland C++ Builder, y tengo unas buenas docenas de proyectos hechos con él, pero dada la cantidad de bandazos que está dando últimamente Borland, que si Inprise el enterprise, que si TurboMan el poderoso, que si CodePlex el colchón… La fama de eliminar de un plumazo proyectos sin siquiera haberlos terminado (kylix, codewright, etc), me hacen huir como de la peste…

  9. «La VM de .NET esta podrida», es lo mas tonto que he escuchado (con todo respeto, pero hasta los mas inteligentes dicen tonterias).

    Estoy de acuerdo con «BRUNO» si eso fuera cierto los programadores de .NET estariamos en serios problemas.

    Un Saludo.

  10. Porque no instalas el executable debug en lugar del release ?

    Asi al menos el codigo extra del debug, ayudaria a estabilizar las cosas ahi o no ?

  11. Juan Fco. Berrocal: te puedo asegurar algo: lo estáis. En los constructores estáticos las excepciones no se lanzan bien, está el tema del Background de una ficha, que se corrompe él solito… Dime tu a mi qué es eso…

    necudeco: lee lo que he añadido al post: la aplicación, tras pasarla por el compilador del Orcas y encontrar mi última pifia, ya no falla.

  12. Rafael, creo que lo de la corrupción del fondo de un form, pasaba en VS2003 pero en VS2005 ya no… O bien era en VS2005 pero en el SP1 sí lo habian arreglado… Argggh! Sé que lei algo al respecto pero no recuerdo a qué productos ni versiones afectaba.

    Lo de los ctors estáticos… eso ya tiene más maña pinta. ¿No se habrá dado cuenta nadie más? ¿No lo habrá reportado nadie al blog del equipo de desarrollo en MS? Me parece que regalaban una suscripcion MDSN si un bug que reportabas era cierto y era grave. Yo creo que si te apuras lo consigues 😉

    En cuanto a Borland/Codeplex/You-name-it: Tienes más razón que un santo, de hecho queda MUY poquita gente leal a Borland (con lo que ese nombre llegó a significar para un programador!) y de los que aún trabajan (trabajamos) con Delphi o los diferentes Builders, es porque en nuestras empresas no se ha dado aún el paso de migrar a .NET (aunque en la mayoría posiblemente ya se esté estudiando ahora que la plataforma se puede decir que es madura).

    Y desde luego si alguien pretende pasar a .NET utilizando Delphi for .NET más vale que se quede quietecito. El único Object Pascal que vale la pena utilizar en .NET es Chrome.

    Saludos

  13. Bueno yo no tengo tanto nivel con la programacion como vosotros, asi que me paso muchas veces arreglando los fallos y tirandome de los pelos con los Asserts de las narices, jejeje. Que lojicamnete siempre seran culpa mia, pero bueno.
    En esta pagina se explica una forma para controlar a apliaciones ‘mal programadas’ como las mias, jejej y no morir en el intento.

    http://ubick-127.blogspot.com/2007/05/monitorizar-la-finalizacion-de.html

    Interesante, no?

    Agur.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *