Как подключить содержимое любых файлов для использования в коде C / C++

Привет, Хабровчане!

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

О чем речь?

Задача состояла в подключении файлов: HTML, JS, CSS; без специальной подготовки. Так же неудобно подключать бинарные файлы (например картинки) конвертируя их в HEX. Так как не хотелось конвертировать в HEX или разделять на строки, искал способ подключения файла в адресное пространство программы.

Как обычно это выглядит

Пример, c разделением строк:

const char text[] =
    "<html>"            "\r\n"
    "<body>Text</body>" "\r\n"
    "</html>";

Пример, с HEX (больше подходит для бинарных данных):

const char text[] =
    {
        0x3C, 0x68, 0x74, 0x6D, 0x6C, 0x3E, 0x0A, 0x3C,
        0x62, 0x6F, 0x64, 0x79, 0x3E, 0x54, 0x65, 0x78,
        0x74, 0x3C, 0x2F, 0x62, 0x6F, 0x64, 0x79, 0x3E,
        0x0A, 0x3C, 0x2F, 0x68, 0x74, 0x6D, 0x6C, 0x3E,
        0
    };

Видел даже такое:

#define TEXT "<html>\r\n<body>Text</body>\r\n</html>"
const char text[] = TEXT;

Все #define располагались в отдельном .h файле и подготавливались скриптом на Python. С аннотацией, что некоторые символы должны быть экранированы \ вручную в исходном файле. Честно немного волосы дыбом встали от такого мазохизма.

А хотелось, чтобы файлы можно было спокойно редактировать, просматривать и при компиляции всё само подключалось и было доступно, например так:

extern const char text[];

Оказалось всё просто, несколько строчек в Assembler.

Подключаем файл в Arduino IDE

Добавляем новую вкладку или создаём файл в папке проекта с названием text.S, там же размещаем файл text.htm.

Содержимое файла text.htm:

<html>
<body>Text</body>
</html>

Содержимое файла text.S:

.global text
.section .rodata.myfiles
text:
    .incbin "text.htm"
    .byte 0

Не забываем нулевой символ \0 в конце, он здесь в строке сам не добавится.

Сам скетч:

extern const char text[] PROGMEM;

void setup()
{
    Serial.begin(115200);
    Serial.println(text);
}

void loop() { }

Компилируем, загружаем и смотрим вывод:

Отлично, когда-то бы я от радости прыгал до потолка, от того что всё получилось.

Код работает в AVR8, но например в ESP8266 получим аппаратный сбой. Всё потому, что чтение из Flash доступно по 32 бита и по адресам кратным 32 бит. Чтобы было всё хорошо, каждому файлу требуется делать отступ для кратности, код будет выглядеть так:

.global text
.section .rodata.myfiles

.align 4
text:
    .incbin "text.htm"
    .byte 0

Загрузить можно в секцию кода: .irom.text, если не хватает места в .rodata.

Для STM32 так же рекомендуется выравнивать по 32 бита, но не обязательно.

А как записать размер данных во время компиляции? Например, для бинарных данных не получится остановится по нулевому символу. Так же просто:

.global text, text_size

.section .rodata.myfiles

text:
    .incbin "text.htm"
    text_end:
    .byte 0

text_size:
	.word (text_end - text)

Объявление:

extern const char text[] PROGMEM;
extern const uint16_t text_size PROGMEM;

Осталось написать макрос, для удобства подключения файлов:

.macro addFile name file
    .global \name, \name\()_size
//  .align 4
    \name:
        .incbin "\file"
        \name\()_end:
        .byte 0
//  .align 4
    \name\()_size:
        .word (\name\()_end - \name)
.endm

.section .rodata.myfiles

addFile text1   1.txt
addFile text2   2.txt
addFile text3   3.txt

И макрос для объявления:

#define ADD_FILE(name) \
    extern const char name[] PROGMEM; \
    extern const uint16_t name##_size PROGMEM;

ADD_FILE(text1);
ADD_FILE(text2);
ADD_FILE(text3);

void setup()
{
    Serial.begin(115200);
    Serial.println(text1);
    Serial.println(text1_size);
    Serial.println(text2);
    Serial.println(text2_size);
    Serial.println(text3);
    Serial.println(text3_size);
}

void loop() { }

Вывод:

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

Подключаем любой файл и не только, в GNU toolchain

Принцип тот же самый, ничем не отличается для Arduino. В принципе в Arduino используется тот же toolchain от Atmel.

Только здесь у нас в руках Makefile и мы можем до компиляции и сборки запустить какой-нибудь скрипт.

Для примера возьму код из готового моего проекта на STM32, где автоматически при компиляции увеличивается версия сборки. Так же включаются в проект WEB-интерфейс для последующего использования в LWIP / HTTPD.

Скрипт version.sh:

#!/bin/bash
# Version generator
# running script from pre-build

MAJOR=1
MINOR=0

cd "$(dirname $0)" &>/dev/null

FILE_VERSION="version.txt"
FILE_ASM="version.S"

BUILD=$(head -n1 "$FILE_VERSION" 2>/dev/null)
if [ -z "$BUILD" ]; then
	BUILD=0
else
	BUILD=$(expr $BUILD + 1)
fi
echo -n "$BUILD" >"$FILE_VERSION"

cat <<EOF >"$FILE_ASM"
/**
* no editing, automatically generated from version.sh
*/

.section .rodata
.global __version_major
.global __version_minor
.global __version_build
__version_major: .word $MAJOR
__version_minor: .word $MINOR
__version_build: .word $BUILD
.end
EOF

cd - &>/dev/null
exit 0

Создаётся файл version.S в который из version.txt загружается номер версии предыдущей сборки.

В Makefile добавляется цель pre-build:

#######################################
# pre-build script
#######################################
pre-build:
	bash version.sh

В цель all надо дописать pre-build:

all: pre-build $(BUILD_DIR)/$(TARGET).elf $(BUILD_DIR)/$(TARGET).hex $(BUILD_DIR)/$(TARGET).bin

Объявление и макросы для printf у меня в macro.h:

extern const uint16_t __version_major;
extern const uint16_t __version_minor;
extern const uint16_t __version_build;
#define FMT_VER             "%u.%u.%u"
#define FMT_VER_VAL         __version_major, __version_minor, __version_build

В HTTPD из LWIP немного был удивлён когда увидел, что содержимое файлов надо хранить вместе с заголовками HTTP. Чтобы не менять архитектуру, загрузку делал как это организовано в примере fsdata.c. Использовал fsdata_custom.c, для этого установлен флаг HTTPD_USE_CUSTOM_FSDATA.

Код в fsdata_custom.c:

#include "lwip/apps/fs.h"
#include "lwip/def.h"
#include "fsdata.h"
#include "macro.h"

extern const struct fsdata_file __fs_root;

#define FS_ROOT &__fs_root

Сборка файлов fsdata_make.S:

.macro addData name file mime
	\name\():
	.string "/\file\()"
	\name\()_data:
	.incbin "mime/\mime\().txt"
	.incbin "\file\()"
	\name\()_end:
.endm

.macro addFile name next
	\name\()_file:
	.word \next\()
	.word \name\()
	.word \name\()_data
	.word \name\()_end - \name\()_data
	.word 1
.endm

.section .rodata.fsdata
.global __fs_root

/* Load files */
addData __index_htm         index.htm           html
addData __styles_css        styles.css          css
addData __lib_js            lib.js              js
addData __ui_js             ui.js               js
addData __404_htm           404.htm             404
addData __favicon_ico       img/favicon.ico     ico
addData __logo_png          img/logo.png        png

/* FSDATA Table */
addFile __logo_png          0
addFile __favicon_ico       __logo_png_file
addFile __404_htm           __favicon_ico_file
addFile __ui_js             __404_htm_file
addFile __lib_js            __ui_js_file
addFile __styles_css        __lib_js_file
__fs_root:
addFile __index_htm         __styles_css_file

.end

В начале каждого файла загружается заголовок, пару примеров из папки mime.

Файл html.txt:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Connection: close

Файл 404.txt:

HTTP/1.1 404 Not found
Content-Type: text/plain; charset=UTF-8
Connection: close

Нужно обратить внимание на пустую строку, чего требует спецификация HTTP для обозначения конца заголовка. Каждая строка должна заканчиваться символом CRLF (\r\n).

P.S. Код проекта из ветхого сундука, так что в реализации мог забыть чего ни будь уточнить.

В завершении

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

Спасибо за внимание, удачных разработок!

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

    +5
    Для CMake есть готовое решение CMakeRC
    Подробнее можно почитать тут: A Resource Compiler with CMake and Standard C++
      +1
      Интересный проект. Но автору такое скорей всего не подойдет.
      Судя по коду, этот CMakeRC задействует пол стандартной библиотеки.
      Что конечно удобно, но просто не влезет в AVR.
        0
        Автору точно не подойдет. Я понадеялся, что он в принципе кому-то может быть полезен.
      +8

      Решение интересное, но, кажется, что некоторых кейсах это проще сделать через xxd.


      xxd --include filename

      выведет что-то вроде:


      unsigned char filename[]={ 0x48, ...}; 
      unsigned int filename_len = 123;
        +3

        Для небольших файлов. Если файл несколько Мб, то зачем лишний раз напрягать компилятор? (И хранить сам файл в двух вариантах: оригинал и с-массив.) Лучше сделать через objcopy, как замечено в комментарии ниже. Да и xdd нет под виндой из коробки.

          +1

          Особенно это хорошо для эмбеддеда, потому что он не дописывает const. В результате полмегабайтный файл пытается влезть в оперативку. И руками этот const приходится дописывать

          +9

          Я пару раз делал подключал бинарные файлы через objcopy --input-target binary
          Быстрый гуглинг так же предлагает https://github.com/graphitemaster/incbin.


          А вообще — ждем принятия соответствующего Proposal и облизываемся на растовый std::include_bytes

            +1
            Еще один вариант — сначала перевести в *.o, а потом подключать как любой другой объектник:
            $%.o: $src/example.img
            mkdir -p $(builddir)
            arm-none-eabi-ld -r -b binary -o $@ $<
            arm-none-eabi-objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents $@ $@

            extern const char *data_start = asm("_binary_src_fatexample_img_start");
            extern const char* data_end = asm("_binary_src_fatexample_img_end");
              0
              Делал аналогично.

              ld -r -b binary demo.ui -o demo_ui.o


              // from demo.ui
              extern const char _binary_demo_ui_start[];
              extern const char _binary_demo_ui_end[];
              
                0
                Преобразование из .data секции в .rodata все же важный шаг.
              0

              Насчёт микроконтроллеров: во время работы контроллера данные хранятся на flash или целиком загружаются в RAM? У меня почему-то получалось, что при объявлении в коде статического массива с данными на stm32 тот грузился целиком в оперативку и это меня не очень устраивало.

                +1
                Смотря, как объявить. Если массив объявить константным (static const xxx[] = {...}), то он не будет перегружаться в ОЗУ.
                  +1

                  Для разных микроконтроллеров по-разному. Для stm32 вам ответили, а, например, для avr надо кроме этого добавлять к определению массива attribute(progmem) или что-то ещё, смотря какой компилятор.

                    +1

                    Секция .rodata у ARM Cortex M, как правило, располагается во флеше, а .data — в оперативке. Но если у вас что-то чуть сложнее микроконтроллера, то правила линковки и загрузки могут быть уже другими (к примеру, если программа грузится не с eXecute In Place-накопителя)

                    0

                    А для js/ts есть не велосипедный способ сделать такое? Интересует вкомпиляция (например) картинки в бинарном виде. В golang завезли embed на туже тему.

                      0

                      Если используете бандлер, например webpack, то да.

                        +2

                        rust+webassembly, а дальше уже используешь из ts/js
                        :D

                        +5

                        Кстати говоря, я еще год назад рассказывал[1], что в следующем стандарте препроцессор C будет, вероятно, поддерживать такое включение директивой embed [2].


                        [1] Статья про преполагаемые нововведения
                        [2] Последняя итерация предложения в комитете

                          0
                          А что про С++? Означает ли добавление директивы в С, что в С++ её тоже будут должны поддержать?
                        0
                        В Sonic Adventure эксешник больше 100Мб был в 2004 году, там все модели уровней и пропов лежат, кроме персонажей.
                          –6
                          Что то типа json_encode (для php, или JSON.stringify дляjavascript) использовать не хотите?
                          Эти методы сами экранирует 'опасные' символы, делая очень эффективную строку (с оговорками для не латинских символов и неправильного выбора кодировки), совместимую с C-языками.

                          p.s. objcopy добавляет данные в obj файл

                            +1

                            Главная проблема перечисленных вами функций — в том, что они работают с юникодовыми строками, т.е. последовательностями символов — в то время как бинарные данные являются последовательностью байт. И не любую последовательность байт можно интерпретировать как последовательность символов для кодирования.


                            Вторая проблема такого кодирования — в том, что оно ничуть не эффективно, формат \uNNNN требует шести байт для представления двух.


                            с оговорками для не латинских символов

                            Таких "символов" в бинарных данных будет половина.

                              0

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


                              Можно обойти проблему загнав всё в hex, от которого автор успешно ушёл.

                            0
                            Интересно, а для обычной Visual Studio такая возможность имеется?
                            PS Вообще фича вставки бинарников весьма очевидна, странно что еще в Си ее не реализовали. В языке D кстати такая возможность добавлена официально.
                                0

                                Я использую objcopy как некое кроссплатформенное решение, но нужно установить objcopy на винду.


                                Про очевидность :) Для вас — очевидно, для меня — очевидно, но я поспрашивал знакомых с/с++ разработчиков, оказываается для многих совершенно не очевидно :) следуют вопросы: а зачем? а почему в файлах не устраивает? и тп :)


                                И я тоже не понимаю почему возможность добавлять бинарные данные в .data и в .rdata секции не добавлена в С. Хотя может считали избыточным, что есть другие тулы для этого.

                                0

                                del

                                  +4
                                  А получится ли обойтись просто string literal?
                                  По крайней мере, в с+11 всякие экзотические строки можно так задавать:
                                  const auto str = 
                                  R"(
                                     first line blabla #~\/&$
                                     second line blabla
                                  )";
                                    

                                    +1

                                    Мы у себя именно так и сделали.
                                    Тоже смотрели на ресурсы и xxd.
                                    Но ресурсы — это всё же не от языка, а внешнее (нет желания напороться на формат экзешников, где они не предусмотрены). xxd — да, но с "сырыми" литералами он тоже выглядит излишне (к тому же требует установки).
                                    В итоге "навелосипедили" скрипт для cmake, который нужные файлы кодирует в один или несколько литералов. которые потом в основной сборке подключаются через #include.


                                    (в один или несколько — потому что эти самые "экзотические строки" всё же имеют ограничения. Под виндой — 65к на литерал).

                                    –1
                                    Если просто положить бинарник — то можно и так.
                                    А если нужно посчитать CRC всей секции данных (а то и всех секций сразу) и положить его в бинарник, то кроме как посчитать CRC в python-скрипте и пропатчить бинарник — ничего не придумывается.
                                      +2
                                      вот такой способ ещё встречал
                                      ld -r -b binary file.txt -o file.o
                                      а в остальных файлах можно использовать
                                      extern char _binary_file_txt_end[];
                                      extern char _binary_file_txt_start[];
                                      
                                        –1
                                        решение хорошее, но на практике получается что мелкий html практически пофиг как подключать, а вот большой код отдается сжатым типа index.html.gz. Да еще если используется какой либо фреймворк, то его подключать в исходник удобней только при сборке прошивки… и получается что все равно вызывать некий пребилд и смысла особого нет.

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

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