¿Por qué uso C++? (IV)

Esta entrada es continuación de esta otra.

Otra cosa que me llama mucho la atención es el comentado del código. Yo no comento, o más bien comento poco o muy poco, generalmente en las cabeceras de las clases y los métodos, y sin embargo cojo código que no he tocado en tres o cuatro años y lo entiendo a la primera, y de hecho, cuando me avisan de algún bug, conforme me van contando lo que pasa, si recuerdo el código, veo no sólo en qué parte está, sino casi el método que está fallando. Y si no lo recuerdo, un rápido vistazo a los fuentes rápidamente me lleva a las líneas del error.

Harina de otro costal es luego encontrar lo que falla. Muchas veces no es trivial y me lleva su tiempo, pero si en general tengo acceso al depurador, un traceado y una parada en el lugar oportuno soluciona el tema. Ya lo he dicho antes, no suelo tener los típicos problemas de pisado y fugas de memoria, así como de sincronización, o más bien si los tengo los cazo cuando estoy creando el programa, por lo que en general no suelo encontrarme con bugs post-release demasiado gordos. Siempre hay excepciones, claro.

Depurar hardware es harina de otro costal, así como depurar protocolos de comunicaciones en tiempo real, y más difícil todavía cuando se juntan ambas cosas, máxime porque no puedes parar el código a ver qué está pasando. Cuando quieras hacerlo ya has abortado el proceso, o se te la llenado la uart, o te ha saltado un timer que no debía saltar, etc. A veces, poniendo y activando los puntos de interrupción adecuados y en la secuencia adecuada, se puede ver algo, pero al final lo más fácil es el traceado por puerto serie o por luces. Sí, luces, poner dos o tres LEDs o bombillas en una placa y encenderlas y apagarlas según te interese para ver por dónde pasa tu programa o el valor de alguna variable.

El traceado es más fácil, porque sólo tienes que ir poniendo salidas a la ventana de debug en el PC, y a un puerto serie o a un log en RAM con la información que quieras ver, y luego analizarla a tiempo pasado, volver a colocar y/o cambiar más trazas y seguir. Con lo de “log en RAM” me refiero a ir almacenando una serie de cadenas en RAM y luego mirarlas con el JTAG para simular lo mismo que si me las hubiera enviado por puerto serie cuando no tengo uno disponible.

¿Por qué uso C++? (III)

Esta entrada es la continuacón directa de esta otra.

Esto me lleva a un tema bastante candente y en el que mucha gente hace hincapié: los tests. Ignoro cómo se llama el tipo de test que hice, y realmente me importa bien poco por no decir nada. El que me conozca sabe que no creo en los tests organizados porque son eso, organizados. El comentario que siempre hago es que si te falla un test, ¿qué es lo que está fallando, el código de la rutina a verificar o el código que comprueba la rutina? Volvamos por un momento a lo de los enteros de 64 bits. Supongamos que el compilador con el que estamos verificando las operaciones tuviera un fallo con los enteros de 64 bits y que nuestro programa fallara. El tema no anda muy lejos del famoso bug numérico del Pentium ya que los micros modernos pueden trabajar con enteros de 64 bits si así lo quieres (y de hecho, la aplicación que comprobaba la salida numérica era de 64 bits para evitarme algún tipo de problema en el runtime de C++). O si lo queremos, nos fijamos en los fallos de la biblioteca de 32 bits del compilador para el microprocesador de destino, que fallaba cuando ponías más de una operación aritmética en una misma línea de código.

¿De qué nos sirven los tests? En este caso, de nada. Incluso se puede dar la circunstancia de que código defectuoso en la parte a verificar, más código defectuoso en la de verificación termine en unos resultados correctos pero que luego fallarán estrepitosamente cuando se pongan en producción. ¿Os dais cuenta de la paradoja?

Con esto no quiero decir que los tests no sirvan, lo que no sirve son los tests automatizados y tontos. Una rutina sencilla necesita un test sencillo, pero la probabilidad de que una rutina sencilla falle es muy baja, sobre todo si es trivial, y hacer tests de cosas complejas es tanto o más complejo que el propio código a comprobar. Imaginaros que por un momento simplemente hubiera puesto el generador aleatorio en marcha generando números al azar y comprobar si dichos números funcionaban bien. Ese test no me hubiera garantizado nada, porque no tendría control sobre el dominio generado.

Pero esperad, esperad un poco. ¿Generar números aleatorios de 64 bits con un generador que va desde 0 a 32767? ¿Cómo se hace eso? ¿__int64 i64=rand()<<24+rand()<<16+rand()<<8+rand()? Desde luego que así no, ya que estamos fijando una pauta binaria que se va a repetir en cada número y nos vamos a dejar otras fuera. Podríamos buscarnos el código de un generador aleatorio de caracteres y obtener 8 caracteres aleatorios en secuencia. ¿no? Pues no, porque seguimos obviando otras pautas binarias. Una opción es un generador aleatorio que obtuviera enteros de 64 bits, que los hay, pero incluso así tampoco íbamos a obtener todas las pautas. ¿La solución? Un generador binario (que genere unos y ceros) cargado con diferentes pautas secuenciales.

¿Veis que no es fácil? ¿Y si en lugar de operar con números binarios (es decir, almacenar los enteros en forma binaria) lo hacemos con BCD empaquetado? ¿Cómo debería ser el comprobador a partir de un entero nativo de 64 bits? ¿Y si nos equivocamos en el código que convierte de binario a BCD a la hora de hacer el test? ¿Y cómo comprobamos el comprobador de binario a BCD empaquetado?…

Creedme: la gran mayoría de tests no sirven de nada si no se estudia en detalle cómo hacerlos, y es por eso por lo que no creo en ellos. Evidentemente yo testeo, y no poco, pero no sigo ninguna metodología de ningún tipo, o más bien sigo la forma que mejor conviene al código que estoy haciendo en ese momento.

Esto nos lleva a otro tipo de tests todavía más difíciles de hacer, que son los tests de protocolos de comunicaciones y de máquina física. Suponed por un momento que tenemos un PC controlando un semáforo, y que el semáforo cuenta con un protocolo serie para decirnos no sólo cómo están todas las luces, sino para informarnos de si hay alguna fundida y de la densidad del tráfico en cada calle. ¿Cómo comprobamos eso? ¿Cómo comprobamos algo tan sencillo como el código que genera el checksum de cada mensaje si el código que lo comprueba va a ser el mismo que lo va a generar?

Bueno, esta tiene truco, porque lo que tenemos que comprobar es que la trama recibida sume cero o 0xff o el valor que se determine, pero os aseguro que se dan situaciones así. No digamos ya cuando tenemos que comprobar que los mensajes que recibimos sean reales, se correspondan con lo que realmente esté haciendo el semáforo. Y justo al revés, que lo que nosotros enviemos haga los cambios pertinentes en las luces. Podemos poner a un tío delante del semáforo y que nos vaya diciendo las cosas, pero en estos casos la gente se aburre como una ostra rápidamente y empieza a cometer errores. O podemos construirnos un “comprobador de semáforos”, poniendo una fotocélula delante de cada luz, y una cámara que cuente los coches en cada calle, y enviar eso al PC, y hacer un programa que compruebe si nuestro programa está haciéndolo bien…

¿Qué pasa si nos equivocamos con el programa de comprobación? ¿Cómo comprobamos el programa de comprobación? ¿Con el original? ¿Y si se combina un error en ambos que hace que el sistema resultante funcione bien en apariencia, ponemos el programa en producción, y a los diez minutos hemos armado la de dios en la calle?

Pero aun hay más. Si se estropea un sensor del programa de comprobación, ¿qué? ¿Qué pasará cuando el semáforo nos envíe datos incorrectos porque se haya averiado? ¿De cuántas formas se puede averiar? ¿Y si se avería de una que no hemos contemplado, y lo que es peor, nuestra máquina de estados de semáforos se queda pillada en un estado del que no puede salir?

Asusta, ¿eh? Bueno, no mucho, pero son situaciones de la Vida Real ™ que se parecen mucho a las que yo tengo que testear, y es por eso por lo que no entiendo cómo un programa que presenta unos datos sacados de una base de datos puede requerir tantos tests y tanta martingala. Comprendo que cuando hay cálculos y operaciones matemáticas podría haber algo de complejidad, pero muchas veces es el propio gestor de bases de datos el que hace las operaciones. También entiendo que pueda haber interrelaciones entre distintas partes, pero, sinceramente, un control de almacén, o una facturación son algo trivial (o al menos así lo creo) comparado con un protocolo de comunicaciones más o menos decente.

Tampoco he hablado del dominio de la aplicación y de los tests. En el caso de los semáforos, ¿a quién debemos dejar que programe los tests? ¿Cómo podemos testear todas las condiciones posibles, o más bien verificar todos los tipos de condiciones que se puedan generar? Y lo que es peor, ¿estamos seguros de que el software va a poder lidiar con todas las posibles condiciones, que no se va a generar una que termine no en un atasco, sino en un accidente con muertes? Ciertamente no es un tema sencillo del que no quiero hablar más aquí, pero que quizás tome en otro momento.

¿Por qué uso C++? (II)

Esta entrada es la continuacón directa de esta otra.

Ya lo he dicho antes, hay aplicaciones que resultan absurdas realizadas en C++, y también otras que también lo son cuando están hechas con otros lenguajes. Imaginaros un Autocad o un Office que estuviera íntegramente escrito en .NET. Si ya nos quejamos del rendimiento y la velocidad de carga de ellos, ya no os digo si en lugar de ser código nativo fuera .NET, o peor aún, Java (No le deseo ningún mal a Java, pero es lento de cojones, y las aplicaciones que he visto hechas con él también lo son. Ignoro si se debe al propio lenguaje o a la posible incompetencia de los programadores, pero el hecho final es que es lento).

Pues bien, en mi caso, C++ es el lenguaje que más objetivos cumple respecto a mis necesidades: programas industriales de uso interno a la empresa (para la cadena de producción y para testear productos), programas de demostración de nuestros productos, simuladores, pedazos de código de demo, firmware para nuestros cacharros y firmware de interoperabilidad entre el PC y el aparato (cuando no es el PC el propio aparato), y muy de tarde en tarde alguna aplicación completa o casi completa para algún producto de terceros cuando el producto no tiene nada que ver con nuestra línea principal de productos (es decir, si nos dedicáramos a hacer máquinas para tostar pipas de girasol, esa aplicación podría ser para un mando a distancia).

Como mucho código está compartido entre el PC y el firmware, lo ideal es C++, y C cuando no haya otra posibilidad. Además, la mayoría de cosas que hago está relacionada con protocolos de comunicaciones, y el PC entra para la simulación y verificación de los mismos.

Os pongo un ejemplo. Hace unos meses tuve que implementar una biblioteca aritmética de enteros de 64 bits para un micro de 8 bits. Es decir, teníamos que realizar operaciones entre enteros de 64 bits dentro de un micro de 8 bits, y a lo más que llegaba el compilador con más posibilidades era a trabajar con enteros de 32. No es que ese fuera el objetivo del firmware, sino que varias de sus tareas exigían el uso de ese tipo de operaciones, así como convertir cadenas de texto en enteros y viceversa. La solución impedía el uso de C++ y de algunas clases que hay circulando por ahí, así que era necesaria una implementación con dos enteros de 32 bits unidos en una estructura, todo ello en C y ocupando el menor tamaño de código posible y con la ejecución más rápida.

¿Cómo habríais resuelto vosotros el tema? Una posibilidad era ir probando en el micro mediante el proceso de editar código, compilar en plataforma cruzada, grabar en el micro y depurar con el JTAG… Laborioso, ¿no? Además, está el problema de cómo comprobar si las operaciones han sido correctas o no.

Ahí es donde entra el PC de lleno. La solución consistió en hacer un programa en estricto C (para que luego el compilador embebido tragara sin problemas) y mezclarlo con código en C++ que, aprovechando que el PC sí que es capaz de trabajar con enteros de 64 bits, comprobara la salida del otro código. Otro problema es el dominio de la aplicación. Si no compruebo todas las combinaciones posibles pudiera ocurrir que el programa fallara con alguna combinación de números, por lo que se requería una prueba bastante exhaustiva aunque no completa porque no es viable comprobar todas las combinaciones aritméticas (suma, resta, multiplicación y división) de cualesquiera dos enteros de 64 bits. Entre las comprobaciones hay que tener en cuenta las operaciones entre dos números muy pequeños, dos muy grandes con y sin desbordamiento, uno grande y uno pequeño en ambos lados, también con y sin desbordamiento, y sobre todo en la franja en la que el resultado y los operandos están en el entorno del salto de los que caben en 32 bits a los más bajos de 64…

Dicho y hecho, tras unos cuatro mil trillones de números generados aleatoriamente con conocimiento de causa (siguiendo las reglas descritas arriba) y un fin de semana de un Q4 al 100% de CPU, comprobamos que la biblioteca no tenía errores o estaba más lo más cerca posible de no tenerlos, de hecho descubrimos código profesional que hay suelto por ahí que en determinadas circunstancias falla, pero esa es otra historia.

Una vez colocado en el micro junto al resto del firmware comprobamos que… ¡fallaba miserablemente! De hecho era completamente incapaz de generar una sola operación correcta. Tras una investigación descubrimos que lo que fallaba no era nuestro programa, sino la biblioteca de enteros de 32 bits del compilador y el propio compilador, que no era capaz de trabajar ni con una estructura de dos enteros de 16 bits lado a lado ni con sus punteros. Esa también es otra historia que no viene al caso y que se solucionó simplificando el código (separando operaciones complejas de multiplicación más suma de enteros de 32 bits en operaciones más sencillas con datos intermedios) y usando variables globales separadas en lugar de estructuras.

En resumen, para que eso funcionara tuvimos que tirar a la basura los conceptos de ocultación de datos, genericidad y abstracción. Hicimos un proyecto de demo con los problemas del compilador y de la biblioteca, se lo enviamos al fabricante del compilador y… ¿Habéis recibido vosotros respuesta? Nosotros tampoco.

Esta entrada continuará la semana que viene.

¿Por qué uso C++? (I)

Hace poco me preguntaron que contara mis experiencias en mi trabajo con C y C++. Y eso lo hacen no solo aquí, sino que muchas veces me lo preguntan en la Vida Real™, y la verdad es que lo tengo bastante difícil, porque considero que no tengo experiencias con él, y cuando las tengo no puedo (ni debo) hablar de ellas, así que voy a intentar acercarme lo más posible a lo deseado, sabiendo que no voy a poder explicarme bien y que posiblemente levante alguna que otra polémica.

Para mi C++ es el lenguaje. Así muy, pero que muy por encima, conozco Visual Basic (el clásico y el moderno), supongo que podría leer y medio interpretar un programa escrito con ellos, lo mismo que con Pascal por mi herencia de C++ Builder. Algo sé de Java, sobre todo de cuando salió, y en mis tiempos mozos programé con dBase y luego con Clipper. En su momento sabía Cobol (que olvidé al momento de hacer el examen) y era bastante productivo con los ficheros .BAT, haciendo verdaderas virguerías. También sabía varios ensambladores, entre ellos el del Z80, x86 y 80×31.

Los lenguajes que conozco bien son C, C++ y C#. Y sus idiomas, es decir, Visual C++, C++ Builder, Windows CE (que incluye a Windows Mobile), MFC, Win32 y .NET. También he hecho mis pinitos con C y Linux, algo de las APIs de GTK y de KDE. Ahora estoy indeciso si seguir con QT o profundizar más en MFC… QT me tienta mucho, pero mucho de verdad, sin embargo lo veo como demasiado inseguro, sobre todo desde que lo ha cogido Nokia y está sacando versión tras versión cada cuatro meses, a piñón fijo, esté como esté el producto, y así está acumulando bugs y problemas. Otra cosa que no me mola de QT es que con la versión LGPL no puedo enlazar estáticamente, sino que tengo que distribuir las DLLs adecuadas como runtimes, y para runtimes ya tenemos bastante con los de Visual C++ y su puñetero DLL Hell, y tampoco es cuestión de distribuir 300 megas de runtimes cada cuatro meses. Sin embargo, una de las cosas por las que QT brilla con luz propia es que soporta una enorme cantidad de plataformas como Windows CE, Symbian, MAC, Linux, Windows…

También tengo bastante experiencia en programar hardware sin sistema operativo, mayormente en C porque muchas veces no hay compilador de C++. De todo lo que hago es lo que menos me gusta más que nada porque tengo que usar C y veo las enormísimas limitaciones que tiene frente a su hermano mayor. Cosas que se podrían solucionar de un plumazo (es un decir) con una jerarquía de clases y polimorfismo las tengo que hacer con arrays de punteros a funciones, sentencias switch y otras zarandajas (que, entre nosotros y sin que nadie más se entere, es lo que termina haciendo el compilador de C++ pero de forma transparente para el programador). Tampoco me gusta mucho tener que pelearme con la configuración de los registros del micro ni el estudio de los datasheet que eso conlleva, etc. Además, la mayoría de compiladores de C no son estándar y ni siquiera lo cumplen. No es que traigan extensiones, que son de desear para algunas cosas como definir interrupciones o colocar variables en ROM, RAM, FLASH o registros, sino que, dependiendo del fabricante y el micro de destino, muchas cosas funcionan de forma diferente a lo esperado.

Y bajo toda esta experiencia propia no creo que haya nada como C++. Su expresividad, su potencia y su rapidez son inigualables ante cualquier otro lenguaje. Ya sé que es difícil de aprender, y más todavía de usar, pero pilotar aviones también lo es y nadie se queja. Y, personalmente, todos esos problemas que tiene la gente con C++ yo no suelo tenerlos. Evidentemente cometo bugs como todo mortal, y a veces me atasco en tonterías, pero casi nunca tengo los típicos problemas de fugas de memoria y no liberación de recursos.

Será porque siempre que pongo un new, pongo un delete, siempre que hago o modifico el constructor hago el destructor, siempre que llamo a una función que abre un recurso también llamo a la que lo cierra, e intento evitar salir de un método si no es al final (incluso con gotos, sobre todo en embedded), y si lo hago miro hacia arriba para ver qué tengo que cerrar/liberar. Y por supuesto uso, allá donde estén, las herramientas de análisis de código y de detección de problemas, en las que Visual C++ es inigualable y a veces te dan una grata sorpresa, ya sea por la genialidad a la hora de detectarte un sibilino potencial problema o por la de mostrarte algo verdaderamente estúpido y sin sentido.

C++ es para mi la herramienta, el súmmum de todos los lenguajes compilados de programación, y hasta la fecha no hay ningún otro que lo supere. De hecho, siempre lo he dicho y lo repito cada vez que tengo oportunidad: si te has propuesto aprender C++ y no has podido es porque eres mal programador. No hay otra. Lo que sí entiendo es que C++ no sea adecuado para todo, ni de lejos. Entiendo que si vas a hacer una aplicación de bases de datos típica, C++ queda un poco… digamos… pequeño. No pequeño en cuanto a características, sino pequeño en su fijación por el detalle, porque seamos serios: C++, con una buena biblioteca de acceso a bases de datos, podría ser insuperable. Y si no que se lo pregunten a Clipper. [Para el que no lo sepa, Clipper era un front-end con sintaxis similar a dBase generalmente simulada con macros que pasaba a C y que luego se generaba un p-code que era interpretado por un runtime. De hecho podías compilar bloques de código y guardarlos en una tabla como p-code, que luego era interpretado y ejecutado por el motor principal –más o menos lo que son ahora los operadores delta]. Y si con C se podía hacer eso, ya no os digo con C++ y una biblioteca similar.

Alguno podría pensar que la incapacidad de aprender a programar C++ podría venir de la dificultad y abstrusismo del propio lenguaje, pero os puedo asegurar que no es así. Podría compararlo con el álgebra y el cálculo. Un lenguaje como C# o Java es álgebra, mientras que C++ es análisis. Hay muchas cosas que se pueden hacer con la primera, pero hay otras que o bien son muy difíciles o bien completamente imposibles. Imagina que necesitas obtener los parámetros de una curva. Puedes representarla gráficamente usando el álgebra, y ver sus máximos y sus mínimos, y calcular una tangente por aproximación, pero si realmente quieres ver todos sus detalles, obtén su derivada primera y segunda, y obtendrás la tangente en todos los puntos y sus máximos y mínimos…

Y de igual forma que hay gente que se para en el álgebra y es incapaz de seguir con el análisis, también hay programadores que son incapaces de avanzar hacia C++, y de igual forma que un matemático que no sepa análisis no será muy buen matemático, un programador que no pueda aprender C++ tampoco será un buen programador. Ojo, he dicho que no pueda aprender, no he dicho que tenga que usar C++ para todo.

Tampoco me gustaría que se me interpretara como que si no sabes C++ no eres programador. Nada más lejos de mi intención. Existen y existirán muchísimos programadores que son buenos y no saben C++ porque no les haya hecho falta ya que ante todo, está el concepto de un lenguaje para cada tarea, que ya he avanzado un poco más arriba. Yo mismo, de hecho, hago muchas pequeñas utilidades para consumo propio en C# porque resulta mucho más rápido aunque luego el desempeño no sea el ideal, utilidades que me sirven para procesar o convertir datos y que están asociadas a proyectos más grandes. Y también tengo aplicaciones completas hechas en C#, tanto públicas (como el zxFortunes) como de uso interno o comercial (que se acompañan con el hardware que vendemos).

Esta entrada continuará la semana que viene.

Hemos leído: Windows Internals 5ª Edición

No sé si es que la memoria me falla, o que tiempos antiguos fueron mejores o que simplemente he mezclado dos libros, pero lo cierto es que este libro me ha reportado más desilusiones que ilusiones. Recuerdo haber leído ediciones anteriores (la cuarta seguro que no), aunque en mi biblioteca física –la de los libros en papel, vamos-, sólo veo el de Helen Custer, que fue, si de nuevo las meninges no me engañan, el primero o la precuela de la serie. Pero lo que no recuerdo es que fuera tan políticamente correcto, y eso es lo que me ha defraudado de este.

Porque en el libro no viene nada que no esté documentado en el DDK, el SDK u otra documentación emitida por Microsoft, lo único es que, aquí, está todo reunido de una forma más o menos coherente, organizada y en papel.

Es evidente que intentar explicar en un libro –aunque sea de más de 1200 páginas- el funcionamiento de un sistema operativo que tiene más de cincuenta millones de líneas de código es tarea imposible.

Lo que no esperaba es que fuera tan políticamente correcto, tan formalmente estudiado para no resultar inadecuado, ya que seguro muchas de las cosas que se cuentan en él no son tal y como se han presentado, o se han omitido. Y estoy seguro de ello, porque en algunas secciones hay huecos y cosas que no están del todo explicadas. Si bien se pueden deber al desconocimiento de los autores, o al recorte de páginas, el texto podría haber sido más fluido. Hay secciones enteras que no son más que un mero recitamiento en plan loro de lo aprendido, en lugar de resultar un continuo coherente. Y de hecho, hay partes que no las he leído por aburridas, pesadas y por apuntar a ser un mero listado de características tal y como se pueden encontrar en una documentación oficial.

Y como muestra de lo omitido, un botón: ¿Se fragmenta el registro de Windows?, escrito en este mismo blog. Pues bien, hay muchas secciones así, que te dejan con la palabra en la boca, como si sólo te hubieran contado lo que han querido…

No obstante esas pegas, que pueden enteramente deberse a mi mismo y a que no aceptaría la invitación a un club que me aceptara como socio (parafraseando del mala manera al insigne Groucho), el libro debe ser una gozada para muchos administradores y algunos programadores, ya que te explica muchas cosas que de otro modo quedarían al aire o como interpretación de un curso de sistemas operativos 101.

Así mismo, en aquellos casos en los que pienses que el problema no está en tu código o tu script, puedes bucear dentro de las estructuras internas de Windows (allá donde el libro las explica, que no es en muchos sitios, la verdad) para descubrir dónde te has equivocado tras muchas horas de investigar donde no era.

Y poco más que contar, si quieres ver qué trae el libro por dentro, pues te das un voltio por Amazon y miras el índice.