Kane G. Anderson

C and C++ interop in embedded systems

· Blog · 17 min read

C and C++ interop in embedded systems

Note: this content is best viewed on ChatGPT or Q-PAC’s Confluence page if you can. The code formatting didn’t copy over very nicely.

https://chatgpt.com/share/e/6852002a-d1b4-8005-91a8-0f7e5df32c0e

https://teamq-pac.atlassian.net/wiki/x/bIA87

I had a great chatGPT session where I learned a lot. Thought I would document it and share in case it can be helpful to any fellow wizards.


Our Fan Controller has a firmware architecture that splits the code base into statically linked library layers. There are 5 layers:

  1. Microprocessor Support Pack (MSP)
  2. Drivers
  3. Board Support Pack (BSP)
  4. Hardware Abstraction Layer (HAL)
  5. Application

This all came about because I had some code in our BSP that defines a binary_output. We had a lot of repeated code (one for each gpio output). So I combined them all into a monstrous macro…

#define CREATE_BINARY_OUTPUT(name, gpio_port, gpio_pin, mode, pull, default_state) \
  binary_output_internal name##_internal; \
  GPIO_Handle name##_pin = { .port = (gpio_port), .pin = (gpio_pin) }; \
  GPIO_Setup name##_setup = {.type = GPIO_IO_TYPE_OUTPUT, .output_mode = (mode), \
 .pull_type = (pull)}; \
  static void name##_init() { \
    gpio_init(&name##_pin, &name##_setup, name##_pin.port, name##_pin.pin); \
    set_binary_output(&name##_pin, default_state); \
    name##_internal.state = default_state; \
  } \
  static void name##_set(binary_state new_state) \
  { \
    set_binary_output(&name##_pin, new_state); \
    name##_internal.state = new_state; \
  } \
  static binary_state name##_check(void) \
  { \
    return name##_internal.state; \
  } \
  static binary_state name##_toggle(void) \
  { \
    name##_set(invert_binary_state(name##_internal.state)); \
    return name##_check(); \
  } \
  binary_output name = {.init = name##_init, .state = name##_check, .set_to = name##_set, .toggle = name##_toggle};

For anyone who’s written C before, that looks like a disgusting filthy rotten infested puss-filled boil ready to pop.

It was ugly, but it saved about 100 lines of code.

I wanted to leverage a class in C++ since I was essentially trying to do class-like stuff. I wanted to put C++ in the BSP layer, then consume that library’s functionality by a C function in the next layer up. So I asked chatGPT how to do that.

I went down a 1.5-hour-long rabbit hole looking at chatGPT and wrapping my head around it. Transcript is below.

I think we can use something like this for our code.

You said

When compiling code mixed with C and C++, is there a way for the C to call C++? Or just the other way around? Asking because I’m trying to compile a library of functions which will be used by C. Wondering if I can mix C++ at the lower architecture layer and still be okay?

ChatGPT said:

Yes, it is possible for C code to call into C++ code, but you have to take specific steps to make it work. This is a fairly common scenario when building libraries that leverage C++ features internally but are exposed to C-based consumers. Here’s how it works:


✅ Calling C++ from C

While C++ can directly call C functions (as long as they are declared with extern "C"), calling C++ from C requires a bridge layer because C has no understanding of C++ features like name mangling, classes, function overloading, etc.

To allow C code to call C++ functions:

1. Create an extern "C" Interface

You need to write a C-compatible wrapper in C++ that exposes functions using extern "C" to disable name mangling:

// mylib.cpp
#include <string>

std::string internal_cpp_function() {
  return "Hello from C++";
}

extern "C" const char* get_message() {
  static std::string msg = internal_cpp_function();
  return msg.c_str(); // Ensure the string stays valid
}

2. Create a C Header

Then provide a header that can be included from C:

// mylib.h
#ifdef __cplusplus
extern "C"
{
#endif
const char* get_message();
#ifdef __cplusplus
}
#endif

Compile your C++ code with a C++ compiler, your C code with a C compiler, and link them together using the C++ linker. For example:

g++ -c mylib.cpp gcc -c main.c g++ -o my_program mylib.o main.o

The final link must be done with g++ to ensure the C++ standard library is included.


🔁 Summary of Interoperability

DirectionPossible?How
C++ → C✅ YesUse extern "C" on C declarations
C → C++✅ YesWrite C-compatible extern "C" wrappers in C++
C++ class → C🚫 Directly noMust use opaque pointers and C-compatible APIs
C++ features (overloading, templates) → C🚫 NoMust wrap in simple C-style APIs

🔧 Practical Advice for Library Design

If you’re building a library that will be consumed by C code:

This pattern is widely used, including in projects like OpenCV, TensorFlow Lite, and LLVM.

Let me know if you want an example with class-based wrappers or dynamic memory management patterns too.

You said:

Yes please, how about the minimal working example please. Just foo and bar, nothing fancy

ChatGPT said:

Absolutely! Here’s a minimal working example demonstrating how C code can call a C++ function through a wrapper. It involves:


🔹 Step 1: mylib.cpp (C++ file)

// mylib.cpp
#include <iostream>
void bar() {
  std::cout << "Hello from C++ function 'bar()'!\n";
}
// C-compatible wrapper
extern "C" void foo() {
 bar(); // Call the actual C++ function
}

🔹 Step 2: mylib.h (C/C++ header)

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
void foo(); // C-visible function
#ifdef __cplusplus
}
#endif
#endif // MYLIB_H

🔹 Step 3: main.c (C file)

// main.c
#include "mylib.h"
int main() {
 foo(); // This will call the C++ function bar() via the wrapper
 return 0;
}

🔹 Step 4: Build & Run

g++ -c mylib.cpp        # Compile the C++ code
gcc -c main.c           # Compile the C code
g++ -o app main.o mylib.o   # Link using g++ to bring in the C++ runtime

./app

You should see:

Hello from C++ function 'bar()'!

This is the simplest working example of C calling into C++ safely using a wrapper. Let me know if you want to expand this into classes or dynamic memory interfaces!

You said:

How about a C++ class that instantiates a C struct with a member function pointer? Then the library’s consumer would call the struct’s function by dereferencing the pointed to member.

ChatGPT said:

Great idea — you’re describing a pattern similar to how C interfaces can “hook into” C++ class functionality via function pointers in a struct. This technique allows a C consumer to call methods implemented in C++ without knowing they’re C++ — a neat form of polymorphism for interop.

Here’s a minimal working example of what you’re describing:


🧱 Overview


🔹 Step 1: mylib.cpp (C++ code)

// mylib.cpp
#include <iostream>
#include "mylib.h"
class Bar {
public:
 void do_work() {
    std::cout << "Bar::do_work() from C++ class\n";
 }
};
// Static wrapper function matching C function pointer signature
static void bar_callback(void* ctx) {
  Bar* self = static_cast<Bar*>(ctx);
  self->do_work();
}
extern "C" CallbackInterface* create_interface()
{
 // Allocate on heap so it lives beyond function scope
  Bar* bar = new Bar();
  CallbackInterface* iface = new CallbackInterface;
  iface->ctx = static_cast<void*>(bar);
  iface->callback = &bar_callback;
 return iface;
}
extern "C" void destroy_interface(CallbackInterface* iface) {
 if (!iface) return;
 delete static_cast<Bar*>(iface->ctx); // Free the C++ object
 delete iface; // Free the struct
}

🔹 Step 2: mylib.h (C-compatible header)

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
 void* ctx;
 void (*callback)(void* ctx);
} CallbackInterface;
CallbackInterface* create_interface();
void destroy_interface(CallbackInterface* iface);
#ifdef __cplusplus
}
#endif
#endif // MYLIB_H

🔹 Step 3: main.c (C consumer)

// main.c
#include "mylib.h"
int main() {
  CallbackInterface* iface = create_interface();
 // Call C++ logic via function pointer
  iface->callback(iface->ctx);
 destroy_interface(iface);
 return 0;
}

🔹 Step 4: Build and Run

g++ -c mylib.cpp        # Compile C++ part
gcc -c main.c           # Compile C part
g++ -o app main.o mylib.o   # Link with C++ linker
./app

Expected output:

Bar::do_work() from C++ class

✅ Advantages


Let me know if you’d like:

This pattern scales well for plug-in architectures or when wrapping modern C++ libraries in C-accessible APIs.

You said:

Nice, thanks. You’re crushing it. Hey just wondering if we can do all this without using the heap? Can a static class produce two copies of CallbackInterface like this?

CallbackInterface iface;
CallbackInterface iface2;
static_class_create_interface(iface, 1);
static_class_create_interface(iface2, 2);

The solution you provided would work, but I’m heap averse since I’m working in embedded.

ChatGPT said:

Absolutely — your instinct is 100% right for embedded. Avoiding the heap in embedded systems is best practice, and yes, you can do this entirely with statically allocated CallbackInterface instances — you just need to ensure that:

  1. The backing C++ object (Bar) is also stored statically or externally.
  2. The C++ class has a way to distinguish or access the correct instance based on the interface.

Let’s walk through a version of your idea using stack/static allocation only, no heap, with a statically instantiated class wrapper.


✅ Key Design Points


🔹 Step 1: mylib.cpp (no heap, static-only)

// mylib.cpp
#include <iostream>
#include "mylib.h"
class Bar {
public:
 Bar(int id) : id_(id) {}
 void do_work() {
    std::cout << "Bar[" << id_ << "]::do_work()\n";
 }
private:
 int id_;
};
// Two static instances
static Bar bar1(1);
static Bar bar2(2);
// Static wrapper matching C callback signature
static void bar_callback(void* ctx) {
  Bar* self = static_cast<Bar*>(ctx);
  self->do_work();
}
// Fills a CallbackInterface from a static instance based on ID
extern "C" void static_class_create_interface(CallbackInterface* out, int id) {
 if (!out) return;
 if (id == 1) {
    out->ctx = static_cast<void*>(&bar1);
 } else if (id == 2) {
    out->ctx = static_cast<void*>(&bar2);
 } else {
    out->ctx = nullptr;
    out->callback = nullptr;
 return;
 }
  out->callback = &bar_callback;
}

🔹 Step 2: mylib.h (unchanged)

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
 void* ctx;
 void (*callback)(void* ctx);
} CallbackInterface;
void static_class_create_interface(CallbackInterface* out, int id);
#ifdef __cplusplus
}
#endif
#endif // MYLIB_H

🔹 Step 3: main.c (your example — now totally valid!)

// main.c
#include "mylib.h"
int main() {
  CallbackInterface iface;
  CallbackInterface iface2;
 static_class_create_interface(&iface, 1);
 static_class_create_interface(&iface2, 2);
  iface.callback(iface.ctx);
  iface2.callback(iface2.ctx);
 return 0;
}

🔹 Step 4: Compile & Run

g++ -c mylib.cpp
gcc -c main.c
g++ -o app main.o mylib.o
./app

Expected output:

Bar[1]::do_work() Bar[2]::do_work()

🛠️ Notes for Embedded Use


If you’d prefer zero static state in the C++ library, the same principle works with caller-owned objects passed in from the C layer — let me know if that fits your architecture better.

This pattern you’re building toward is rock-solid for safe, low-overhead C/C++ interop in embedded. Let me know if you need variants for const correctness, non-member callbacks, or portability across compilers like Keil, IAR, or Clang.

You said:

caller-owned objects passed in from the C layer does make more sense. I also do need portability for gcc and IAR. I’m curious about your comment “This is heap-free, reentrant-safe (if used carefully), and efficient.” What do you mean by ‘if used carefully’?

ChatGPT said:

Great questions — you’re clearly building for a high-reliability system, and you’re thinking in all the right directions. Let’s break this down:


🧠 “If used carefully” — what it means

In the phrase:

“heap-free, reentrant-safe (if used carefully), and efficient”

The caveat “if used carefully” is about reentrancy, which can be subtle in embedded C/C++ interop. Here’s what it means specifically in this context:


🔸 1. Avoid Global State That Isn’t Thread/Reentrant Safe

Even if your C API is heap-free and exposes only CallbackInterface with user-provided context, the implementation in C++ might use global or static variables inside the class.

Dangerous:

class Bar {
 int last_value; // Not safe if shared or mutated across calls
public:
 void do_work(int val) {
    last_value = val; // Race condition if shared across threads or calls
 }
};

Safe:


🔸 2. Don’t Store Pointers to Stack Objects in Long-Lived Structs

If the C layer passes a stack variable as context:

Bar my_local_bar;
CallbackInterface iface = { &my_local_bar, callback };

…and iface outlives the function that created my_local_bar, you get dangling pointers. Common in event-driven systems.

✔️ Best practice: the caller owns the lifetime of both the C++ object and the CallbackInterface that wraps it.


🔸 3. Don’t Call Back into the Same Interface From Multiple Threads Without Sync

If the same CallbackInterface is invoked from multiple threads or interrupt contexts, and it wraps a non-thread-safe C++ object, you need to handle this in the C++ class — e.g., using mutexes or atomic flags (portable ones, like CMSIS or C11 atomics).


🔸 4. Ensure callback(ctx) Always Validates the ctx

Never trust the context blindly:

static void callback(void* ctx) {
 if (!ctx) return;
  Bar* self = static_cast<Bar*>(ctx);
  self->do_work();
}

This helps avoid silent faults if an uninitialized struct is used.


🛠️ Portability: GCC and IAR

This approach is fully portable across GCC and IAR as long as:

⚠️ Compiler Notes


✅ Summary: Reentrant-safe If…

PracticeRisk if IgnoredSafe Alternative
No shared static/global stateRace conditionsKeep data in per-object context
No heapMemory leaks, fragmentationUse caller-owned buffers
No stack pointers in callback structsDangling pointerUse static/global/heap-backed instances
Validate ctxCrashesNull-check before casting
Thread-safe accessData corruptionUse locks or atomic access if needed

Would you like a working version of a caller-owned-object design pattern too? I can walk you through a version where main.c owns a C++ object and passes it to a registration function, fully heap-free.

You said:

Yes please. Another minimum working example please.

ChatGPT said:

Perfect — here’s a minimal working example of a heap-free, caller-owned object interface, where a C program owns a C++ object’s memory and uses a CallbackInterface to invoke behavior on it. All portable across GCC and IAR (as long as your toolchain supports compiling C++).


📦 Summary

You’ll get:


🔹 Step 1: mylib.h (public C header)

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
 void* ctx;
 void (*callback)(void* ctx);
} CallbackInterface;
// Initialize an interface using a caller-owned C++ object
void init_interface(void* cpp_obj, CallbackInterface* out);
#ifdef __cplusplus
}
#endif
#endif // MYLIB_H

🔹 Step 2: mylib.cpp (C++ implementation, no heap)

// mylib.cpp
#include <iostream>
#include "mylib.h"
class Bar {
public:
 Bar(int id) : id_(id) {}
 void do_work() {
    std::cout << "Bar[" << id_ << "]::do_work()\n";
 }
private:
 int id_;
};
// The callback must match the C signature
static void bar_callback(void* ctx) {
 if (!ctx) return;
  Bar* self = static_cast<Bar*>(ctx);
  self->do_work();
}
// C-visible initializer, caller provides cpp_obj
extern "C" void init_interface(void* cpp_obj, CallbackInterface* out) {
 if (!cpp_obj || !out) return;
  out->ctx = cpp_obj;
  out->callback = &bar_callback;
}

🔹 Step 3: main.c (C code owns the object and interface)

// main.c
#include "mylib.h"
// Pretend opaque C++ type
typedef struct Bar Bar;
int main() {
 // Static storage for the C++ object
 char bar_storage[sizeof(Bar)];
 // Placement-new construction of the C++ object
  Bar* bar1 = (Bar*)bar_storage;
 extern void* create_bar_in_place(void* mem, int id);
 create_bar_in_place(bar1, 42);
  CallbackInterface iface;
 init_interface(bar1, &iface);
  iface.callback(iface.ctx);
 return 0;
}

🔹 Step 4: Add placement.cpp for create_bar_in_place

// placement.cpp
#include "mylib.h"
#include <new>
// Provide construction API for embedded C-side placement class Bar;
extern "C" void* create_bar_in_place(void* mem, int id) {
 return new (mem) Bar(id); // Placement-new into caller memory
}

🧪 Step 5: Build & Run

g++ -c mylib.cpp
g++ -c placement.cpp
gcc -c main.c
g++ -o app main.o mylib.o placement.o
./app

Expected output:

Bar[42]::do_work()

✅ Embedded Notes

inline void* operator new(size_t, void* ptr) noexcept { return ptr; }

Would you like me to give you a version where Bar is only forward-declared in the header (fully opaque to C), or optionally extend this for arrays / multiple interfaces?

You said:
Yes, give me a version where Bar is only forward-declared in the header please

Excellent — keeping Bar opaque to C is a clean and safe pattern for interop. Below is a fully working heap-free setup, where:


✅ Benefits


📁 File Breakdown

. ├── main.c             # C-side: owns Bar memory and interface
  ├── mylib.h            # Shared header, C/C++ compatible
  ├── mylib.cpp          # C++ logic (Bar and interface init)
  ├── placement.cpp      # Placement-new constructor

🔹 mylib.h — forward-declare Bar for C

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct Bar Bar; // Opaque type, no definition in C
typedef struct {
 void* ctx;
 void (*callback)(void* ctx);
} CallbackInterface;
// Set up the interface to point to a Bar instance
void init_interface(void* cpp_obj, CallbackInterface* out);
// Create Bar object in user-provided memory
void* create_bar_in_place(void* mem, int id);
#ifdef __cplusplus
}
#endif
#endif // MYLIB_H

🔹 mylib.cpp — C++ logic and wrapper

// mylib.cpp
#include <iostream>
#include "mylib.h"
class Bar {
public:
 Bar(int id) : id_(id) {}
 void do_work() {
    std::cout << "Bar[" << id_ << "]::do_work()\n";
 }
private:
 int id_;
};
static void bar_callback(void* ctx) {
 if (!ctx) return;
  Bar* self = static_cast<Bar*>(ctx);
  self->do_work();
}
extern "C" void init_interface(void* cpp_obj, CallbackInterface* out) {
 if (!cpp_obj || !out) return;
  out->ctx = cpp_obj;
  out->callback = &bar_callback;
}

🔹 placement.cpp — placement constructor

// placement.cpp
#include "mylib.h"
#include <new>
class Bar;
extern "C" void* create_bar_in_place(void* mem, int id) {
 return new (mem) Bar(id);
}

IAR note: if the linker complains about placement new, add this in placement.cpp:

void* operator new(size_t, void* ptr) noexcept { return ptr; }

🔹 main.c — only sees opaque Bar

// main.c
#include "mylib.h"
#include <stdio.h>
#include <string.h>
int main() {
 char bar_buffer[64]; // Big enough to hold Bar (assumed max size)
 memset(bar_buffer, 0, sizeof(bar_buffer));
  Bar* bar = (Bar*)bar_buffer;
 create_bar_in_place(bar, 123); // Construct object
  CallbackInterface iface;
 init_interface(bar, &iface);
  iface.callback(iface.ctx);
 return 0;
}

🛠️ Build & Run

g++ -c mylib.cpp
g++ -c placement.cpp
gcc -c main.c
g++ -o app main.o mylib.o placement.o
./app

Output:

Bar[123]::do_work()

🧯 Optional Safety Checks

If you’re paranoid (as one should be in embedded), you can:


Let me know if you’d like to:

This pattern is production-grade and can scale to embedded plugins, drivers, or HAL wrappers.