It depends! If a C++ source file includes the header inside an extern "C"
section, the header is compiled as C++. If a C source file includes the header, the header is compiled as C. Hence, the header file should be both valid C and valid C++.
Including C Headers from C++ Sources
GoogleTest, CppUTest and QtTest are widely used unit test frameworks written in C++. The first two come with a mocking framework. The last one provides table-driven tests and some special Qt features (e.g., QSignalSpy). Especially if we know them from testing C++ code, we want to use them for testing C code, too. And we can do this – with a little bit of care.
We define the test class TestRingbuffer
in GoogleTest as follows, where ringbuffer.h and ringbuffer.c contain the code to be tested.
// File: test_ringbuffer.cpp
extern "C"
{
#include "ringbuffer.h"
}
class TestRingbuffer : public testing::Test
{
};
TEST_F(TestRingbuffer, capacity)
{
ringbuffer_t rb = ringbuffer_create(5);
EXPECT_EQ(ringbuffer_capacity(rb), 5);
}
ring buffer_t
is an opaque pointer to an abstract data type (ADT) mimicking an object in C. The functions of the ADT are declared in ringbuffer.h and defined in ringbuffer.c. Both files are written in C as is the rest of our software (sorry, firmware). The important parts of ringbuffer.h look as follows:
// File: ringbuffer.h
typedef struct ringbuffer_instance_t* ringbuffer_t;
ringbuffer_t ringbuffer_create(uint32_t capacity);
uint32_t ringbuffer_capacity(ringbuffer_t this);
void ringbuffer_destroy(ringbuffer_t this);
The above type and functions are implemented in ringbuffer.c, which includes ringbuffer.h. Their implementation is not relevant for this post.
// File: ringbuffer.c
#include "ringbuffer.h"
We add the source files test_ringbuffer.cpp and ringbuffer.c to the CMakeLists.txt of the test_ringbuffer
project.
// File: CMakeLists.txt
project(test_ringbuffer LANGUAGES CXX C)
enable_testing()
add_executable(${PROJECT_NAME} test_ringbuffer.cpp ringbuffer.c <more source files>)
add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME})
When we build the project test_ringbuffer
,
- test_ringbuffer.cpp and all other files ending with .cpp are compiled with the C++ compiler, and
- ringbuffer.c and all other files ending with .c are compiled with the C compiler.
When the C++ compiler sees #include "ring buffer.h"
inside the extern "C"
section of test_ringbuffer.cpp, it regards the header ringbuffer.h as C++. Hence, the code in ringbuffer.h and its included headers must be valid C++ code. However, the symbols in these header files have C linkage due to extern "C"
, hence the C++ compiler doesn’t mangle the symbol names.
As this
is a C++ keyword, the code in ringbuffer.h is not valid C++. We fix this by replacing this
with instance
.
// File: ringbuffer.h (now valid C and C++)
typedef struct ringbuffer_instance_t* ringbuffer_t;
ringbuffer_t ringbuffer_create();
uint32_t ringbuffer_capacity(ringbuffer_t instance);
void ringbuffer_destroy(ringbuffer_t instance);
When the C compiler sees #include "ringbuffer.h"
at the beginning of ringbuffer.c, it regards the header ringbuffer.h as C. Hence, the code in ringbuffer.h and its included headers must be valid C code, which is true for ringbuffer.h. All symbols in these files have C linkage.
In summary: The code in ringbuffer.h must be both valid C and valid C++. The extern "C"
declaration has no influence whether the headers included in the extern-C section are compiled as C or C++. It only switches off C++ name mangling.
List of Incompatibilities between C and C++
You find the example code for the incompatibilities in the GitHub repository add-training-add-ons. The top-level CMake file is examples/CMakeLists.txt. Uncomment the line #add_subdirectory(incompatible-c-cpp)
and build the project. The compiler will complain about the incompatibilities. The header examples/incompatible-c-cpp/src/ringbuffer.h contains a description of the error messages and how to fix them.
Do Not Use C++ Keywords in C Headers
As seen above, we must not use C++ keywords like this
in C headers. We replace them by variable names that are valid in both C and C++. For example, we replace this
by instance
. The C++ compiler emits an error messages like this:
ringbuffer.h:19:43: error: invalid parameter name: 'this' is a keyword
Do Not Use struct void*
for Opaque Pointers to C Objects
We usually declare the opaque pointer type to a C object as follows:
// File: ringbuffer.h (valid C and C++)
typedef struct ringbuffer_instance_t* ringbuffer_t;
The structure ringbuffer_instance_t
is declared in the corresponding source file ringbuffer.c so that clients don’t see its declaration.
// File: ringbuffer.c
struct ringbuffer_instance_t
{
uint32_t capacity;
};
Some C developers want to make extra sure that ringbuffer_t
is opaque and declare it as follows:
// File: ringbuffer.h (valid C, but invalid C++)
typedef void ringbuffer_instance_t;
typedef struct ringbuffer_instance_t* ringbuffer_t;
The C compiler is happy with the resolved type definition: typedef struct void* ringbuffer_t
. The C++ compiler complains with the error message:
ringbuffer.h:12:16: error: typedef 'ringbuffer_instance_t' cannot be referenced with a struct specifier
The fix is to remove the first typedef
.
Interesting Resources
- Arne Mertz: Calling C Code from C++ With ‘extern “C”‘. Arne explains why we should use
extern "C"
only in C++ files and not in C files. - Nick Miller: Practical Design Patterns – Opaque Pointers and Objects in C. The opaque pointer pattern enables us to use objects (abstract data types) in C. The pattern can also be used if we don’t want to allocate memory dynamically. Nick points that we should use the pattern only when we need multiple instances.