I traditionally haven't been the biggest fan of this book. Most of what they list is stuff that just looks like using C++ just for the sake of it, with no real benefit over a C codebase.
For instance they don't appear to use RAII at all, even in places where it's an obvious win (InterruptLock).
As the maintainer of a C++14 proprietary RTOS, it's probably the most convincing argument to the C guys as to why C++ can benefit embedded codebases. Like not being able to forget to re-drop thread priority when you've temporarily increased it.
Or one of my favorite patterns is returning the object representing the temporary higher priority. If the caller doesn't do anything with the return value, the destructor is called and thread priority reverts to what it was. However, it gives you a chance for the caller to hold on to that high priority object, and get more work done essentially atomically.
But the compiler doesn't optimize out the constructor, right? It can't.
In an RTOS of all places, that seems awfully expensive. One generally avoids wasted resources in embedded -- the trade-off is it requires more care during development.
I've never seen RAII work well in an embedded system. Not being able to handle exceptions or errors during destruction ruins everything.
> But the compiler doesn't optimize out the constructor, right? It can't.
> In an RTOS of all places, that seems awfully expensive. One generally avoids wasted resources in embedded -- the trade-off is it requires more care during development.
The compiler has no issues "optimizing out" the constructor in my experience. The asm looks just like the equivalent C in all of the cases I've seen.
> I've never seen RAII work well in an embedded system. Not being able to handle exceptions or errors during destruction ruins everything.
I mean, we disallow exceptions anyway. And we use RAII for objects where there isn't a weird failure case to be handled like my example of thread priority guards.
The practicalities of bare metal programming means that a lot of the time an individual work item has to jump contexts (ie. stacks and register sets) in a way that doesn't map cleanly to C++ style exceptions.
> I haven't found many practical guides or tutorials of how to use C++ superiority and boost development process compared to conventional approach of using “C” programming language.
... and does not mention RAII in their quest to write such a guide, they have seriously failed at their goal.
Without exceptions most of your STL containers become unsable, unless aborting is an option when otherwise bad_alloc would get thrown.
Also without exceptions you really need double initialize pattern for your class types. I.e. a ctor that cannot (and will not throw) and then a "error_code Init(...)" method that can return an error code.
Dynamic allocation is banned for many bare metal applications. Dynamic allocations make it harder to reason about worst-case memory usage, especially with fragmentation.
I find the improved initializers in C++11 a very nice addition for such usecases. One can use a struct for the application state (with struct/std::array members etc), and initialize it statically in one statement. Nicely declarative, space/code efficient, and with good type-safety.
> Dynamic allocation is banned for many bare metal applications. Dynamic allocations make it harder to reason about worst-case memory usage, especially with fragmentation.
This right here. There is no STL in true embedded C++ apps.
It is possible to make compile time allocated templated data structures, they work just fine, but they aren't the STL by any stretch.
IMHO this type of C++ is much better to use. Having to plan ahead for worst case memory usage for all classes and statically allocate memory at compile time simplifies a LOT of code. Pointers don't disappear on you (although contents can obviously change!), and removing all the code handling alloc and de-alloc removes a huge chunk of code.
Honestly having gone from "C with classes C++" to "full on C++11", I'm missing the C with classes. Fewer ways to abstract means finding the code that is actually running becomes a lot easier.
Also having code size be an issue means people don't get to write their own set of abstraction layers that end up having only 1 or 2 concrete implementations. :/
> Without exceptions most of your STL containers become unsable, unless aborting is an option when otherwise bad_alloc would get thrown.
On bare-metal, you probably want to be using std::array<> most often (Thanks C++11), which I don't believe have any errors or allocations.
Dynamic Memory is useful in application code, but under bare metal... its often less useful than carefully managed static allocations. Or perhaps... carefully managed static allocations (std::array) are easier to "prove" that edge-cases won't happen.
---------
Something like std::vector or std::unordered_map which reallocate themselves and moves all around memory seems like a huge, unnecessary complication in memory-restricted situations that warrant bare-metal coding.
I guess with a proper std::Allocator class, std::vector and std::unordered_map would be useful. But... that gets complicated. And not very many C++ programmers are aware of how to use "Placement New" or other allocator features. Its probably best to just stick to the simple std::array and avoid dynamic memory.
Why? Would you also abort on other resource allocation errors, such as opening a socket, or creating a process wide mutex or whatever?
I mean sometimes for some apps aborting on such errors (on some embedded platforms where they must work within specific resource constraints) can be an OK strategy but in general people seem to have this special idea about "bad alloc" conditions.
The usual misconceptions are that a) it cannot happen and b) nothing can be done.
If the system is out of memory there isn't much you can do. The general answer is you don't run out of memory in the first place. Linux will kernel panic in similar situations and you are done there too. (in most cases linux unwinds. fails the operation, and continues; but there cases where the only thing left to do is panic and halt)
I heard Lisp machines would just stop everything and give you a REPL so you have a chance to clean things up, or just reboot. I don't see why modern systems can't reserve some amount of memory for such a recovery process. In my Linux experience I'm either saved by the OOM Killer (which is dubious 'safety' at best) or the system just hardlocks and I have to either REISUB or sometimes force a power off.
On a Symbolics Lisp Machine one lands in a low-level command loop, adds more virtual memory (swapping space on some disk) and then can continue from there.
Like I said for some specicial resource constrained system abort() can be a viable solution, or if you just don't want to deal with any resource allocation failures.
In general though if you have a std::vector and a push_back throws bad_alloc, that bad_alloc really only pertains to that particular vector using that particular allocator.
If you assume that a bad_alloc coming from anywhere indicates some kind of system/process wide condition you're assuming too much. UNLESS you've designed the system, i.e. in this case the allocators and such and know this assumption to be true, otherwise no.
Additionally consider a case where your code is for example reading some data off a disk in a loop and the Nth iteration and push_back throws, the stack is unwound, the vector is destroyed and the memory it consumed before is released. In a "normal" application the bad_alloc can propagate up the stack until it's handled by whatever strategy that the application uses for general resource failures.
The two takeaways here are that bad_alloc doesn't really indicate any system wide condition (unless you know that it does), so don't assume such.
Secondly bad_alloc is just like any other resource allocation failure. If your application aborts() when it cannot open a socket, that's fine. But if it doesn't, then why would it abort on memory allocation failure?
And I really want to stress here that bad_alloc doesn't mean same as OOM for example from the kernel's perspective.
It really only pertains to a particular allocator, UNLESS you KNOW otherwise.
"Would you also abort on other resource allocation errors, such as opening a socket, or creating a process wide mutex or whatever?"
You seem to believe this is not a common choice? I've seen plenty of programs crash on being unable to open a socket/file.
I'm not saying this is a good thing, just that your rhetorical implication that it is unusual seems misplaced.
And it's not always even that bad. It is true that something can be done, yes, but that something often requires massive fundamental rearchitecting of the core paradigm of the system or something. In most modern languages it isn't that easy to try to deal with resources problems. Erlang could probably do it, and... uhh... I'm already running out of possibilities here.
If you can't allocate a socket, can your app work (game in offline mode)?
If you can't allocate memory, can your app work (... example ...)? I guess something like trying to load a huge file (say spreadsheet) in excel-type of app.
Can you give an example of how one could recover from a bad alloc (especially in a "bare metal" situation where you probably can't move anything to disk)?
It's contrived, but; imagine a mark and sweep garbage collector. Instead of freeing memory, you mark it as free-able. When you get a bad alloc you an then sweep and actually free the resources, and retry the allocation.
A more realistic example might be if you have an application that allocates a large chunk of memory up front for processing later. If it fails, you can stall until a point where you know the memory will be available, or you could retry with a smaller allocation.
Yea, I've always wondered, for shops that try to make that "C++ but without exceptions" policy work, how do you handle things like vector::push_back() or map::operator[] failing? Just throw up your hands and crash?
I didn't read the book but if you're "bare metal" you want to avoid dynamic allocations in the place. Secondly, if you really run out of memory then you've screwed up something really badly and yes, in these situations your best option is to let the watchdog eat your poor carcass.
What you really want to do in these cases is log debugging info to a noinit section of memory or nonvolatile memory and then reset the processor. Then the firmware can check for and report any logged errors on startup. If you do this religiously you might make it to fifty with most of your hair.
Also, for those programmers who have some input on the EE's designs, a stuff option for MRAM is amazing. It's essentially equivalent to battery backed SRAM, but one chip. We stick logs and hardware exception stack traces/register dumps in there on dev units.
We at least just don't use the STL. We have an in house data structures library that's almost entirely intrusive.
One of the nice things about C++ is that the STL isn't using special case backdoors in the language, so if it doesn't work for your use case you can write your own data structures and have full language support.
Seems to me if vector::push_back fails you are already deep into memory problems. I may put a global catch around most code to avoid crashes but I think once push_back fails, a lot of other stuff won't work either.
C++ can work but yeah, you really can't use the STL stuff since the API doesn't leave you other choice but to abort() if an exception was to be thrown.
If you want some other strategy then you have no choice but to implement your own containers with APIs that can support such operation.
This is a fine "long answer" to the question how do I write C++ on bare metal?. The short answer is don't, medium is if you don't know why every major operating system at every size is written in C, then you have a lot of learning to do.
... this might have been true 20 years ago. It certainly isn't today. Perhaps you stopped learning at some point?
It's important (as many posters in this thread have already noted) to understand that C++ is many languages, and that bare-metal C++ is not necessarily the same language you would use elsewhere.
The fact remains, however, that in the right hands it can be an excellent tool for the task, and certainly more expressive and capable than its predecessor.
Windows is well known to have portions of the kernel written in C++, and MacOSX has huge portions written in ObjectiveC. The greater part of Android is Java-based.
Only Linux is really purely C-based, since Linus really hates C++ with a passion. But there's a reason why even the GCC project has moved over to C++.
"Only linux"
And every BSD, Magenta (a brand new kernel), ThreadX, FreeRTOS, QNX, Integrity, HP-UX, SeL4, Illumos, TI-RTOS, RTEMS...
XNU does not appear to have any Objective-C in the kernel, and it has no C++ in the scheduler, syscall handler, or any other critical section. I doubt Windows has C++ in any such place either.
Inside the XNU tarball (the actual kernel inside OS X) there are 1,078 C source code files containing 652,124 source (not comment, not whitespace) lines. There are exactly 10 Objective-C files containing a total of 1787 code lines, and it's not certain they are even built as part of the kernel binary.
To be fair, there is some amount of C++ in XNU these days. XNU contains 97 C++ files totalling 68,673 code lines, it seems mostly in iokit[50kSLOC] and libkern[17kSLOC] (infrastructure to support the iokit C++ code, maybe).
DirectX is very C++, using namespaces and stuff. Its a bit wonky because DirectX itself is older than C++98, but its clearly a C++-based API. Its very efficient and performant too.
Not sure what you consider "near", but I certainly wouldn't consider even Direct3D 12 to be anywhere "near" direct access to the GPU. It doesn't expose the shader ISA, it doesn't expose the command stream format, it doesn't expose the register sets.
The user mode scheduler is not the system scheduler, thus my statement holds.
> Not sure what you consider "near", but I certainly wouldn't consider even Direct3D 12 to be anywhere "near" direct access to the GPU. It doesn't expose the shader ISA, it doesn't expose the command stream format, it doesn't expose the register sets.
Indeed, the ability for C++ to create abstractions that can track memory spaces in DirectX is proof positive that C++ has useful features applicable to the OS developer.
Note that both CUDA and OpenCL support C++ features as well. The RAII feature of C++ alone is a huge benefit to anybody tracking what bits are where in OS settings. Have you tried to use C++ in the last 10 years?
I'm aware that I/O Kit is written in C++, I added that to my comment (maybe you saw the old version). Drivers are not really kernel per se, there are plenty of systems which have no drivers in the kernel (usually except RTC, which is in kernel in XNU anyway).
Also, it's not really C++, it's a pretty serious subset (lacking templates, multiple inheritance, exceptions, reflection, funky new casts [reinterpret_cast, static_cast, dynamic_cast, const_cast], and even namespaces(!)).
> Also, it's not really C++, it's a pretty serious subset (lacking templates, multiple inheritance, exceptions, reflection, funky new casts [reinterpret_cast, static_cast, dynamic_cast, const_cast], and even namespaces(!)).
Oh come on. Its not like you see C99 dynamic arrays used very much in Kernel mode.
That's the joy of programming languages. Good programmers use the good parts and ignore the bad parts. C++ has a lot of bad parts (with regards to kernel programming. Such as how most of the STL automatically reallocates memory on its own whims).
So, you ignore the parts that don't work for your task, and the work with the parts that are useful. Very few people have good uses of multiple inheritance anyway.
----------
> Also, it's not really C++, it's a pretty serious subset (lacking templates, multiple inheritance, exceptions, reflection, funky new casts [reinterpret_cast, static_cast, dynamic_cast, const_cast], and even namespaces(!)).
BTW: The -kernel flag in Visual Studio only disables:
* Dynamic Casts (Reflection/RTTI is disabled), Exceptions, and the DEFAULT New / Delete.
Templates, Static_cast / Const_cast and all that good stuff are still available.
> I'm aware that I/O Kit is written in C++, I added that to my comment (maybe you saw the old version).
The danger of quick edits. : )
> Drivers are not really kernel per se, there are plenty of systems which have no drivers in the kernel (usually except RTC, which is in kernel in XNU anyway).
I mean, except more than half of the OSs you listed. And of those you listed that are microkernels, only QNX has gotten any serious traction.
> Also, it's not really C++, it's a pretty serious subset (lacking templates, multiple inheritance, exceptions, reflection, funky new casts [reinterpret_cast, static_cast, dynamic_cast, const_cast], and even namespaces(!)).
Yeah, compilers have come a really long way in the nearly 20 years since that was designed, but they want to keep ABI compatibility.