C/C++. Как использовать внедряемые ресурсы приложения, при работе в GCC на Linux

    Захотелось как-то мне использовать в Linux внедряемые ресурсы, причём, автоматически. В общем, задача такая:


    1. Имеется Eclipse проект программы на C++.
    2. ОС: Linux Ubuntu. Компилятор: G++
    3. В проекте используются данные из внешних файлов: строки локализации, SQL-запросы, картинки, звуки и т.д.
    4. Все ресурсы необходимо внедрить в исполняемый файл, ибо программу планируется распространять, как портативную.
    5. Кроме того, хочется, что бы процесс был максимально автоматизирован, ибо лень.

    Для начала, поиск по форумам дал несколько возможных способов решения задачи. Среди найденных наиболее универсальным мне показалась идея использовать параметр «--format=binary» линковщика «ld». Посты на форумах обещали, что команда вида:

    g++ -Wl,--format=binary -Wl,my.res -Wl,--format=default

    прилинкует к приложению файл «my.res» и создаст два символа — _binary_my_res_start и _binary_my_res_end, указывающих, соответственно, на начало и конец тех самых данных, которые были в прилинкованном файле. Следовательно, обращение к данным из C++ можно было бы осуществить как-то так:
    	
    	extern const uint8_t my_res_start[]	asm("_binary_my_res_start");
    	extern const uint8_t my_res_end[]	asm("_binary_my_res_end");
    

    Но не тут-то было. Пишем всё, как надо, а компилятор недоволен. Символа «_binary_my_res_start», видите ли, он найти не может. Ну ничего, nm нам в помощь. Пишем следующую команду:

    nm MyProgramm |grep -w -o -P -e '_binary_[\w\d_]+'

    И получаем:

    
    _binary__home_unknown_workspace_MyProgramm_res_my_res_sql_end
    _binary__home_unknown_workspace_MyProgramm_res_my_res_sql_start
    

    Выходит, что имя символа включает в себя весь путь до него, что, в перспективе, может привести к необходимости постоянного переписывания заголовочного файла, содержащего ссылки на ресурсы. Проблема решается, если в событие PostBuild в настройках проекта Eclipse добавить вызов следующего скрипта:

    #!/bin/bash
    OUTPUT=$1/resources.h
    printf 	'#ifndef __RESOURCES_H__\n' > "$OUTPUT"
    printf 	'#define __RESOURCES_H__\n\n' >> "$OUTPUT"
    printf	'#include <inttypes.h>\n\n' >> "$OUTPUT"
    SYMBOLS=$(nm NewsParser |grep -w -o -P -e '_binary_[\w\d_]+') >> "$OUTPUT"
    VAR_SIZES_LIST=''
    for SYMBOL in $SYMBOLS
    do
    	VAR_NAME=$(echo $SYMBOL | grep -o -P -e 'res_[\w\d_]+'|cut -c 5-)		
    
    	if [[ -z $(echo $SYMBOL|grep _size) ]] 
    	then  
    		printf '\textern const uint8_t '$VAR_NAME'[]\tasm("'$SYMBOL'");\n\n' >> "$OUTPUT"
    	else 
    		START_VAR=$(echo $VAR_NAME|rev|cut -c 5-|rev)'start'
    		END_VAR=$(echo $VAR_NAME|rev|cut -c 5-|rev)'end'
    		VAR_SIZES_LIST=$VAR_SIZES_LIST$(printf '\\tconst uint64_t '$VAR_NAME'\\t=\\t'$END_VAR' - '$START_VAR';\\n\\n')
    	fi
    done
    printf "$VAR_SIZES_LIST" >> "$OUTPUT"
    printf 	'#endif\n' >> "$OUTPUT"
    printf  'File '$OUTPUT' is generated.\n'
    

    Как добавить скрипт «update_resource.sh», лежащий в корне проекта, в событие PostBuild в настройках проекта Eclipse.


    Хорошо. Теперь заголовочный файл будет каждый раз как новенький, а обращаться к данным можно по именам переменных, которые не будут меняться, если только не переименовать сам ресурсный файл. Кроме того, данный скрипт вычисляет размер для каждого ресурса. Не то что бы отнять от указателя на конец указатель на начало было большой проблемой, но, всё же, так удобнее.

    Но это, пока что, не всё. Ведь добавление каждого нового ресурса в проект будет превращаться в форменный АД. И эту проблему также можно решить при помощи скрипта, только уже на этапе линковки:

    
    FLAGS=$1
    OUTPUT_FLAG=$2
    OUTPUT_PREFIX=$3
    OUTPUT=$4
    INPUTS=$5
    RESOURCE_PATH=$6
    RESOURCES=''
    for res_file in $(ls $RESOURCE_PATH/*)
    do
    	RESOURCES=$RESOURCES' '-Wl,$res_file
    	echo 'Ресурс '$res_file' добавлен в сборку' 
    done
    g++ $FLAGS $OUTPUT_FLAG $OUTPUT_PREFIX$OUTPUT $INPUTS  -Wl,--format=binary $RESOURCES -Wl,--format=default 
    

    Как в настройках проекта Eclipse заменить вызов стандартного линковщика на собственный скрипт.


    • Красным на картинке выделено место, в котором вместо стандартной команды вызова линковщика прописан путь к скрипту «link.sh», лежащему в корне проекта.
    • Зелёным на картинке выделено место, в котором к обычным параметрам линковщика добавляется ещё один, который сообщает скрипту расположение каталога с ресурсами.
    • Кроме того, важно не забыть обернуть двойными кавычками остальные параметры, чтобы они случайно не побились пробелами не в том порядке, в котором их ждёт скрипт.


    Отлично. Теперь все файлы, которые лежат в подкаталоге «res», будут сами попадать в ресурсы при каждой сборке.

    Комментарии 20

      0
      В чём смысл добавления ресурсов подобным образом? Можно же просто генерировать c-array с данными ресурса в PreBuild скрипте.
        0
        Хороший способ, но не универсальный. Если у Вас много маленьких файлов, то исходник с ресурсами будет содержать кучи текста, который будет значительно замедлять компиляцию.

        Хотя, если нужно добавить всего один или два файла в ресурсы, то Ваш способ, пожалуй, проще будет.
          0
          Пересобираться данный файл будет только при обновлении файлов-ресурсов — т.е довольно редко.
            0
            Так-то да. Но, всё же, долго. Кроме того, если программа в разработке и делается несколькими людьми, то добавлять ресурсы в неё будут не так уж и редко. И каждое такое добавление заставит всех, кто скачает злополучный коммит, снова ждать пересборки файла ресурсов.
              +1
              Разработка с использованием Qt сопровождается именно таким способом использования ресурсов. Я бы не сказал, что это оказывает сколь-либо заметное влияние на время сборки проекта, особенно на фоне тормозов MinGW (если сборка идет под Windows). Конечно мне не приходилось сталкиваться с проектом, содержащим тысячи файлов ресурсов. Вполне допускаю, что в этом случае будет все как вы говорите.
                0
                Мне приходилось, это решается просто разбиением на отдельные .qrc-файлы, в каждом из которых будет разумное число ресурсов. Будут пересобираться только те сравнительно небольшие файлы, которые изменились (а меняются они намноого реже, чем код).
                  0
                  Тоже хороший вариант, но я, к сожалению, работаю без Qt. Во всяком случае, данный способ я искал именно исходя из требования максимально возможной универсальности.

                  PS: Хотя против Qt я ничего не имею и очень даже его люблю.
                    0
                    Ну да, понятно, что Qt — там много всего, и может быть много лишнего, для вашей какой-то конкретной задачи целый Qt может быть избыточен. Я о самой идее, что можно не один исходник на все ресурсы генерировать, а разбивать их на разные исходники, будут пересобираться только те, которые реально изменились — в ответ вот на это возражение:

                    Если у Вас много маленьких файлов, то исходник с ресурсами будет содержать кучи текста, который будет значительно замедлять компиляцию
                      0
                      И, опять же, Вам нужно будет придумывать удачный принцип разбиения. Я же писал, что данный способ выбран как наиболее универсальный. И как любой универсальный способ он имеет кучу недостатков, при сравнении с более специализированными.

                      Естественно, что для более узких задач можно придумать более удобные способы работы с ресурсами. И эти способы будут и проще и быстрее на своих задачах, но они не будут универсальны.

                      В ходе своих поисков я рассматривал все эти способы и, если это будет кому-то интересно, возможно сделаю в будущем статьи и по другим способам работы с ресурсами в программах для Linux.
        +1

        Если там используется полное имя файла, может имеет смысл завести ишью в gcc и попросить их сделать возможность либо использовать относительные имена, либо явно указывать идентификатор для ресурса? То, что вы там кривыми скриптами правите символы — это ненормально.


        И еще, а нельзя ли сделать секцию в ELF с кастомным именем и засунуть ресурс в нее?

          0
          Явно указывать имя ресурса было бы неплохо. По сути ради этого все эти «кривые скрипты» и написаны. А если бы они ещё и заголовочник для ресурсов генерировали, то вообще офигенно было бы. Но такого функционала я, в частности, в официальных манах не нашёл. Возможно плохо искал.

          PS: И по поводу правки символов: а что Вам конкретно не нравится? Если Вы видите в моих скриптах ошибки или потенциально опасные места, то пожалуйста поделитесь со всеми. Если же Вам просто не нравится использование скриптов, при сборке, то, хочу заметить, что это лишь дело вкуса.
          +5
            0
            Я бы не заморачивался и накидал бы скрипт например на lua что бы он генерировал все необходимые бинарные данные.
            Типа такого
            [gen-res.lua]
            function gen(data,name,h,c)
            	local size,buf_size,buf,w,i,a,la,line
            	size=data:seek("end") data:seek("set",0)
            	h:write("enum { "..name.."_size="..size.." };\n")
            	h:write("extern const unsigned char "..name.."["..name.."_size];\n")
            	c:write("const unsigned char "..name.."[]={\n")
            	a=0 w=16 buf_size=32 buf={}
            	while buf do
            		la=1
            		while la<=#buf do
            			line="\t"
            			for i=1,w do
            				if la>#buf then break end
            				line=line..string.format("%3d",buf:byte(la))
            				a=a+1 la=la+1
            				if a<size then line=line.."," end
            			end
            			c:write(line.."\n")
            		end
            		buf=data:read(buf_size)
            	end
            	c:write"};\n"
            end
            function gen_list(list,hdr,src)
            	local h,c,d
            	hdr=hdr or "res.h" 
            	src=src or "res.c" 
            	h=io.open(hdr,"w+") or error "unable to create header"
            	c=io.open(src,"w+") or error "unable to create source"
            	h:write("// "..hdr.." : autogenerated file\n")
            	h:write"#pragma once\n"
            	for line in io.lines(list) do
            		local name,fn=line:match"%s*(%S+)%s+(.+)%s*"
            		if fn then 
            			d=io.open(fn,"rb") or error("unable to open "..fn)
            			gen(d,name,h,c)
            			d:close()
            		end
            	end
            	c:close()
            	h:close()
            end
            
            gen_list "list.txt"
            

            [list.txt]
            res1 file1.dat
            res2 file2.dat
            res3 file3.dat
            

              0
              А чем не устроил AppImage?
                0
                Хорошая штука. Но я искал максимально универсальный метод. Для изложенного мною метода нужно всего две вещи: GCC и BASH. Всё! Оба есть на любом Linux. Я понимаю, что можно придумать кучи специализированных методов работы с ресурсами, которые будут лучше во всём. Но они не будут универсальны. Мне нужен был именно универсальный метод. Я его нашёл. Подумал, что он ещё кому-то может быть нужен (ну мало-ли).
                +2

                Вы немножко усложнили себе жизнь на ровном месте. Если давать ld относительные адреса и правильно устанавливать cwd, то получаются действительно нормальные символы:


                ~ 
                ➜ ld -r -b binary -o blob.o some/dir/to/blob.png               
                
                ~ 
                ➜ objdump -t blob.o                       
                
                blob.o:     file format elf64-x86-64
                
                SYMBOL TABLE:
                0000000000000000 l    d  .data  0000000000000000 .data
                0000000000000000 g       .data  0000000000000000 _binary_some_dir_to_blob_png_end
                0000000000000000 g       .data  0000000000000000 _binary_some_dir_to_blob_png_start
                0000000000000000 g       *ABS*  0000000000000000 _binary_some_dir_to_blob_png_size

                А если использовать абсолютные, то да, и символы будут с абсолютным путём
                ~ 
                ➜ ld -r -b binary -o blob.o $(realpath some/dir/to/blob.png)
                
                ~ 
                ➜ objdump -t blob.o                                         
                
                blob.o:     file format elf64-x86-64
                
                SYMBOL TABLE:
                0000000000000000 l    d  .data  0000000000000000 .data
                0000000000000000 g       *ABS*  0000000000000000 _binary__home_prok_some_dir_to_blob_png_size
                0000000000000000 g       .data  0000000000000000 _binary__home_prok_some_dir_to_blob_png_end
                0000000000000000 g       .data  0000000000000000 _binary__home_prok_some_dir_to_blob_png_start
                0

                Не совсем в тему, просто забавный факт: GIMP умеет экспортировать картинки в сишный исходник:


                /* GIMP RGB C-Source image dump (test.c) */
                
                static const struct {
                  guint          width;
                  guint          height;
                  guint          bytes_per_pixel; /* 2:RGB16, 3:RGB, 4:RGBA */ 
                  guint8         pixel_data[100 * 100 * 3 + 1];
                } gimp_image = {
                  100, 100, 3,
                  "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377"
                  "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377"
                ...
                  +1
                  Недавно столкнулся с подобной задачей, полностью решил ее используя github.com/vector-of-bool/cmrc
                    0

                    Спасибо! Красивое решение.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое