C as a scripting language thanks to TinyCC

This year I’d like to play around with music. Mostly live looping. Live looping is a pretty specific way of making music. It’s about recording short, simple music clips and overlaying those to create more complex and interesting music.

A recent googling session took me to OpenAV’s website. It’s Harry Van Haaren’s initiative. He’s focusing on making open source application’s for live music performances. You can find the apps' code on his Github page. I really recommend checking it out.

What caught my attention was the [Ctlra][ctlra] project. It tries to create an intermediate layer between the hardware controller driver and the application using it. That means any controller can be very easily hot plugged to any application. In this post I want to explain one trick Harry uses in [Ctrlra][ctlra] code. That is, using C like a scripting language, thanks to TinyCC compiler.

TinyCC is small, simple, and very fast C compiler. It has a peculiar feature which makes it possible to turn C into a scripting language. Just add #!/usr/bin/tcc -run to the start of a C source file, and execute the source file. It’s also used as a shared library to add dynamic code generation capabilities to other programs.

I was interested in how the dynamic compilation feature works. The best way to learn about something is to do it yourself. I’ve written a very simple app which uses the TinyCC as a library to compile a source file during runtime. You can find it here. It doesn’t have any dependencies outside the standard library. The TinyCC library is included in the repo.

Let me explain briefly how it works. First I have defined a structure which describes the script file.

typedef char* (*script_hello)(void);
typedef char* (*script_bye)(void);


typedef struct
script_t
{
  char* path;
  void* program;
  time_t time_modified;
    
  script_hello hello;
  script_bye bye;
} script_t;

Before the structure itself, there are the interface typedefs. Those are the functions that are expected to be present in the script source file.

The compile_program function takes the script_t structure and does all the TinyCC compilation steps:

static int
compile_program(script_t* script)
{
  TCCState* tcc = tcc_new();
  if (!tcc)
  {
    printf("[TCC:ERR] Failed to create tcc context!\n");
    return -1;
  }

  tcc_set_lib_path(tcc, "./tcc_local");
  tcc_add_include_path(tcc, "./tcc_local");

  tcc_set_error_func(tcc, 0x0, tcc_error);
  tcc_set_options(tcc, "-g");
  tcc_set_output_type(tcc, TCC_OUTPUT_MEMORY);

  int ret = tcc_add_file(tcc, script->path);
  if (ret < 0)
  {
    printf("[TCC:ERR] Failed to add tcc file!\n");
    tcc_delete(tcc);
    return -1;
  }

  // tcc_relocate called with NULL returns the size that's necessary for the added files.
  script->program = calloc(1, tcc_relocate(tcc, NULL));
  if (!script->program)
  {
    printf("[TCC:ERR] Failed to allocate memory for the program!\n");
    tcc_delete(tcc);
    return -1;
  }

  // Copy code to memory passed by the caller. This is where the compilation happens (I think...).
  ret = tcc_relocate(tcc, script->program);
  if (ret < 0)
  {
    printf("[TCC:ERR] Failed to allocate memory for the program!\n");
    tcc_delete(tcc);
    return -1;
  }

  script->hello = (script_hello)tcc_get_symbol(tcc, "target_hello");
  script->bye = (script_bye)tcc_get_symbol(tcc, "target_bye");

  return 0;
}

I don’t see value in explaining each line of this function. Treat it as a minimalisitic example of a function which can compile another C file. The end part of this function is where the program grabs the pointers to the recompiled functions and binds them to the script structure.

A rude way of testing this, is to run this in a while loop.

  while (1)
  {
    struct timespec t = {};
    t.tv_sec = 0;
    t.tv_nsec = 200000000;
    nanosleep(&t, &t);

    compile_program(&script);
    printf("%s\n", script.hello());
    printf("%s\n", script.bye());
  }

A better way is to observe the last modification’s timestamp, reload and recompile the source file only when the file has been modified.

TinyCC compiler isn’t a new compiler. This is not a new trick and it might come in handy only in certain situations. It also imposes some limitations on how you structure your code and introduces a dependency on the tcc library. It’s better to know this trick exists and never use it than needing it and not knowing how to go about it.