MODERN C++ IN EMBEDDED DEVELOPMENT: using c libraries
Language linkage
C++ programs can use C code. The linkage between C++ and software modules written in other programming languages is called language linkage.
extern string-literal {declaration}
Standard guarantees only two language linkages: C++ and C. C++ is the default language linkage.
extern “C” {
void my_c_function();
}
The above external linkage makes it possible to link C++ code with the C function my_c_function. That is, we can call my_c_function from our C++ code.
Name mangling
Name mangling is one of the reasons we need to specify language linkage when linking against C code. It’s the feature the C++ compiler uses to enable function overloading by generating function names with an encoding of the types of the function arguments. Let’s take a look at the following example:
void func(int a) {
}
void func(float a) {
}
int main() {
func(1);
func(1.0f);
return 0;
}
Below is the disassembly of the compiled example:
_Z4funci:
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
nop
pop rbp
ret
_Z4funcf:
push rbp
mov rbp, rsp
movss DWORD PTR [rbp-4], xmm0
nop
pop rbp
ret
main:
push rbp
mov rbp, rsp
mov edi, 1
call _Z4funci
mov eax, DWORD PTR .LC0[rip]
movd xmm0, eax
call _Z4funcf
mov eax, 0
pop rbp
ret
.LC0:
.long 1065353216
As we can see, the compiler generated two symbols: _Z4funci – our example function with int as the argument type, and _Z4funcf – our function with float as the argument type.
To prevent name mangling and make linking with C code possible, we need to apply the extern “C” language linkage specifier to the C functions we want to link with C++ code. It allows us to call C functions from C++ code and vice versa.
C standard library in C++
C++ wraps the C standard library and provides header files with the same name as the C language version but with a “c” prefix and no extension. For example, the C++ equivalent for the C language header file <stdlib.h> is <cstdlib>.
In GCC implementation C++ wrappers include C standard library headers, for example <cstdio> includes <stdio.h>. If you dive into <stdio.h> you can see that it guards function declarations with __BEGIN_DECLS and __END_DECLS macros. Here’s the definition of these macros:
/* C++ needs to know that types and declarations are C, not C++. */
#ifdef __cplusplus
# define __BEGIN_DECLS extern "C" {
# define __END_DECLS }
#else
# define __BEGIN_DECLS
# define __END_DECLS
#endif
So, standard C library files take care of C++ compatibility, and this practice is also used in many HAL implementations provided by microcontroller vendors.
When including non-system C headers, in case they don’t guard function declarations with extern “C”, we can do that in a place where we include them:
extern “C” {
#include “my_c_library.h”
}
Using C libraries in a C++ project
In order to use a C function in C++ code, the function declaration needs to have a proper language linkage. Let’s look at a trivial example of a C++ class, MyClass, which has a method print_my_name.
#include <cstdio>
class MyClass {
private:
const char * my_name_ = "MyClass";
public:
void print_my_name () {
puts(my_name_);
}
};
int main() {
MyClass my_obj;
my_obj.print_my_name();
return 0;
}
By including <cstdio>, we can use the puts function from C standard library. And there’s nothing wrong with this approach, but we can do much better in C++.
We could wrap this functionality provided by a C library, in this case, a system library, and in the case of embedded systems, often, a vendor-provided function, in the Printer C++ class.
The next would be to transform MyClass into a template class and instantiate it with different Printer classes. So what do we get by this:
- Mocking C++ frameworks can usually mock only C++ classes, so now we can easily mock all the functionality provided in the C libraries we are using,
- We separated the business logic from hardware-dependent functionality that vendors usually provide in C libraries and, as a result, have portable pieces of code that are configurable in compile time,
- Not only can we mock functionality provided by C code, but we can also stub it and run simulations on target.
#include <cstdio>
struct Printer {
static void print_str(const char * str) {
puts(str);
}
};
template <typename P>
class MyClass {
private:
const char * my_name_ = "MyClass";
P printer_;
public:
void print_my_name () {
printer_.print_str(my_name_);
}
};
int main() {
MyClass<Printer> my_obj;
my_obj.print_my_name();
return 0;
}
The above example looks more complex than the first version of this program, but it’s just a basic template knowledge. And if you come from a C background, you must be sure the second version will result in a bigger flash footprint. Well, it turns out that the disassembly of both versions is the same:
.LC0:
.string "MyClass"
main:
sub rsp, 8
mov edi, OFFSET FLAT:.LC0
call puts
xor eax, eax
add rsp, 8
ret
This is often called a zero-cost abstraction. It might be a bit misleading terminology. Even though both versions will result in the same assembly code, the compiler will take more time to crunch the second one, so the more proper term would be a zero-run-time-cost abstraction.
Amar Mahmutbegović
Head of Engineering
Embedded developer with a proven history in the development of BLE embedded firmware in C and C++. Co-founder of Semblie.
S