The many faces of C++

Ok, so it’s actually “phases” of C++…

So when you write a piece of C++ code, the most fundamental part to understand, is what gets evaluated when. So, let’s start when you press “upload”.

The preprocessor

The first thing that happens is that the code runs through the preprocessor. The preprocessor works a bit like a text editor, doing operations like inserting files replacing words, evaluating simple expresions and deleting segments of text. Most preprocessor stuff starts with a #, like #if, #include or #define. The exception is that defines work kind of like a search and replace on the rest of the code, not just the lines that start with #.

The output of the preprocessor is a text file which contains the expanded, edited code to be compiled.

The compiler

Now it’s time for the compiler to translate the text into code. The compiler does most of the heavy lifting of translating text into code. In some sense, this is a very straightforward process, and most things you write in the code gets translate directly into machine code instructions, one function at a time.

C++ has a powerful template system, it is in fact a language unto itself. Templates gets expanded in the compiler, and this expansion can decide what code to compile, or generate multiple versions of code for compilation. One of the most straightforward example of this is the std:if_same_type<> template which is like an if statement for types, which gets calculated in the compiler.

There is also something called a “constexpr”, which may get evaluated in the compiler. A constexpr function looks just like a normal function, but has to follow certain rules to make it possible for the compiler to run it at compile time. Constexpr functions can be used as template arguments, in which case the have to be executed at compile time. Now, even if a constexpr function gets evaluated later (at runtime), the result is the same, so for the most part it doesn’t matter.

The output of the compiler is an .o (object) file which contains compiled code, grouped into functions. It’s not ready to execute yet though, it’s more like a zip file where each function is listed separately inside.

The linker & collect2

The linker is in charge of taking all the .o files, joining everything together and then updating all code jumps to point to the right place, producing a proper executable file as output. Because C++ is complicated, it has a plugin called collect2 that actually does some of the template expansion and optimization work. Not particularly relevant here though.

The linker also throws away any code that was compiled but ended up not being needed. The way it does this turns out to be interesting, and can be used to do some really funky optimizations, but please don’t do that…

The output of the compiler is pure machine code. None of the original text is left. These files are hard to read, but there is a tool called objdump that translate each machine instruction into text which helps.

Execution

Once the code has been preprocessed, compiled, linked and uploaded, we can actually run the code. At this point the function calls and the if statements actually get to run. Now, each program has it’s own peculiarities about which order things run, but let’s look at some ProffieOS specifics.

Async

In general, almost everything in ProffieOS operates in asynchronous mode. That means that most code will start an operation, but not actually wait for it to finish. A typical example is playing two sounds after each other. In ProffieOS you can’t just write:

  play("sound1.wav");
  play("sound2.wav");

Because the first play() will start the first sound and return while first couple of samples are still playing. Obviously there are ways to make the code wait for the wav to finish, but doing that would mean that something else doesn’t get done when it is supposed to.

Loop

Generally speaking, in ProffieOS, instead of waiting for something, we have to do something called polling. Basically, we put a piece of code in the Loop() function, which gets called hundreds of times per second, that checks if it’s time to do something or not, kind of like:

  if (sound_one_started_ && sound_one_is_done_playing()) {
    sound_one_started_ = false;
    play("sound2.wav");
  }

Now, there may be easier ways get multiple things played, but most of the time, that just means using a helper class which does the polling for you.

Interrupts

ProffieOS does a bunch of things in interrupts, most notably sound reading and playing, motion sensor reading and sending data to displays. Interrupts are nice, because when they are triggered, the processor immediately stops what it’s doing and calls the interrupt function. This allows for very tight timing. The drawback is that because interrupts can happen at any time, a lot of special precautions have to be taken to prevent bad states to occur. Here is a simple example:

int A;
void add1() { A = A + 1; }

This gets compiled into something like:

    read A into register 0
    load 1 into register 1
    add register 1 to register 0
    write register 0 to A

If you call this from both regular code, and an interrupt, this can happen:

    read A into register 0  // A=0
    load 1 into register 1
        INTERRUPT OCCURS AND SAVES ALL REGISTERS TO STACK
         read A into register 0  // A  = 0
         load 1 into register 1
         write register 0 to A  // A = 1
        INTERRUPT ENDS AND RESTORES REGISTERS FROM STACK
    write register 0 to A // A =1 again!

Basically add1 got called twice, but only got updated once. This sort of things can cause all sorts of crashes.

The biggest part where this matters is reading from the SD card. ProffieOS uses an interrupt for reading from the SD card, and to prevent crashes it’s impossible to do reads from the SD card outside of interrupts at the same time. We have a locking mechanism to sort of work around this, but while locked, no audio can be read from the SD card, so doing that often causes audio glitches,

Not sure if this is actually helpful to anybody, but if it is I might write some more explainers about C++ and ProffieOS, so let me know what you think.

7 Likes

Yes please to more articles like that. I find it fascinating.

I’m down for a trip into the rabbit hole. :smiley:

As a former c++ hobby developer, this is all fun to read and revisit.

That last part about interrupts gets into something I’ve been wondering for a long time, basically what the structure of proffie OS is and why it was done that way. Like the idea that SD card reads must happen in interrupt, but that blocks other things… What implications does us have for other stuff like accelerometer interrupts, sound ring buffer, etc.

All of that is frankly outside of c++ specifically, and more about this specific program.

But in the back of my mind I wonder, if this were made today with how powerful modern processors are, could you do things differently or more simply? Like using pio or multiple cores to basically do true multitasking, stuff like that.

At any rate, fascinating read

Unfortunately, embedded CPUs aren’t progressing as fast as I would like, so even if I re-did things today, some things would be done the same way.

However, I have hope, that one day we’ll have a linux-capable computer in our sabers. At that point, everything can have it’s own thread, and things that needs interrupts would become kernel-level drivers.

At the end of the day, PIO and multicore doesn’t matter. It’s the amount of memory, speed, and the availability of an MMU that matters. The rest can be solved with a little bit of clever coding.

There are a few chips that could be used this way already, unfortunately they have several disadvantages[1] compared to the STM chips I’ve been using, leading me to think that maybe I still need an STM processor to handle all the IO stuff.

[1] Not 5v tolerant, not enough PWM units, uncertain WS2811 capabilities and no PIO

1 Like