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.