C++ Language Support

This document provides a comprehensive guide on using the C++ language within the SDK development. It emphasizes the importance of maintaining compatibility with C environments and outlines best practices for efficient and effective interoperation between C and C++ code.

Core Feature Support

The SDK supports a carefully selected subset of C++ features tailored for embedded development environments. These constraints are driven by the need to optimize for limited memory, processing power, and power consumption inherent to embedded systems. As a result, some advanced C++ features are disabled by default to conserve resources and ensure predictable performance. Here’s a detailed look at the supported features:

C++ Feature

Feature

Default State

Enable Method

Exception Handling

Disabled

Enable via compile flags such as -fexceptions.

RTTI (Type Info)

Disabled

Activate with -frtti compile flag.

STL Containers

Disabled

Link with libstdc++ to utilize standard template libraries.

These features are essential in more heavyweight applications but can lead to increased binary sizes and execution overhead, making them less suitable for constrained environments unless explicitly needed.

Configuration Guide

Project Configuration

To begin using C++ in your project, you must configure the environment by setting a specific macro in the project files. This serves as a trigger to enable C++ support throughout the SDK. To do this, include the following line in the root configuration file located at sdk\src\mcu\rtl87x3\system_rtl87x3.c:

#define CXX_support

This directive signals the build system to accommodate C++ files and linkages during compilation and linking.

C/C++ Interoperation

Interoperability between C and C++ is a critical aspect of mixed-language programming, offering the ability to leverage C++ features in C-based projects. This is achieved by ensuring that C++ functions intended for use in C are exposed with C linkage using the extern C construct. This guides the compiler to use C naming conventions, ensuring compatibility with C:

extern "C"
{
    // Expose C interface
    void my_cpp_api_init()
    {
        // Initialization code
    }
}

In your C program, you can then call these C++ functions as if they were originally implemented in C:

extern void my_cpp_api_init();

void app_main()
{
    my_cpp_api_init();
}

Defining Main in C++

In projects where app_main is defined in a C++ source file, it’s crucial to declare it with C linkage to ensure the C runtime can correctly identify and call the function:

extern "C" void app_main()
{
}

This ensures seamless integration with the existing C components of your application.

Heap Allocation and Deallocation

In embedded systems, managing memory effectively is vital given the limited resources. By overriding the global operator new and operator delete functions, you can tailor memory allocation strategies to suit the system’s requirements, providing control over how memory is requested and released:

void *operator new(std::size_t size)
{
    void* ptr = malloc(size);  // Allocate memory
    return ptr;
}

void *operator new[](std::size_t size)
{
    void* ptr = malloc(size);  // Allocate array memory
    return ptr;
}

void operator delete(void* ptr)
{
    free(ptr);  // Free memory
}

void operator delete[](void* ptr)
{
    free(ptr);  // Free array memory
}

This allows developers to integrate with existing memory management systems, such as custom heap managers or pools, ensuring efficiency and predictability in memory usage.

Static and Dynamic Library Initialization

Initialization of libraries is critical for setting up an application before its main logic begins execution. Statically linked libraries are typically initialized at link time, providing a straightforward and deterministic setup:

  • Static Libraries: Initialization and cleanup are arranged during the linking process, ensuring all required resources are available when the application starts.

  • Dynamic Libraries: These libraries provide flexibility, loading either at application start or dynamically as needed through mechanisms like dlopen.

Complex applications may require specific initialization routines to run even before main. This can be structured using:

  • Atexit Registration: Registering cleanup functions to run at program exit.

  • Constructor Functions: Using constructors to execute code as part of static object initialization.

  • Initialization Arrays: Directly inserting functions into the init section of the binary, providing precise control over initialization order and timing.

The reference code can be found in sdk\src\mcu\rtl87x3\system_rtl87x3.c.

#if defined (__GNUC__)
    typedef void (*init_fn_t)(void);
    extern uint32_t _init_array;
    extern uint32_t _einit_array;
    init_fn_t *fp;
    for (fp = (init_fn_t *)&_init_array; fp < (init_fn_t *)&_einit_array; fp++)
        {
        (*fp)();
    }
#elif defined (__ARMCC_VERSION)
    typedef void PROC();
    extern const unsigned long SHT$$INIT_ARRAY$$Base[];
    extern const unsigned long SHT$$INIT_ARRAY$$Limit[];

    const unsigned long *base = SHT$$INIT_ARRAY$$Base;
    const unsigned long *lim  = SHT$$INIT_ARRAY$$Limit;

    for (; base != lim; base++)
        {
        PROC *proc = (PROC *)((const char *)base + *base);
        (*proc)();
    }
#endif

This level of control is crucial for environments where initialization order critically affects functionality and stability.

Exception Safety

Managing exceptions is a complex but essential aspect of robust application design. In embedded environments, where exceptions might be disabled at the hardware layer to save resources, alternative strategies must be employed:

  1. Disable Exceptions in Low-Level Code: At the hardware interface layer, explicitly disable exceptions to maintain performance and reduce binary size.

  2. Unified Handling in Business Logic: Employ unified strategies for handling errors and exceptions consistently across the application. This often involves using error codes or enumeration types for error propagation.

  3. Resource Management: Employ the RAII pattern to ensure that resources are automatically released when they go out of scope, mitigating resource leaks and ensuring clean transitions.

class GpioGuard
{
public:
    GpioGuard(gpio_num_t num) : pin(num)
    {
        gpio_reset_pin(pin);  // Initialize resource
        gpio_set_direction(pin, GPIO_MODE_OUTPUT);
    }

    ~GpioGuard()
    {
        gpio_reset_pin(pin);  // Release resource
    }

private:
    gpio_num_t pin;
};

RAII helps encapsulate resource management within objects, ensuring deterministic release of resources such as memory, file handles, or hardware interfaces.

Performance Optimization

Efficient use of resources is paramount in embedded systems. Here are strategies to enhance performance with a focus on minimizing memory footprint:

  1. Remove Unused Features: Use the -fno-rtti flag to disable runtime type information, reducing size and improving performance.

  2. Eliminate Dead Code: Enable -ffunction-sections and -fdata-sections followed by linker flags such as --gc-sections to remove unused functions and data.

  3. Control Template Instantiation: Carefully control the instantiation of templates to avoid code bloat—prefer using explicit instantiations where possible to manage the increase in binary size resulting from template use.

These strategies collectively ensure that the application remains efficient, responsive, and maintains a manageable footprint, even as complexity grows. By adhering to these guidelines and principles, developers can effectively integrate C++ into embedded applications, leveraging its expressive power and features.