From fb9143e2c9f273facc1925072cbc033842e183f6 Mon Sep 17 00:00:00 2001 From: Sudeep Mohanty Date: Mon, 26 Jan 2026 11:20:02 +0100 Subject: [PATCH] feat(cmakev2): Added plugins example for cmakev2 This commit updates the examples/build_system/cmake/plugins example for the new build system and adds the newly created example at examples/build_system/cmakev2/features/plugins. --- examples/build_system/cmakev2/README.md | 13 ++ .../cmakev2/features/plugins/CMakeLists.txt | 36 +++++ .../cmakev2/features/plugins/README.md | 132 ++++++++++++++++++ .../components/plugin_hello/CMakeLists.txt | 3 + .../components/plugin_hello/plugin_hello.c | 45 ++++++ .../components/plugin_nihao/CMakeLists.txt | 5 + .../components/plugin_nihao/plugin_nihao.c | 45 ++++++ .../plugins/components/plugins/CMakeLists.txt | 3 + .../components/plugins/include/plugins_api.h | 48 +++++++ .../plugins/components/plugins/linker.lf | 13 ++ .../plugins/components/plugins/plugins.c | 73 ++++++++++ .../features/plugins/main/CMakeLists.txt | 3 + .../plugins/main/plugins_example_main.c | 13 ++ 13 files changed, 432 insertions(+) create mode 100644 examples/build_system/cmakev2/features/plugins/CMakeLists.txt create mode 100644 examples/build_system/cmakev2/features/plugins/README.md create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf create mode 100644 examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c create mode 100644 examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt create mode 100644 examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c diff --git a/examples/build_system/cmakev2/README.md b/examples/build_system/cmakev2/README.md index 7810995150..02c8ac14a1 100644 --- a/examples/build_system/cmakev2/README.md +++ b/examples/build_system/cmakev2/README.md @@ -145,6 +145,19 @@ Demonstrates building multiple configurations of a single application. - CMake presets for easy configuration switching - Separate build directories for side-by-side builds +--- + +### plugins +**Location:** [plugins/](./features/plugins/) + +Demonstrates link-time plugin registration using whole-archive linking and constructor functions. + +**Key Concepts:** `WHOLE_ARCHIVE`, constructor functions, dynamic registration, static registration + +**When to use:** When you need a flexible plugin system that automatically discovers and registers plugins at runtime. + +--- + ## Build System v2 API Quick Reference ### Project Setup diff --git a/examples/build_system/cmakev2/features/plugins/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/CMakeLists.txt new file mode 100644 index 0000000000..655c2d75c2 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/CMakeLists.txt @@ -0,0 +1,36 @@ +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmakev2/idf.cmake) +project(plugins C CXX ASM) + +# Use project-level initialization instead of idf_project_default +idf_project_init() + +# Create executable with explicit component specification +# This approach includes plugin components in the executable +idf_build_executable(${CMAKE_PROJECT_NAME}.elf + COMPONENTS main plugins plugin_hello plugin_nihao) + +# Generate binary +idf_build_binary(${CMAKE_PROJECT_NAME}.elf + OUTPUT_FILE "${CMAKE_BINARY_DIR}/${CMAKE_PROJECT_NAME}.bin" + TARGET ${CMAKE_PROJECT_NAME}_binary) + +# Create flash target +idf_flash_binary(${CMAKE_PROJECT_NAME}_binary + TARGET app-flash + NAME "app" + FLASH) + +# Check binary size +idf_check_binary_size(${CMAKE_PROJECT_NAME}_binary) + +# Create app target +add_custom_target(app ALL DEPENDS ${CMAKE_PROJECT_NAME}_binary) + +# Generate metadata +idf_build_generate_metadata(BINARY ${CMAKE_PROJECT_NAME}_binary) + +# Create other utility targets +idf_create_menuconfig(${CMAKE_PROJECT_NAME}.elf + TARGET menuconfig) diff --git a/examples/build_system/cmakev2/features/plugins/README.md b/examples/build_system/cmakev2/features/plugins/README.md new file mode 100644 index 0000000000..37c81a22dd --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/README.md @@ -0,0 +1,132 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-P4 | ESP32-S2 | ESP32-S3 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | -------- | -------- | + +# Link Time Plugins Registration + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +This example demonstrates features of ESP-IDF build system related to link time registration of plugins. Link time registration of plugins is often used to add multiple implementations of a certain feature without having to make the application aware of all these implementations. With this approach, adding a new implementation is often as simple as adding a new source file or a new component. Aside from plugins, link time registration may be used for other purposes, such as automatic registration of test cases. + +# Overview of link time registration + +When using link time registration, there are typically two challenges: getting the plugin code linked into the application and enumerating the plugins at run time. The following sections explain these problems and the solutions to them. + +## Ensuring that the plugin code is included into the executable + +When GNU linker (ld) links a static library, it considers each object file in the library separately. The object file is ignored if it doesn't resolve any unresolved references known to the linker at that moment. With link-time plugin registration this is typically the case — the application doesn't explicitly reference any plugins, so the linker sees no reason to include the respective object files into the executable. + +Aside from adding an explicit reference from the application to the plugin object file, there are two common ways to resolve this issue: + +1. Link the object file of the plugin directly to the executable, and not via a static library. +2. Instruct the linker to include every object file of a library into the executable, even those which don't resolve any references from the rest of the application. For GNU ld this can be achieved by surrounding the library on the linker command line with `-Wl,--whole-archive` and `-Wl,--no-whole-archive` flags. + +ESP-IDF build system implements the 2nd approach by providing a `WHOLE_ARCHIVE` component property. It can be set in component CMakeLists.txt in two ways. One is to add `WHOLE_ARCHIVE` option when calling `idf_component_register`: + +```cmake +idf_component_register(SRCS file.c + WHOLE_ARCHIVE) +``` + +Another is to call `idf_component_set_property` after registering the component: +```cmake +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) +``` + +This will instruct the build system to surround the component library with whole-archive flags on the linker command line, ensuring that all object files from the library get included into the final application. + +Note that the linker also performs "garbage collection" at the end of the linking process, eliminating all functions and global variables which are not referenced anywhere. This is addressed in the current example using `__attribute__((constructor))` function attribute (for dynamic registration) and `KEEP()` linker fragment flag (for static registration). + +## Registering and enumerating the plugins + +To make use of the plugins linked into the application, the application must somehow enumerate them. There are 2 common ways to register and enumerate the plugins: dynamic and static. This example demonstrates both approaches. + +### Dynamic registration (or self-registration) + +With this approach, each plugin module has a function with `__attribute__((constructor))` attribute (in C) or a static global object with a non-trivial constructor (in C++). Startup code calls all constructor functions before the application entry point (`app_main`) is executed. Plugin constructor functions then register themselves by calling a function defined in the application. As an example, this registration function can, add structures describing the plugins into a linked list. + +### Static registration + +This approach relies on plugin description structures being collected into an array at link time. + +For each plugin, a structure describing the plugin (or a pointer to it) is placed into some special input section using `__attribute((section(".plugins_desc")))`. Using the linker script generator features in ESP-IDF, all entries from this input section can be gathered into a continuous array, surrounded by some symbols (e.g. `_plugins_array_start`, `_plugins_array_end`). At run time, the application casts the `&_plugins_array_start` pointer to the plugin description structure pointer and then iterates over structures collected from all plugins. + +## Example code overview + +Unlike `cmakev1` which used a global `COMPONENTS` variable, `cmakev2` uses an explicit dependency-driven model. For plugin architectures, this example demonstrates the use of `idf_build_executable()` with specific components to ensure plugins are properly included and linked. + +```cmake +idf_project_init() +idf_build_executable(${CMAKE_PROJECT_NAME}.elf + COMPONENTS main plugins plugin_hello plugin_nihao) +``` + +This example contains 4 components: + +* `main` — Only calls two sample functions defined in `plugins` component. +* `plugins` — The main part of the plugin system. + + For dynamic registration, it provides an API which plugin components call to register themselves (`example_plugin_register`). + + It also provides two sample functions for the application: + - `example_plugins_list`: prints the list of registered plugins. This function demonstrates static registration. + - `example_plugins_greet`: calls a function defined by each plugin with a given argument. This function demonstrates working with dynamically registered plugins. +* `plugin_hello` and `plugin_nihao` — two almost identical components, each provides one plugin. + + Note that multiple plugins may also be defined in the same component. + +Please refer to the comments in the example code for a more detailed description. + +## How to use example + +### Hardware Required + +This example runs on any ESP development board, no special hardware is required. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +``` +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type `Ctrl-]`.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +Nihao plugin performing self-registration... +Successfully registered plugin 'Nihao' +Hello plugin performing self-registration... +Successfully registered plugin 'Hello' +I (325) cpu_start: Starting scheduler on PRO CPU. +I (0) cpu_start: Starting scheduler on APP CPU. +List of plugins: +- Plugin 'Hello', function greet=0x400d4f40 +0x400d4f40: plugin_hello_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_hello/plugin_hello.c:14 + +- Plugin 'Nihao', function greet=0x400d4f70 +0x400d4f70: plugin_nihao_greet at /home/user/esp/esp-idf/examples/build_system/cmake/plugins/build/../components/plugin_nihao/plugin_nihao.c:14 + +Calling greet function of plugin 'Hello'... +Hello, World! +Done with greet function of plugin 'Hello'. +Calling greet function of plugin 'Nihao'... +你好 World! +Done with greet function of plugin 'Nihao'. +``` + +## Troubleshooting + +When implementing the approaches described in this example, the following issues may occur: + +* Plugin self-registration function (constructor function) is never called. To troubleshoot this, check the application .map file — it is generated in the `build` directory of the project. Look for the object file where the constructor function is defined. If the object file and the constructor function are missing, it means that the object file was discarded. Double-check that the `WHOLE_ARCHIVE` property of the component is set correctly. Verify that on the linker command line, the component library is surrounded by `-Wl,--whole-archive`, `-Wl,--no-whole-archive`. To see the linker command line, build the project with verbose (-v) flag. +* With static registration, the plugin description structure is missing from the link-time array. Same as in the case above, start by examining the map file. + - If the plugin object file is missing from the map file, double-check that the `WHOLE_ARCHIVE` property of the component is set correctly (see the instructions above). + - If the plugin object file is present, but the plugin description structure is missing, check that the linker fragment rule and `__attribute((section(...)))` use the same section name. Check that the linker fragment rule uses `KEEP()` flag. + - If the plugin description structure is in the map file but is not located inside the link-time array (is located in some other section), check the generated linker script found in the build directory (`build/esp-idf/esp_system/ld/sections.ld`). Check that the rules for placing the plugin description structure have correct precedence with respect to other rules in the linker script. + diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt new file mode 100644 index 0000000000..1a1ee9fbe4 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS plugin_hello.c + PRIV_REQUIRES plugins + WHOLE_ARCHIVE) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c new file mode 100644 index 0000000000..fc4c4ed755 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_hello/plugin_hello.c @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "plugins_api.h" + +/** + * This is an example function implemented by the plugin. + */ +static void plugin_hello_greet(const char* arg) +{ + if (arg == NULL) { + return; + } + printf("Hello, %s!\n", arg); +} + +/* The code below demonstrates both static and dynamic registration approaches. */ + +/** + * Static registration of this plugin can be achieved by defining the plugin description + * structure and placing it into .plugins_desc section. + * The name of the section and its placement is determined by linker.lf file in 'plugins' component. + */ +static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = { + .name = "Hello", + .greet = &plugin_hello_greet +}; + +/** + * Dynamic registration of this plugin can be achieved by calling plugin registration function + * ('example_plugin_register') from a "constructor" function. Constructor function is called automatically + * during application startup. + */ +static void __attribute__((constructor)) plugin_hello_self_register(void) +{ + printf("Hello plugin performing self-registration...\n"); + example_plugin_register(&(example_plugin_desc_t){ + .name = "Hello", + .greet = &plugin_hello_greet + }); +} diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt new file mode 100644 index 0000000000..b215ea454a --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS plugin_nihao.c + PRIV_REQUIRES plugins) + +# This is equivalent to adding WHOLE_ARCHIVE option to the idf_component_register call above: +idf_component_set_property(${COMPONENT_NAME} WHOLE_ARCHIVE TRUE) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c new file mode 100644 index 0000000000..0e5c7ba4ae --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugin_nihao/plugin_nihao.c @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include "plugins_api.h" + +/** + * This is an example function implemented by the plugin. + */ +static void plugin_nihao_greet(const char* arg) +{ + if (arg == NULL) { + return; + } + printf("你好 %s!\n", arg); +} + +/* The code below demonstrates both static and dynamic registration approaches. */ + +/** + * Static registration of this plugin can be achieved by defining the plugin description + * structure and placing it into .plugins_desc section. + * The name of the section and its placement is determined by linker.lf file in 'plugins' component. + */ +static const example_plugin_desc_t __attribute__((section(".plugins_desc"),used)) PLUGIN = { + .name = "Nihao", + .greet = &plugin_nihao_greet +}; + +/** + * Dynamic registration of this plugin can be achieved by calling plugin registration function + * ('example_plugin_register') from a "constructor" function. Constructor function is called automatically + * during application startup. + */ +static void __attribute__((constructor)) plugin_nihao_self_register(void) +{ + printf("Nihao plugin performing self-registration...\n"); + example_plugin_register(&(example_plugin_desc_t){ + .name = "Nihao", + .greet = &plugin_nihao_greet + }); +} diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt new file mode 100644 index 0000000000..c132c9bfad --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS plugins.c + INCLUDE_DIRS include + LDFRAGMENTS linker.lf) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h b/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h new file mode 100644 index 0000000000..6ff6e76e6e --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/include/plugins_api.h @@ -0,0 +1,48 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/* This structure describes the plugin to the rest of the application */ +typedef struct { + /* A pointer to the plugin name */ + const char* name; + + /* A function which the plugin provides to the application. + * In this example, this function prints something to the console + * depending on the value of the argument 'arg'. + */ + void (*greet)(const char* arg); +} example_plugin_desc_t; + +/** + * @brief Register the plugin with the application + * This function is called from each plugin's "constructor" function. + * It adds the plugin to the list. + * @param plugin_desc Pointer to the structure which describes the given plugin. + */ +void example_plugin_register(const example_plugin_desc_t* plugin_desc); + +/** + * @brief Print the list of registered plugins to the console. + * This function is called from the application. + */ +void example_plugins_list(void); + +/** + * @brief Invoke 'greet' function of each registered plugin with the given argument. + * This function is called from the application. + * @param arg argument to pass to plugins' greet functions. + */ +void example_plugins_greet(const char* arg); + + +#ifdef __cplusplus +} +#endif diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf b/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf new file mode 100644 index 0000000000..0b1dafff45 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/linker.lf @@ -0,0 +1,13 @@ +[sections:plugins_desc] +entries: + .plugins_desc + +[scheme:plugins_desc_default] +entries: + plugins_desc -> flash_rodata + +[mapping:plugins_desc] +archive: * +entries: + * (plugins_desc_default); + plugins_desc -> flash_rodata KEEP() SORT(name) SURROUND(plugins_array) diff --git a/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c b/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c new file mode 100644 index 0000000000..0019d35cb2 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/components/plugins/plugins.c @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include +#include "plugins_api.h" + +/** + * Demonstration of dynamic registration (self-registration): + * + * - example_plugin_register function is called from "constructor" functions of each plugin. + * Information about the plugin is passed inside 'example_plugin_desc_t' structure. + * This function adds each plugin description into linked list (s_plugins_list). + * + * - example_plugins_greet function iterates over the linked list. + */ + +struct plugin_record { + example_plugin_desc_t plugin_desc; + LIST_ENTRY(plugin_record) list_entry; +}; + +static LIST_HEAD(plugins_list, plugin_record) s_plugins_list = LIST_HEAD_INITIALIZER(s_plugins_list); + +void example_plugin_register(const example_plugin_desc_t* plugin_desc) +{ + struct plugin_record *record = (struct plugin_record *) malloc(sizeof(struct plugin_record)); + if (record == NULL) { + abort(); + } + memcpy(&record->plugin_desc, plugin_desc, sizeof(*plugin_desc)); + + static struct plugin_record *tail = NULL; + if (tail == NULL) { + LIST_INSERT_HEAD(&s_plugins_list, record, list_entry); + } else { + LIST_INSERT_AFTER(tail, record, list_entry); + } + tail = record; + printf("Successfully registered plugin '%s'\n", plugin_desc->name); +} + +void example_plugins_greet(const char* arg) +{ + struct plugin_record *it; + LIST_FOREACH(it, &s_plugins_list, list_entry) { + printf("Calling greet function of plugin '%s'...\n", it->plugin_desc.name); + (*it->plugin_desc.greet)(arg); + printf("Done with greet function of plugin '%s'.\n", it->plugin_desc.name); + } +} + +/** + * Demonstration of static registration. + * Symbols '_plugins_array_start' and '_plugins_array_end' mark the beginning and end + * of the array where 'example_plugin_desc_t' structures are placed by the linker. + * The names of these variables are determined by linker.lf in 'plugins' component, + * look for 'SURROUND(plugins_array)'. + */ + +void example_plugins_list(void) +{ + printf("List of plugins:\n"); + extern const example_plugin_desc_t _plugins_array_start; + extern const example_plugin_desc_t _plugins_array_end; + for (const example_plugin_desc_t* it = &_plugins_array_start; it != &_plugins_array_end; ++it) { + printf("- Plugin '%s', function greet=%p\n", it->name, it->greet); + } +} diff --git a/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt b/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt new file mode 100644 index 0000000000..2c35b46ce1 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "plugins_example_main.c" + INCLUDE_DIRS "." + PRIV_REQUIRES plugins) diff --git a/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c b/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c new file mode 100644 index 0000000000..22af957366 --- /dev/null +++ b/examples/build_system/cmakev2/features/plugins/main/plugins_example_main.c @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include "plugins_api.h" + +void app_main(void) +{ + example_plugins_list(); + example_plugins_greet("World"); +}