Search code examples
c++cmakecross-compilingembedded-resource

Embed resources (eg, shader code; images) into executable/library with CMake


I am writing an application in C++ which relies on various resources in my project. Right now, I have the relative path from the produced executable to each resource hard-coded in my sources, and that allows my program to open the files and read in the data in each resource. This works ok, but it requires that I start the executable from a specific path relative to the resources. So if I try to start my executable from anywhere else, it fails to open the files and cannot proceed.

Is there a portable way to have CMake embed my resources into the executables (or libraries) such that I can simply access them in memory at runtime instead of opening files whose paths are brittle? I have found a related question, and it looks like embedding resources can be done well enough with some ld magic. So my question is how do I do this in a portable, cross platform manner using CMake? I actually need my application run on both x86 and ARM. I am ok with supporting only Linux (Embedded), but bonus points if anyone can suggest how to do this for Windows (Embedded) as well.

EDIT: I forgot to mention a desired property of the solution. I would like to be able to use CMake to cross-compile the application when I am building for ARM rather than have to compile it natively on my ARM target.


Solution

  • One of the easiest ways to do this is to include a small, portable C program in your build that reads the resource and generates a C file that contains the length of the resource data and the actual resource data as an array of constant character literals. This will be entirely platform independent, but should only be used for resources that are reasonably small. For larger resources, you probably don't want to embed the files in your program.

    For resource "foo", the generated C file "foo.c" would contain:

    const char foo[] = { /* bytes of resource foo */ };
    const size_t foo_len = sizeof(foo);
    

    To access the resource from C++, you declare the following two symbols in either a header or the cpp file where they're used:

    extern "C" const char foo[];
    extern "C" const size_t foo_len;
    

    To generate foo.c in the build, you need a target for the C program (call it embedfile.c), and you need to use the add_custom_command command to call this program:

    add_executable(embedfile embedfile.c)
    
    add_custom_command(
      OUTPUT foo.c
      COMMAND embedfile foo foo.rsrc
      DEPENDS foo.rsrc)
    

    Then, include foo.c on the source list of a target that requires the "foo" resource. You now have access to the bytes of "foo".

    The program embedfile.c is:

    #include <stdlib.h>
    #include <stdio.h>
    
    FILE* open_or_exit(const char* fname, const char* mode)
    {
      FILE* f = fopen(fname, mode);
      if (f == NULL) {
        perror(fname);
        exit(EXIT_FAILURE);
      }
      return f;
    }
    
    int main(int argc, char** argv)
    {
      if (argc < 3) {
        fprintf(stderr, "USAGE: %s {sym} {rsrc}\n\n"
            "  Creates {sym}.c from the contents of {rsrc}\n",
            argv[0]);
        return EXIT_FAILURE;
      }
    
      const char* sym = argv[1];
      FILE* in = open_or_exit(argv[2], "r");
    
      char symfile[256];
      snprintf(symfile, sizeof(symfile), "%s.c", sym);
    
      FILE* out = open_or_exit(symfile,"w");
      fprintf(out, "#include <stdlib.h>\n");
      fprintf(out, "const char %s[] = {\n", sym);
    
      unsigned char buf[256];
      size_t nread = 0;
      size_t linecount = 0;
      do {
        nread = fread(buf, 1, sizeof(buf), in);
        size_t i;
        for (i=0; i < nread; i++) {
          fprintf(out, "0x%02x, ", buf[i]);
          if (++linecount == 10) { fprintf(out, "\n"); linecount = 0; }
        }
      } while (nread > 0);
      if (linecount > 0) fprintf(out, "\n");
      fprintf(out, "};\n");
      fprintf(out, "const size_t %s_len = sizeof(%s);\n\n",sym,sym);
    
      fclose(in);
      fclose(out);
    
      return EXIT_SUCCESS;
    }