Захотелось как-то мне использовать в 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)


  1. m0xf
    22.09.2019 19:38

    В чём смысл добавления ресурсов подобным образом? Можно же просто генерировать c-array с данными ресурса в PreBuild скрипте.


    1. JBMurloc Автор
      22.09.2019 20:13

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

      Хотя, если нужно добавить всего один или два файла в ресурсы, то Ваш способ, пожалуй, проще будет.


      1. phantom-code
        22.09.2019 21:38

        Пересобираться данный файл будет только при обновлении файлов-ресурсов — т.е довольно редко.


        1. JBMurloc Автор
          22.09.2019 21:58

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


          1. phantom-code
            22.09.2019 22:03

            Разработка с использованием Qt сопровождается именно таким способом использования ресурсов. Я бы не сказал, что это оказывает сколь-либо заметное влияние на время сборки проекта, особенно на фоне тормозов MinGW (если сборка идет под Windows). Конечно мне не приходилось сталкиваться с проектом, содержащим тысячи файлов ресурсов. Вполне допускаю, что в этом случае будет все как вы говорите.


            1. KanuTaH
              22.09.2019 22:10

              Мне приходилось, это решается просто разбиением на отдельные .qrc-файлы, в каждом из которых будет разумное число ресурсов. Будут пересобираться только те сравнительно небольшие файлы, которые изменились (а меняются они намноого реже, чем код).


              1. JBMurloc Автор
                22.09.2019 22:49

                Тоже хороший вариант, но я, к сожалению, работаю без Qt. Во всяком случае, данный способ я искал именно исходя из требования максимально возможной универсальности.

                PS: Хотя против Qt я ничего не имею и очень даже его люблю.


                1. KanuTaH
                  22.09.2019 22:52

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

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


                  1. JBMurloc Автор
                    22.09.2019 23:00

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

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

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


  1. Sabubu
    22.09.2019 22:35

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


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


    1. JBMurloc Автор
      22.09.2019 22:46

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

      PS: И по поводу правки символов: а что Вам конкретно не нравится? Если Вы видите в моих скриптах ошибки или потенциально опасные места, то пожалуйста поделитесь со всеми. Если же Вам просто не нравится использование скриптов, при сборке, то, хочу заметить, что это лишь дело вкуса.



  1. kovserg
    22.09.2019 23:27

    Я бы не заморачивался и накидал бы скрипт например на 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
    


  1. vdudouyt
    23.09.2019 07:34

    А чем не устроил AppImage?


    1. JBMurloc Автор
      23.09.2019 07:46

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


  1. Prototik
    23.09.2019 12:44
    +1

    Вы немножко усложнили себе жизнь на ровном месте. Если давать 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


    1. atrosinenko
      23.09.2019 13:40

      -


  1. atrosinenko
    23.09.2019 13:41

    Не совсем в тему, просто забавный факт: 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. SONce
    23.09.2019 14:21

    Недавно столкнулся с подобной задачей, полностью решил ее используя github.com/vector-of-bool/cmrc


    1. Kvento
      23.09.2019 20:11

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