← All Articles

Generate Source Files With CMake

Posted on

Sometimes when writing c++ programs I have some textual data that I want to use in the program that I want to be part of the executable instead of in flat files next to the executable. However while developing it is far easier to have the textual data in separate files - instead of in source files as strings.

But then you suddenly have the issue, how do you create c++ strings out of textfiles at runtime? A quickly hacked c++ program and some cmake magic off course.

This c++ was hacked together within thirty minutes or so.

#include <cstdio>
#include <cstdlib>
#include <string>
#include <cstdarg>
#include <dirent.h>

using namespace std;

std::string working_path;
FILE* fp_in = 0;
FILE* fp_out = 0;


void out( const char* format, ...) {
   const char* _format = format;
   char printBuffer[1024];
   va_list list;
   va_start(list, format);
   vsprintf(printBuffer, _format, list);
   va_end(list);
   fprintf(fp_out, printBuffer, "");
}

void write_file_header() {
  out("//AUTO GENERATED\n");
  out("#include \"text_string_collection.hpp\" \n");
  out("namespace text_strings {\n");
  out("std::map<std::string, std::string> Collection::get_text_strings() {\n");
  out("std::map<std::string, std::string> text_strings;\n");
  out("std::string tmp;\n\n");
}
void write_file_footer() {
  out("return text_strings;\n");
  //ends method
  out("}\n");
  // ends namespace
  out("}\n");
}

void make_c_string(
      string & in) {
   string out;
   for (size_t i = 0; i < in.size(); ++i) {
      char c = in[i];
      if ('"' == c)
         out += "\\\"";
      else if ('\\' == c)
         out += "\\\\";
      else
         out += c;
   }
   in = out;
}

void write_line(const string & line) {
   out("\"%s\\n\"\n", line.c_str());
}

void open_output() {
  string out_file_name = working_path + "/text_string_collection.cpp";
  fp_out = fopen(out_file_name.c_str(), "wt");
}

void close_output() {
  fclose(fp_out);
}

void open_input(string path) {
  string input_file_name = working_path + "/" + path;
  fp_in = fopen(input_file_name.c_str(), "rt");
}

void close_input() {
  fclose(fp_in);
}

void write_file(string source_file) {
  open_input(source_file);

  char buff[1024];

  out("tmp = \"\";\n");
  while (fgets(buff, sizeof(buff), fp_in)) {
    string s(buff);
    s = s.substr(0, s.find('\n'));
    make_c_string(s);
    string line = "tmp += \"" + s + "\\n\";\n";
    out(line.c_str());
  }
  string last_line = "text_strings[\""+source_file+"\"] = tmp";
  out(last_line.c_str());
  out(";\n\n");
}

void write_text_strings(string prefix) {
  DIR *dir;
  struct dirent *ent;
  string local_path = working_path + "/" + prefix + "/";
  if ((dir = opendir (local_path.c_str())) != NULL) {
    /* print all the files and directories within directory */
    while ((ent = readdir (dir)) != NULL) {
      if(ent->d_type == 8) { //TODO: Fix hardcoded value, should come from the c library (D_DIR, but cant find it)
        write_file(prefix+"/"+ent->d_name); 
      }
    }
    closedir (dir);
  } else {
    perror (local_path.c_str());
  }
}


int main(int argc, char** args) {
   if (argc != 2) {
      printf("syntax error, usage :  compile_text_strings path");
      exit(0xff);
   }
   working_path = args[1];

   open_output();
   write_file_header();
   out("\n\n");
   write_text_strings("characters");
   write_effects();
   write_text_strings("places");
   write_text();
   write_text_strings("scenes");

   out("\n\n");
   write_file_footer();
   close_output();
}

The header file referenced in the compilation program above is this simple:

#ifndef  TEXT_STRING_COLLECTION_H
#define  TEXT_STRING_COLLECTION_H
#include <map>
#include <string>
namespace text_string {
  class Collection {
    public:
      std::map<std::string, std::string> get_shaders();
  };
}
#endif  // #ifdef TEXT_STRING_COLLECTION_H

The final piece of the puzzle is making CMake generate the implementation of the above interface whenever a file in the dirs characters, places or scenes change.

set (CHARACTERS
  characters/zelda.txt
  characters/link.txt
)

set (SCENES
  scenes/intro.txt
  scenes/outro.txtx
)

set (PLACES
  places/cave.txt
  places/forrest.txt
)

add_executable(compile_text_strings compile_text_strings.cpp)

add_custom_command(
  OUTPUT ${CPP_SOURCE_DIR}/text_strings/text_string_collection.cpp
  COMMAND compile_text_strings ${CPP_SOURCE_DIR}/text_strings
  DEPENDS ${CPP_SOURCE_DIR}/text_strings/compile_text_strings.cpp ${CHARACTERS} ${SCENES} ${PLACES}
  WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)

set (SOURCES
  loader.cpp
  ${CPP_SOURCE_DIR}/text_strings/text_string_collection.cpp
)

add_library(zelda_text_strings ${SOURCES})
set_target_properties(zelda_text_strings PROPERTIES ${EXTRA_BUILD_ARGS})

We have hardcoded the file paths - you could probably make CMake do this dynamically - but I like to have my dependencies explicitly named.

Now the loader class from this library can be written in many ways - I usually do a loader that tries to find a file by name (essentially its path) in the map defined - and fallback to trying the file system. This makes it easy to quickly iterate on a new file - and the add it to the compilation step once it is done.

quicktipcmakecode generationc++