Skip to content

Is the Header Included from extern “C” Compiled as C or C++?

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

Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *