Baking a binary in a header with CMake (part 1)

When using CMake in projects for both my personal and professional life – there is always various tools I want to be able to use. One such tool is to take a binary file, and embed it as a const char array in a C/C++ header. The problem is – I often use CMake and cross-compile using CMake’s toolchain mechanism. The issue is – when you cross compile, any tools you’ve built as part of the build are built for the cross-compiled target! No good when you need to use these tools within the very build you are executing.

After working round this problem for a number of years (usually by building for host first and using those tools in the cross compiled build) I discovered CMake’s script mode, whereby you call CMake with the ‘-P’ option and provide a CMake file to execute. With CMake’s script mode, I can add a custom target within CMake that will use this ‘-P’ option as part of the normal build process to create the header, I can ensure that if someone changes the binary file I am embedding that it is tracked and the header is regenerated, and all of this works for cross-compiled targets too!

Once I understood a little more about the CMake ‘-P’ mode, I then created this CMake script file I’ve named ‘binarybaker.cmakescript’ – the *script suffix isn’t required, but I like it as its very descriptive of how I intend to use this file;

if(NOT DEFINED BINARYBAKER_INPUT_FILE)
  message(FATAL_ERROR
    "Required cmake variable BINARYBAKER_INPUT_FILE not set!"
  )
endif()

if(NOT DEFINED BINARYBAKER_OUTPUT_FILE)
  message(FATAL_ERROR
    "Required cmake variable BINARYBAKER_OUTPUT_FILE not set!"
  )
endif()

if(NOT DEFINED BINARYBAKER_VARIABLE_NAME)
  message(FATAL_ERROR
    "Required cmake variable BINARYBAKER_VARIABLE_NAME not set!"
  )
endif()

First off, I wrote some malformed parameter detecting code – I wanted to be sure I passed in the correctly named arguments to my script!

if(NOT EXISTS ${BINARYBAKER_INPUT_FILE})
  message(FATAL_ERROR "File '${BINARYBAKER_INPUT_FILE}' does not exist!")
endif()

Next, I check that the input file specified actually exists!

file(READ "${BINARYBAKER_INPUT_FILE}" contents HEX)

The main revelation that made me realise I could bake a binary in CMake was that you can read a file using the HEX option – meaning we can meaningfully read a binary! This creates in the variable contents one long string akin to “AE12DFEA123…”

string(TOUPPER "${BINARYBAKER_OUTPUT_FILE}" header_ifndef)
string(REGEX REPLACE "[^A-Z]" "_" header_ifndef "${header_ifndef}")
set(header_ifndef "__${header_ifndef}__")

file(WRITE "${BINARYBAKER_OUTPUT_FILE}" "#ifndef ${header_ifndef}\n")
file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#define ${header_ifndef}\n")

Next, we need to generate an include guard. I convert the filename to UPPER, and then replace any characters in the filename that aren’t A-Z with underscores.

file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#ifdef __cplusplus\n")
file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "extern \"C\" {\n")
file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#endif\n")

file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "const char "
  "${BINARYBAKER_VARIABLE_NAME}[] = {")

Insert extern “C” for C++ to work as expected, and output the name of the array.

string(LENGTH "${contents}" contents_length)
# Need to minus one, as the foreach will go over the end of our var otherwise!
math(EXPR contents_length "${contents_length} - 1")

foreach(iter RANGE 0 ${contents_length} 2)
  string(SUBSTRING ${contents} ${iter} 2 line)
  file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "0x${line},\n")
endforeach()

The juicy bit, where we actually output the file! First up, we get the length of the hex string we have. We then minus one from this, as CMake will go over the end of the upcoming loop if we don’t (this caused me a good 10 minutes of headscratching!). Then, loop over the length of the file in 2 steps, substring to read the two characters we are currently at in the loop, and then append these to the output file.

file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "};\n")

file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#ifdef __cplusplus\n")
file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "}\n")
file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#endif\n")

file(APPEND "${BINARYBAKER_OUTPUT_FILE}" "#endif//${header_ifndef}\n\n")

And lastly the final code output to the resultant header to make it compile with C/C++.

So now we have our CMake script, and it processes our file! The only issue is its not the fastest.

+ ls -l -h medium.jpg
-rw-r--r--@ 1 neil  staff   256K  8 Dec 23:06 medium.jpg
+ cmake -DBINARYBAKER_INPUT_FILE=medium.jpg -DBINARYBAKER_OUTPUT_FILE=medium.jpg.h -DBINARYBAKER_VARIABLE_NAME=medium -P binarybaker.cmakescript
1m14.688s

So we are processing around 3.43kB/s – not very fast indeed!

In my next blog, I’ll take you through the rather random steps I took to optimise this script so that it ran in a sane amount of time!

Advertisements

One thought on “Baking a binary in a header with CMake (part 1)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s