Integrating C++ Drivers into C-Based Firmware

Integrating C++ Drivers into C-Based Firmware

How to Wire a C++ Sensor Driver into Your C Firmware (Without Losing Your Mind)

Imagine this scenario: you’re deep into building out an IoT framework—maybe pushing telemetry data through an EMS mesh network or finalizing a smart agritech setup. Your C firmware is stable, the networking protocols are humming, and everything is on track. Then, someone spots a new temperature sensor that’s 10x cheaper and more accurate than the one you’re currently using.

Naturally, management wants it integrated into the next hardware revision immediately.

You download the official driver for this shiny new piece of silicon, only to discover a major catch: it’s written entirely in C++.

Your core firmware is C.

You could spend the next week rewriting the entire driver from scratch in C. But that’s a massive time sink, practically invites new bugs, and makes future driver updates a nightmare. Instead, there are two practical ways to drop that C++ driver straight into your C-based firmware with minimal friction.

Here is how to bridge the gap.

C compilers have absolutely no concept of C++ classes, objects, or methods. To get the two languages talking, we have to build a translator—a C wrapper around the C++ code.

The secret sauce here is using extern "C" alongside opaque pointers. An opaque pointer (void*) allows us to hold onto a C++ class instance inside our C code without ever exposing the complex internal structure of that class. The C code just sees a generic, harmless handle.

Let’s look at how we’d wrap a modern I2C temperature sensor driver.

1. The Original C++ Driver (TempSensor.hpp & TempSensor.cpp) This is the vendor’s code. You don’t need to touch it. It uses standard C++ features like classes.

// TempSensor.hpp
#pragma once

class TempSensor {
public:
    TempSensor(int i2c_address);
    ~TempSensor();
    bool init();
    float getTemperature();
private:
    int address_;
};
// TempSensor.cpp
#include "TempSensor.hpp"
#include <iostream>

TempSensor::TempSensor(int i2c_address) : address_(i2c_address) {}
TempSensor::~TempSensor() {}

bool TempSensor::init() {
    // Hardware init logic here
    return true; 
}

float TempSensor::getTemperature() {
    // Read from hardware registers
    return 24.5f; // Dummy temperature
}

2. The Bridge Header (TempSensor_C_Wrapper.h) This is the only file your C code will ever see. We use #ifdef __cplusplus so that when a C++ compiler reads it, it disables name mangling (extern "C"). When the C compiler reads it, it just sees standard C function declarations.

// TempSensor_C_Wrapper.h
#pragma once

#ifdef __cplusplus
extern "C" {
#endif

// The Opaque Pointer: C doesn't know what this points to, and it doesn't need to.
typedef void* TempSensorHandle; 

TempSensorHandle TempSensor_create(int i2c_address);
void TempSensor_destroy(TempSensorHandle sensor);
int TempSensor_init(TempSensorHandle sensor);
float TempSensor_getTemperature(TempSensorHandle sensor);

#ifdef __cplusplus
}
#endif

3. The Bridge Implementation (TempSensor_C_Wrapper.cpp) This file is compiled as C++. It translates the simple C functions into the actual C++ object creation and method calls by casting the opaque pointer back to the class type.

// TempSensor_C_Wrapper.cpp
#include "TempSensor.hpp"
#include "TempSensor_C_Wrapper.h"

extern "C" {
    TempSensorHandle TempSensor_create(int i2c_address) {
        return new TempSensor(i2c_address); // Instantiate the C++ object
    }

    void TempSensor_destroy(TempSensorHandle sensor) {
        delete static_cast<TempSensor*>(sensor); // Cast back and clean up
    }

    int TempSensor_init(TempSensorHandle sensor) {
        // Return 1 for success, 0 for failure to keep C happy
        return static_cast<TempSensor*>(sensor)->init() ? 1 : 0;
    }

    float TempSensor_getTemperature(TempSensorHandle sensor) {
        return static_cast<TempSensor*>(sensor)->getTemperature(); // Cast and call
    }
}

4. The Clean C Application (main.c) Your main application logic remains completely blissfully unaware that C++ is running under the hood.

// main.c
#include "TempSensor_C_Wrapper.h"
#include <stdio.h>

int main() {
    // Create the sensor driver instance (e.g., I2C address 0x48)
    TempSensorHandle mySensor = TempSensor_create(0x48); 
    
    if (TempSensor_init(mySensor)) {
        float temp = TempSensor_getTemperature(mySensor);
        printf("Current Temperature: %.2f C\n", temp);
    } else {
        printf("Sensor initialization failed!\n");
    }
    
    // Free the memory
    TempSensor_destroy(mySensor); 
    
    return 0;
}

5. Tying it all together with CMake (CMakeLists.txt) To make this compile, you need to tell your build system to compile the C++ files into a library, and then link that library into your C executable. The C++ linker handles the heavy lifting.

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(SensorFirmware C CXX)

set(CMAKE_C_STANDARD 99)
set(CMAKE_CXX_STANDARD 11)

# 1. Create a static library from the C++ driver and our wrapper
add_library(TempSensorLib STATIC 
    TempSensor.cpp 
    TempSensor_C_Wrapper.cpp
)

# 2. Build the main C program
add_executable(firmware main.c)

# 3. Link the C++ library into the C program
target_link_libraries(firmware PRIVATE TempSensorLib)

Approach 2: The Brute Force Method (Compiling C as C++)

If your project is mostly C but your build system is flexible, you might not need wrappers at all. You can simply tell your compiler to treat everything as C++.

You do this by either renaming your .c files to .cpp, or by passing a specific language flag to your compiler (like -x c++ for GCC/Clang). Once you do that, you can include the C++ driver headers directly in your main application files.

The Warning Label: This is the nuclear option. While it sounds easy, it can instantly break your build if your existing C code relies on things that C++ strictly forbids. For example, C allows implicit void* conversions (like when calling malloc), but C++ requires explicit casting. C++ also has stricter keyword rules, so if you happen to have a C variable named class or new, the compiler will throw hundreds of errors. Use this only if you are confident your C codebase is already highly C++ compliant.

The Takeaway

Mixing C and C++ in embedded systems doesn’t have to be a nightmare of linker errors. By using opaque pointers and a clean extern "C" boundary, you can leverage the massive ecosystem of modern C++ drivers while keeping your core C firmware exactly the way you like it. It saves development time, keeps the codebase modular, and most importantly, gets that new cost-effective hardware online fast.

Stay tuned & Be Curious!