In my previous post, I described a project that I’ve been (very slowly) working on. One of the major aspects of the project was that it would be cross-platform and use the CMake build system – both of which are new to me in this project.
One obstacle that I ran into was that I wanted my application to be able to leverage a JSON schema file for validating the contents of the configuration files used by the project. Unfortunately, simply deploying the schema file alongside the executable has a couple of drawbacks: first, because the schema is deployed separately, it opens the opportunity for a user to accidentally modify or delete the file, in which case the application won’t work. Second, I don’t know of a bulletproof, cross-platform way of making the an executable reference another file in a location relative to the executable given that there is no guarantee of the working directory during execution.
That said, I thought that a better approach might be to embed the resource directly into the executable. There are some platform-specific ways of doing this, like using Microsoft’s resource compiler or GNU’s objcopy. I was hoping, though, that CMake offered some way of abstracting this. While CMake doesn’t appear to have any built-in functionality for doing this directly, I found a very useful post on Stack Overflow describing a clever way to accomplish this. The basic jist of it is that the function generates a C source file that contains the resource data as a byte array. This is done by using the CMake file() READ function with the HEX option to read the input file and then applying some regular expressions to generate the array contents. The file() function is used again to write the boilerplate C syntax in addition to the hex data to a new C source file, which can then be included in the CMake build.
I really liked this approach because it is completely platform-independent, relying only on CMake commands, and it is simple both conceptually and in usage. However, I ended up adapting this approach further for a few reasons:
- I wanted to separate declaration and definition and use include guards to prevent multiple inclusion
- I wanted to isolate these resource variables into their own C++ namespace
- I did not want to pollute the source tree with generated files
The first couple of bullets directly relate to the content of the generated source files. Between breaking out the content into two separate files with #include directives, and the additional lines for namespacing, it would have made the CMake function a bit ugly. A more elegant approach, I thought, was to use CMake’s configure_file() function to just take templates for the C++ header and source files, and have CMake fill in the blanks for the variable names and resource data. The last bullet relates to where things live in relation to the source and build trees. In the original implementation from Stack Overflow, it was left to the caller of the function to decide where the output files would be generated. It would be very easy to naively write the output files directly to the source tree, which would (in my opinion) be a bad idea. In contrast, the use of configure_file() inherently puts the generated files into the build tree in the CMAKE_CURRENT_BINARY_DIR. This is great since it keeps the source tree clean, but it requires some extra care when specifying the files to be added to the library/executable, in that the generated files must be referenced from their location in the build tree.
In my project, I have a subdirectory dedicated to generating resources (even though at the moment I only have the one – “schema.json”). The following is the CMakeLists.txt file that I use in that directory. It translates the “schema.json” file (and could, with some additional calls to create_resource, any number of other files that I might want to embed) into the source files ${CMAKE_CURRENT_BINARY_DIR}/Schema.h and ${CMAKE_CURRENT_BINARY_DIR}/Schema.cpp, adds the .cpp file to a static library corresponding to this subdirectory, and adds the generated headers to the set of include directories used when compiling against this library:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# The following function was inspired by (though now vastly differs from): # https://stackoverflow.com/a/27206982o # Creates files defining a C-style byte array corresponding to the contents # of the specified file. # # The RESOURCE_PATH argument specifies the file to be encoded # The RESOURCE_NAME argument must be a valid C variable name function(create_resource RESOURCE_PATH RESOURCE_NAME) # Read data from resource file into hex string file(READ ${RESOURCE_PATH} filedata HEX) # Convert hex string into C array syntax string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," RESOURCE_BYTE_ARRAY ${filedata}) # Create .h and .cpp files from the templates configure_file(Resource.h.in ${RESOURCE_NAME}.h) configure_file(Resource.cpp.in ${RESOURCE_NAME}.cpp) endfunction() # Specify the files to turn into resources create_resource("schema.json" "Schema") # Compile the resources into a static library file (GLOB RESOURCE_SOURCES "${CMAKE_CURRENT_BINARY_DIR}/*.cpp") include_directories (${CMAKE_CURRENT_BINARY_DIR}) add_library (resources STATIC ${RESOURCE_SOURCES}) |
For completeness, here are the Resource.h.in and Resource.cpp.in templates, respectively, referenced in the above:
1 2 3 4 5 6 7 8 9 10 |
#ifndef ${RESOURCE_NAME}_H #define ${RESOURCE_NAME}_H namespace Resources { extern const unsigned char ${RESOURCE_NAME}[]; extern const unsigned int ${RESOURCE_NAME}Size; } #endif |
1 2 3 4 5 6 7 8 9 10 11 |
#include "${RESOURCE_NAME}.h" namespace Resources { const unsigned char ${RESOURCE_NAME}[] = { ${RESOURCE_BYTE_ARRAY} }; const unsigned int ${RESOURCE_NAME}Size = sizeof(${RESOURCE_NAME}); } |