Menu Share

How to Embed Files in Your C++ Project

by | Published on
category CMake

In this article I am going to explore a way that you can use to embed files in your C++ project. This is usually useful for some small text files that always need to be there when your application is executing. The idea is that you won’t be loading files from the filesystem but instead have them loaded in your application always.

Table of Contents

Working with CMake

To achieve this you have to work with the build system. I am most fond of CMake which is the most popular build system C and C++ projects so this is what I am going to showcase here. This is because the build system will be responsible for copying the file contents into your C++ code so that it gets embedded.

If you are interested more in CMake you can check out my Udemy course on the topic.

What is it to embed a file?

Essentially what you want to achieve is to take the bytes and convert them into an array in C++ that is a static variable. This means that your code will grow in size by the size of that file. These bytes will become part of the library or executable. So you must be careful what files you select to embed into the project. It is appropriate if you want to embed some shaders for graphical programming for example since you almost always need those and rarely you would want to load them every time from disk. They are simple textual code which makes them perfect. Keep in mind that when you embed a file – this file cannot be modified after the build. It is more or less like hardcoding a variable.

How to embed a file with CMake?

To embed a file in CMake I have created myself a simple function. You can put this in a nice “embed.cmake” module so that you can use it at any time and with any project. And here is my solution:

function(target_embed_files TARGET_NAME)
    set(OPTIONS)
    set(SINGLE_VALUE)
    set(MULTIPLE_VALUE FILES)

    cmake_parse_arguments(
        EF
        "${OPTIONS}"
        "${SINGLE_VALUE}"
        "${MULTIPLE_VALUE}"
        ${ARGN}
    )

    string(TOUPPER "${TARGET_NAME}" TARGET_NAME_UPPER)
    string(MAKE_C_IDENTIFIER "${TARGET_NAME_UPPER}" TARGET_NAME_UPPER)

    string(TOLOWER "${TARGET_NAME}" TARGET_NAME_LOWER)
    string(MAKE_C_IDENTIFIER "${TARGET_NAME_LOWER}" TARGET_NAME_LOWER)

    set(GENERATE_HEADER_LOCATION "${CMAKE_BINARY_DIR}/embed/include")
    set(EMBED_HEADERS_LOCATION "${GENERATE_HEADER_LOCATION}/embed")
    set(HEADER_FILENAME "${TARGET_NAME_LOWER}.h")
    set(ARRAY_DECLARATIONS)

    foreach(INPUT_FILE IN LISTS EF_FILES)
        get_filename_component(FILENAME "${INPUT_FILE}" NAME_WE)
        string(TOLOWER "${FILENAME}" FILENAME)
        string(MAKE_C_IDENTIFIER "${FILENAME}" FILENAME)

        file(READ "${INPUT_FILE}" BYTES HEX)
        string(REGEX REPLACE "(..)" "0x\\1, " BYTES "${BYTES}")

        string(CONFIGURE "const std::uint8_t ${FILENAME}[] = { ${BYTES} }\;" CURRENT_DECLARATION)

        list(APPEND ARRAY_DECLARATIONS "${CURRENT_DECLARATION}")
    endforeach()

    string(JOIN "\n" ARRAY_DECLARATIONS ${ARRAY_DECLARATIONS})

    set(BASIC_HEADER "\
#ifndef ${TARGET_NAME_UPPER}_EMBED_FILES
#define ${TARGET_NAME_UPPER}_EMBED_FILES

#include <cstdint>

${ARRAY_DECLARATIONS}

#endif
    ")

    string(CONFIGURE "${BASIC_HEADER}" BASIC_HEADER)

    file(WRITE "${EMBED_HEADERS_LOCATION}/${HEADER_FILENAME}" "${BASIC_HEADER}")
    target_include_directories(${TARGET_NAME} PUBLIC "${GENERATE_HEADER_LOCATION}")
endfunction()

You will notice that I follow a way of writing this that is similar to how the official Modern CMake handles things. We configure embedded files for a single target. This is done by getting each file, reading it as hexadecimal bytes then putting an “\x” before each pair of hex characters. Then this is inserted into a header file and saved in the binary directory. This directory where it is saved is then added as an include path for the target.

To use the above code in your cmake you have to add something like this:

add_executable(demo include/main.h src/main.cpp)
tareget_include_directories(demo PRIVATE include)
target_embed_files(demo FILES shader.glsl)

And here is an example usage in my main.cpp here:

#include <main.h>
#include <embed/demo.h>
#include <iostream>

int main(int argc, char** argv)
{
    std::cout << shader[0] << std::endl;
    return 0;
}

Conclusion

Well this is it about how to embed files into your next project. I hope you find this article useful and if you want you can check out more articles about C++ or CMake on this blog.

Leave a comment

Your email address will not be published. Required fields are marked *