1

Is there a native way in CMake to write an int to a file as bytes?

E.g.

file(SIZE generated_file.txt file_size)  # file_size == 465435

file(WRITE output.txt "${file_size}"

Where I would like ${file_size} written as a fixed number of bytes (E.g. 4) \x00\x07\x1a\x1b vs 465435

starball
  • 20,030
  • 7
  • 43
  • 238
Matt Stokes
  • 4,618
  • 9
  • 33
  • 56

2 Answers2

2

CMake doesn't have integer types. It just has strings. If you want to encode a non-ASCII number literal as a CMake string, the docs for escape sequences in CMake strings is here: https://cmake.org/cmake/help/latest/manual/cmake-language.7.html#escape-sequences. There seems to be no way to write binary/hex literals other than to actually put those characters in the string (and escape them if necessary- Ex. \").

Another approach would be to invoke a program that converts the ASCII decimal string to the integer value and writes it to the file, or outputs the integer to the standard output stream and then use the mechanism of whatever CMake command you use to invoke that program to get the standard output as a CMake string and then call file(WRITE ...). To do that at configure-time, see execute_process. To do it at build-time, see add_custom_command or add_custom_target.

starball
  • 20,030
  • 7
  • 43
  • 238
2

CMake has a command string(ASCII) which can be used for create a byte with the given code. But for create several bytes from a single number, splitting this number into codes should be performed manually.

CMake has not so great support for arithmetic and handling characters in a string, so that seemingly a simple task - splitting a number into hex codes - requires some code:

# Variable which contain a single null-byte.
#
# CMake doesn't allow to create null-byte in the code.
# E.g. using "\0" emits error "Invalid character escape '\0'."
# For that reason we pre-create a file which contains only a single null-byte
# and read that file in CMake.
#
# A file 'null.txt' could be created using this command:
#
#   printf "\0" > null.txt
#
file(READ "null.txt" null_char)

# write_number_to_file(<number> <bytes_count> <filename>)
#
# Writes given number into the file as a sequence of bytes.
#
# Arguments:
# - number - number to write.
#          Could be a decimal, a hexadecimal (prefixed with 0x)
#          or even an expression which is understandable by math(EXPR).
#
# - bytes_count - number of bytes which should be written.
#           If bytes count is more then bytes required to represent a number,
#           the first bytes will be zero.
#
# - filename - path to the file where to write the number.
#
function(write_number_to_file number bytes_count filename)
    # Convert number to hex
    math(EXPR number_hex "${number}" OUTPUT_FORMAT HEXADECIMAL)
    # .. and strip '0x' prefix
    string(SUBSTRING "${number_hex}" 2 -1 number_hex)
    # Compute number of the hex digits in the number.
    string(LENGTH "${number_hex}" hex_digits_count)

    # Maximum number of hex digits, which can be representable by the requested bytes count.
    math(EXPR hex_digits_max_count "${bytes_count} * 2")

    # Perform the padding of hex number up to maximum digits

    # Number of bytes to pad.
    math(EXPR pad_len "${hex_digits_max_count} - ${hex_digits_count}")
    # A string with zeros which will prepend the number.
    string(REPEAT "0" ${pad_len} pad_str)
    # Pad the number.
    string(CONCAT number_hex ${pad_str} ${number_hex})

    # Bytes which will be written into the file
    set(bytes)

    # Iterate over pairs of hex digits, every of which will produce a single byte
    # 1..<bytes_count>
    foreach(i1 RANGE 1 ${bytes_count})
        # Index of the first digit in the pair.
        math(EXPR first_digit_index "${i1} * 2 - 2")
        # Hexadecimal code of the created byte.
        string(SUBSTRING "${number_hex}" ${first_digit_index} 2 byte_hex)

        if (byte_hex STREQUAL "00")
            # Special case: the code is 0, but CMake cannot create null-byte.
            string(CONCAT bytes "${bytes}" "${null_char}")
        else()
            # Regular case: non-0 code.
            # Covert the code into decimal, as string(ASCII) doesn't understand hex.
            math(EXPR byte_code "0x${byte_hex}")
            # Create a byte with the given ASCII code.
            string(ASCII ${byte_code} byte)
            string(CONCAT bytes "${bytes}" "${byte}")
        endif()
    endforeach()
    # Now everything is ready to write the file.
    file(WRITE "${filename}" "${bytes}")
endfunction()

The defined function write_number_to_file could be used as follows:

file(SIZE generated_file.txt file_size)  # file_size == 465435

# Write a number as 4 bytes into the specified file
write_number_to_file("${file_size}" 4 output.txt)

It would be more convenient to have a function which returns bytes representation of a number. So that bytes can be written to the file not only with file(WRITE) but with file(APPEND) or other methods. Unfortunately, CMake badly handles null bytes in the variables, and potential call from the function write_number_to_file

   set(outer_var "${bytes}" PARENT_SCOPE)

would set the outer_var ... to the empty string (because the first byte in bytes variable is null).

Tsyvarev
  • 60,011
  • 17
  • 110
  • 153