lunes, 11 de marzo de 2013

¿Cómo escribir aplicaciones portables?

Antes de decir nada tengo que hacer una aclaración sobre el título, y es qué entiendo por "portable", ya que es una palabra que se puede usar para indicar varias cosas y su significado por defecto en relación a aplicaciones de software creo que ha cambiado con el tiempo. Cuando yo uso "portable", me refiero a aplicaciones que pueden correr en distintos sistemas operativos o arquitecturas de software y hardware (por ejemplo, que puedo hacer andar tanto en Windows como GNU/Linux). La idea es que una aplicación es más portable cuanto menos depende de elementos específicos de un sistema operativo en particular. Creo que los desarrolladores seguimos teniendo ese concepto asociado a "portabilidad".

Sin embargo, actualmente muchos usuarios finales asocian "portable" al hecho de poder llevar la aplicación de una PC a otra en su pendrive para usar sin necesidad de instalarla, como un solo exe que se ejecuta y ya, o como un zip que solo hay que descomprimir. De hecho pueden encontrar googleando que por ahí alguien ofrece PSeInt portable o ZinjaI portable en este sentido, y supongo que todo lo que hizo ese alguien fue instalarlo en una pc y luego comprimir la carpeta de instalación en un zip/rar/autoextraíble/lo-que-prefieran. No se dejen engañar, mis programas se pueden copiar sin necesidad de instalar, siempre, cualquier versión, sin problemas ni modificaciones. El único trabajo adicional que hace el instalador es crear accesos directos y asociar las extenciones, pero sacando eso, no es más que un gran zip preguntando donde descomprimirse (lugar que tranquilamente puede ser un pendrive). Pero esa no es la idea de portabilidad que me preocupa, sino la anterior, y de los detalles de implementación relacionadas a esa primer interpretación es de lo que habla este post.

Hay varias formas de lograr la portabilidad que me interesa. El primer punto (y más importante) es elegir un lenguaje y un conjunto de bibliotecas que sepamos que se encuentren disponibles para las distintas plataformas que tenemos por objetivo. En general es fácil conseguir compiladores o intérpretes en cualquier plataforma para los lenguajes más populares. Entonces, siempre que nos apeguemos a las bibliotecas y demás elementos estándar de un lenguaje no vamos a tener problemas. Eso usualmente no alcanza y tenemos que valernos de bibliotecas adicionales, no estándar, pero siempre hay opciones que pueden conseguirse compiladas o para compilar en los diferentes sistemas operativos. Entonces escribo el código muy cómodo en mi GNU/Linux, pero al final del día puedo ir a un Windows (o usar wine) y recompilarlo allí (casi) sin ningún cambio.

Por ejemplo, wxWidgets es en realidad un wrapper muy complejo para las cosas específicas de cada sistema. Es decir, en GNU/Linux, las implementaciones de los métodos de wx llaman a cosas de gtk, mientras que en Windows usan la winapi. Así las diferencias quedan ahí adentro y mi programa no tiene porqué enterarse (los métodos de wx se ven siempre igual desde "afuera" más allá de qué usen "adentro" en cada caso). Sin embargo, pueden notar pequeñas diferencias entre las distintas versiones y hay que tener cuidado con eso, sobre todo porque no tenerlo en cuenta puede ser un error y puede que si probamos siempre en una misma plataforma no lo descubramos. Por ejemplo, los tamaños por defecto de los controles pueden ser diferentes y entonces lo que pensamos para un so no se ve bien en otro (por eso wx usa sizers y se recomienda nunca poner tamaños fijos, sino tamaños mínimos) u otros detalles (como que el orden en que se llaman dos eventos puede ser el inverso, por ejemplo el de recibir el foco y el de responder a un click en un wxTreeCtrl). En general estos detalles están directa o indirectamente documentados en la referencia de la biblioteca, pero como uno nunca lee toda la referencia antes de empezar, sino que lo hace bajo demanda, lleva tiempo y algo de experiencia identificarlos.

Y cuando esto no es suficiente, un "truco" en C/C++ para tener bajo la manga es la compilación condicional. Si por alguna razón tienen un código propio que necesitan que sea diferente según la plataforma pueden usar directivas de preprocesador como sigue:
   #ifdef __WIN32__
      // hacer algo solo en windows
      string so="Windows";
   #else
      // hacer algo solo en GNU/Linux
      string so="otro, probablemente GNU/Linux"
   #endif
   cout<<"El sistema operativo actual es: "<<so<<endl;

El ejemplo usa un if de preprocesador (antes de compilar se evalúa el if y se compila solo una de las dos opciones) y unas constantes que ya vienen definidas en cada sistema (las usuales son __WIN32__, __APPLE__, __unix__). Lo más común es preguntar, como en el ejemplo, si es Windows o es cualquier otra cosa, ya que el 99% de las otras cosas siguen un estándar llamado POSIX y en general con eso nos alcanza.

A la izquierda un código que utiliza la compilación condicional, 
a la derecha se muestran los perfiles del proyecto para los diferentes sistemas operativos.

Esta portabilidad de la que estoy hablando en primer lugar es la portabilidad del código fuente más que la de la aplicación. Otras alternativas similares que no involucran directamente compilación pueden ser lenguajes interpretados, como Python, o algunas más grises como Java, donde se "compila" a un código intermedio, que una máquina virtual (la cual se consigue para cualquier sistema operativo), termina de convertir a la plataforma en que ejecutemos. La idea en su concepto es excelente, pero en la práctica ese paradigma de "write once, run everywhere" (escribe una vez, corre en todos lados), suele convertirse en "write once, debug everywhere" (escribe una vez, depura en todos lados). Además, la máquina virtual (o el runtime, o como lo llamen según qué lenguaje) puede llegar a ser enorme en varios sentidos. Tanto que prefiero evitarlo y escribir en C++. De esta forma reniego un poquito más yo, pero ofrezco aplicaciones más rápidas, livianas, y con menos dependencias.

Pero no solo de lenguajes y bibliotecas se trata esto de la portabilidad. Así que aquí voy a enumerar otros aspectos prácticos muy importantes a mi criterio. Por un lado el IDE. Aunque en realidad este no es fundamental, es el menos importante, lo presento primero porque tiene que ver con el tema del lenguaje y las bibliotecas del que venía hablando. Muchos proyectos usan diferentes IDEs en diferentes plataformas, pero entonces hay que mantener todos los archivos de proyecto "sincronizados". Si van a desarrollar y probar en diferentes sistemas, suele ser más cómodo utilizar un IDE portable (como ZinjaI :) para que puedan así trabajar siempre de la misma manera, y además para que el IDE ya les solucione algunas tareas. ZinjaI maneja las conversiones que necesita para sus cosas internas automáticamente cuando abrimos un proyecto de un so en otro. Para las cosas que debe configurar el usuario (la compilación: rutas de bibliotecas y esas cosas), todos los IDEs tienen perfiles de compilación (en ZinjaI, solo en proyectos, menú Ejecución->Opciones). Si usamos bibliotecas no estándar vamos a necesitar seguramente distintos perfiles para los disitintos sistemas. Las plantillas de ZinjaI que usan bibliotecas externas como las de wxWidgets y SFML ya tienen estos perfiles predefinidos. Si crean un proyecto en blanco y configuran ustedes las plantillas, deben tener en cuenta esto y crear perfiles separados.

Diagrama ejemplo del proceso para desarrollar PSeInt portable. Con cualquier ZinjaI en cualquier SO escribo mi único código fuente, que hace uso de bibliotecas estándares y portables (y eventualmente algún caso especial propio mediante compilación condicional). Todo el código se compila varias veces, una vez por plataforma, con su compilador particular (en mi caso, ports de gcc). En resúmen, lo de fondo celeste es lo que escribo, lo de fondo naranja son las herramientas que utilizo.

Lo mismo aplica a cualquier otra herramienta adicional que sea fundamental para el proceso de desarrollo, como podrían ser un sistema de control de versiones, o de gestión de proyectos. Pero también a veces algunas herramientas particulares pueden ser extremadamente útiles y solo estar disponible en un sistema particular. Si estas herramientas son parte del proceso de desarrollo, pero no del producto final (como por ejemplo Valgrind, del que ya hablaré en detalle en otra serie de posts), entonces se pueden aprovechar igual, y a veces son las que justifican la portabilidad del código. He leido sobre casos en que se porta a un sistema una aplicación comercial no para distribuirla en ese sistema, sino solo para aprovechar una herramienta de este tipo.
Otro detalle a tener en cuenta al escribir una aplicación portable son los Nombres de Archivos. Los nombres de archivos (cualquiera, imágenes que carga una ventana, cabeceras que incluimos en el código, o los que genere nuestro programa para guardar la configuración, por ejemplo) se tratan de forma diferente en Windows que en el resto de los so. Para empezar, Windows no distingue entre mayúsculas y minúsculas, pero el resto sí. Entonces en Windows es lo mismo el archivo "Hola.txt" que el archivo "HOLa.TXT" que el archivo "hoLA.TxT", pero en GNU/Linux son todos diferentes. Si programan en Windows, tengan cuidado de usar los nombres correctos para los #includes y de escribir los nombres de sus propios archivos siempre igual. Otro detalle está en cómo separan las carpetas. En Windows se usa una barra (\) mientras que en el resto del mundo otra (/). Sin embargo este problema es menor ya que en la mayoría de los casos podemos usar la segunda (/) y la biblitoeca/compilador/quien le toque la entiende igual aunque esté en Windows (por ejemplo, al hacer un #include <GL/glut.h>" no hay problemas compilando en Windows). En caso de haber problemas o querer ser cuidadoso pueden usar un #ifdef, o ver si alguna biblioteca que estén usando tiene algo para esto (como por ejemplo wxFileName::GetPathSeparator en wxWidgets). Otra recomendación importante para evitar muchos problemas es no usar espacios, ni ñs, ni acentos, ni otros caracteres raros en los nombres de archivos o carpetas.

Y además de los nombre propiamente dichos, pensando en los archivos que genera una aplicación (de configuracion por ejemplo), hay que saber elegir su ubicación de acuerdo a las políticas y prácticas recomendadas en cada plataforma. A menos que el programa tenga un "guardar como" y el usuario elija donde, al elegir uno mismo la ubicación es razonable no querer guardar los archivos que el programa genera en la carpeta de instalación, ya que esta será "opt/algo" o "usr/algo" en GNU/Linux, "program files/algo" o "archivos de programa/algo" en Windows, y se supone que el usuario común no tiene permisos para escribir ahí (aunque Windows hace un truco que ya no deberíamos aprovechar, y anda). Cada so tiene carpetas específicas pensadas para esto. En GNU/Linux lo común es hacer una carpeta nueva llamada ".algo" (el punto incial es para que quede oculta) en el home del usuario (que se obtiene con la variable de entorno $HOME). En Windows depende de las versiones (sobre todo pre/post Vista) y hasta donde se no hay una variable como $HOME que entregue la ubicación correcta en todas las versiones, pero pueden usar %APPDATA% que sí está definida en todas y es lo más parecido (da un lugar razonable cuando no es el correcto). Entonces, lo mejor sería que el programa haga una carpeta nueva en %APPDATA%/algo y ahi guarde las cosas (las que no le pregunta al usuario donde guardar). Algo similar aplica a los directorios pensados para archivos temporales. Para consultar estas variables de entorno (HOME, APPDATA, y otras) pueden usar getenv, que es una función estándar de C, o nuevamente cederle el trabajo a su biblioteca de confianza (ver getters estáticos de wxFileName por ejemplo).

Espero que esta ensalada de pequeñas y grandes consideraciones se entienda y les sea útil si consideran escribir o ajustar alguna aplicación para que sea portable en este sentido. Si creen que me pasé por alto algo importante o quieren agregar algún detalle no duden en dejar su comentario.

8 comentarios:

  1. ¡Muy buen post! Muy interesante debido a que es un tema en el que se enfrenta casi todos los días, principalmente para los desarrolladores que utilizen más de dos SO.

    Justamente yo estuve en el desarrollo de un juego de ruleta en C (Un programa sencillo, de cerca de 2000 líneas de código) y lo hice para que funcione en GNU/Linux y Windows.
    Básicamente los problemas con los que me tope fueron que en la función printf() los caracteres especiales (como los acentos y las ñ) se representan de forma diferente, ya que utilizan codificación distinta.
    Luego también renegué con las llamadas al sistema con la función system() y funciones especiales como gotoxy(). Pero con paciencia y cariño (Que traducido sería "mucha búsqueda en Internet" y "un include que defina la portabilidad para cada SO y el uso de los #ifdef") logré que la aplicación compile en los dos SO tan solo declarando al principio del código fuente bajo que SO se va a compilar.
    Particularmente con el tema de la arquitectura no tuve ningún tipo de problemas, ya que lo compile para 32 y 64 bit, nada más. Básicamente los problemas son para el compilador.

    ResponderEliminar
  2. Yo en mi caso para pasar a lo de programas portables trabajo con python y QT pero esto hasta cierto punto es mentira dado que se necesitan las librerías para cada SO sin contar del interprete python.

    igual a sido uno de los lenguajes en los que me e centrado mucho, mas cuando vengo desde java.

    Me gusto mucho tu post seguire leeyendo lo que tienes.

    ResponderEliminar
  3. Consulta: Se ven afectados los comentarios que llevan acentos?

    ResponderEliminar
    Respuestas
    1. No entendí la pregunta... ¿Comentarios en el blog? No, ¿de qué manera?

      Eliminar
    2. me refiero a los comentarios que se le agregan al código fuente.

      Eliminar
    3. No, solo hay que saber qué codificación de caracteres de texto se usa. En ZinjaI y PSeInt uso por defecto el ascii, por ser el más básico y universal. Pero los editores nuevos pueden utilizar otros como utf o unicode. En general, en todas las codificaciones que tienen una base de 8 bits hay un subconjunto de caracteres que es común al ascii original (que en los editores sería iso-8859 creo), para que lo que uno escriba con los caracteres básicos se lea bien en cualquier editor aunque no soporte las diferentes codificaciones. Cuando se introducen acentos, u otros caracteres "raros" (para el Inglés), ahí cambia. Pero solo es cuestión de usar en todos los sistemas una misma codificación, en general todos los editores "completos" permiten configurar eso.

      Eliminar
    4. Entonces la recomendación sería que para escribir los comentarios en los códigos fuentes, se utilice codificación de caracteres unifersales?

      Eliminar
  4. mencionas que para garantizar un buena portabilidad, se debe evitar usar espacios, acentos, eñes y otros caracteres raros en los nombres de archivos o carpetas. Entonces mi consulta es que si uso acentos dentro de un comentario que le agregue al código fuente, me afectaría?

    ResponderEliminar