miércoles, 8 de julio de 2020

Embeber recursos dentro de un ejecutable en GNU/Linux

Trabajando en un nuevo proyecto que tiene que ver con visualización y usa OpenGL me encontré con el problema de cómo distribuir los shaders. Los shaders son, para este caso, simplemente archivos que el ejecutable necesita encontrar. Mi problema es que quiero un ejecutable que no requiera de otros archivos, porque eso puede generarme algunos inconvenientes.

La pregunta es entonces ¿cómo embeber recursos en un ejecutable en GNU/Linux? ¿Cómo hago que esos archivos extra estén dentro del ejecutable, y cómo los recupero desde mi código C++? La respuesta es simple, nada de otro mundo; pero creo que poco conocida. No es la primera vez que tengo este problema, pero en las anteriores no sabía ni había podido encontrar la respuesta.


El problema

En mi aplicación quiero que el ejecutable sea autosuficiente y no deba cargar otros archivos, principalmente para poder invocarlo desde cualquier lado. Si no, si el ejecutable busca recursos en su directorio, al invocarlo desde otro lado hay que tomar recaudos. Y esto de invocarlo desde otro lado es muy frecuente si estamos acostumbrados a trabajar en consola. En cualquier directorio donde tengo un archivo a visualizar, quiero poder escribir "bfr <el_archivo>" y listo (bfr es el ejecutable). Además, quiero que si durante la ejecución cambia el directorio de trabajo, siga encontrando todo.

Hay dos soluciones habituales: que el ejecutable determine su ubicación absoluta al inicio, y a partir de ahí fije todas las rutas de recursos relativas a esa ubicación; o que los recursos estén en una ruta fija y conocida en el sistema. Lo primero no es imposible, pero no es trivial. No alcanza con analizar los argumentos del main (argv[0] en particular) para saber la ruta del ejecutable. Por ejemplo, si se invocó a través de un enlace simbólico, encontraremos la ruta del enlace, no del ejecutable. O si el directorio estaba en la variable PATH, tampoco lo vamos a encontrar. Lo segundo es lo habitual en GNU/Linux al instalar un programa: que las cosas estén por ejemplo en "/usr/local/share/foo/...". Pero esto obliga a instalar (y entonces requiere permisos y solo puede haber una versión), o a definir de alguna manera una ruta alternativa en tiempo de compilación, etc.


La solución

En Windows es normal definir un archivo de recursos (un .rc), que se compila con un paso especial (con mingw, la herramienta es windres) y se enlaza al ejecutable. Es lo que hace ZinjaI, por ejemplo, cuando definimos un ícono para el exe, o un manifest.xml para los proyectos con wxWidgets. Sin embargo, no conocía hasta hace poco alternativa en GNU/Linux. Recientemente me cruzé con una respuesta en stackoverflow, y ahora surgió la ocasión para ponerla en práctica.

El funcionamiento de la solución es realmente muy simple: primero se genera una biblioteca que tiene dentro el archivo como si fuera una constante (como en un xpm, imaginen "const uint_8 el_archivo[] = ...tooodo el archivo...;"), y luego se enlaza al ejecutable. Pero la gracia está en que no hay que convertir al archivo a embeber en un código fuente (como con xxd) para luego compilarlo, sino que la herramienta objcopy puede generar directamente el equivalente ya "compilado". Y luego solo hay que saber qué símbolos se definen ahí dentro para poder referirse a ese contenido desde el programa cliente.

Supongamos que quiero embeber los archivos "foo.vert" y "foo.frag". Primero, genero un objeto para cada uno con "objcopy" (creo que "ld" también podría hacer este mismo trabajo), y luego junto ambos objetos en una biblioteca estática con "ar". Esa biblioteca se podrá enlazar luego al ejecutable:

    objcopy --input binary --output elf64-x86-64 \
        --binary-architecture i386 foo.vert foo_vert.o
    objcopy --input binary --output elf64-x86-64 \
        --binary-architecture i386 foo.frag foo_frag.o
    ar -rcs foo.a foo_frag.o foo_frag.o

Y con eso tenemos el resultado en "foo.a". Si fuese para 32bits, hay que cambiar el "--output elf64-x86-64" por "--output elf32-i386". En cualquier caso, en ZinjaI podemos esconder esto en algunos pasos de compilación personalizados (o si son muchos archivos como en mi caso, un solo paso que invoque a un script de bash).

Ahora, la parte de C++. En este caso, voy a convertir esa info a dos strings porque era lo que necesitaba en mi aplicación (porque cada shader es un código fuente, un archivo de texto):

    extern const char _binary_foo_frag_start[];
    extern
const char _binary_foo_frag_end[];
    std::string foo_frag(_binary_foo_frag_start,_binary_foo_frag_end);

    extern const char _binary_foo_vert_start[];
    extern
const char _binary_foo_vert_end[];
    std::string foo_vert(_binary_foo_vert_start,_binary_foo_vert_end);

Hay que respetar los nombres de los punteros. Por cada archivo embebido, tenemos una etiqueta "_binary_[nombre]_start" que apunta al comienzo de su contenido, y otra "_binary_[nombre]_end" que apunta al final de su contenido. A estos nombres los fija objcopy. Para el tipo, lo más genérico es uint_8, pero en este caso se que es solo texto ascii. La respuesta original de stackoverflow usaba inline assembler, aparentemente para renombrarlos, pero parece no ser necesario. Mejor evitarlo, el inline asm no es estándar, y entonces aquella solución compilaba con gcc pero no con clang; mientras a este ejemplo lo probé con ambos y funciona sin problemas.


En conclusión, comparto este detalle porque aunque una vez conocido es simple, me costó encontrarlo la primera vez. Así de paso también me queda este post a mi como documentación del truco para cuando lo vuelva a necesitar.

1 comentario:

  1. Hoy estaba navegando en algunos blogs sobre programación, y encontre un tutorial con el uso de tu programa. Lo que más me llamó atención fue que puedes expresar un algoritmo totalmente en español, yo ya había pensado en la posibilidad de hacer algo semejante, pero viendo el código fuente creo que ni en mis sueños guajiros hubera podido lograr lo que tu has logrado. Creo que me pondré a estudiar tu código fuente, y quién sabe a lo mejor en el futuro puedo aportar algo importante a tu programa.

    ResponderEliminar