feat(build): add CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY option

libstdc++ selects between a lock-free and a mutex-based policy for
std::shared_ptr reference counting based on this macro. Enabling this
option forces the lock-free policy, which avoids pulling in the
mutex-based implementation and can reduce code size.

Note: the lock-free policy avoids embedding a mutex in each control
block, saving about 88 bytes per shared_ptr, and improves performance
by using atomic operations instead of mutex locking.

Note: this option changes the ABI of libstdc++ atomic/shared_ptr
helpers. It can be incompatible when linking against prebuilt
libraries that were compiled without it, leading to link errors or
undefined runtime behavior. Only enable it if all C++ objects and
libraries in the build use the same setting.
This commit is contained in:
Alexey Lapshin
2026-07-23 18:20:25 +07:00
parent 480d5f2559
commit b3ae652f21
16 changed files with 239 additions and 0 deletions
+33
View File
@@ -94,3 +94,36 @@ target_link_libraries(${COMPONENT_LIB} PUBLIC libgcc_cxx)
if(NOT CONFIG_COMPILER_CXX_EXCEPTIONS)
target_link_libraries(${COMPONENT_LIB} INTERFACE "-u __cxx_fatal_exception")
endif()
# When the lock-free atomic policy is forced, verify the final executable does
# not end up with mixed __gnu_cxx::_Lock_policy instantiations (which can happen
# when linking prebuilt libraries built with a different setting).
if(CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY)
if(IDF_BUILD_V2)
function(cxx_check_atomic_policy target)
add_custom_command(TARGET ${target} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DELF_FILE=$<TARGET_FILE:${target}>
-DCMAKE_OBJDUMP=${CMAKE_OBJDUMP}
-P "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/check_cxx_atomic_policy.cmake"
COMMENT "Checking C++ atomic lock policy in linked executable"
VERBATIM)
endfunction()
idf_component_register_build_event_callback(EVENT POST_ELF CALLBACK cxx_check_atomic_policy)
else()
idf_build_get_property(build_dir BUILD_DIR)
idf_build_get_property(elf_target EXECUTABLE GENERATOR_EXPRESSION)
set(_cxx_atomic_policy_check_marker "${build_dir}/.cxx_atomic_policy_checked")
add_custom_command(OUTPUT "${_cxx_atomic_policy_check_marker}"
COMMAND ${CMAKE_COMMAND}
-DELF_FILE=$<TARGET_FILE:$<GENEX_EVAL:${elf_target}>>
-DCMAKE_OBJDUMP=${CMAKE_OBJDUMP}
-P "${CMAKE_CURRENT_LIST_DIR}/check_cxx_atomic_policy.cmake"
COMMAND ${CMAKE_COMMAND} -E touch "${_cxx_atomic_policy_check_marker}"
DEPENDS "$<TARGET_FILE:$<GENEX_EVAL:${elf_target}>>"
COMMENT "Checking C++ atomic lock policy in linked executable"
VERBATIM)
add_custom_target(cxx_check_atomic_policy DEPENDS "${_cxx_atomic_policy_check_marker}")
idf_build_add_post_elf_dependency("${CMAKE_PROJECT_NAME}.elf" cxx_check_atomic_policy)
endif()
endif()
@@ -0,0 +1,60 @@
# check_cxx_atomic_policy.cmake
#
# Post-build check that verifies the linked executable uses a single libstdc++
# atomic lock policy (__gnu_cxx::_Lock_policy).
#
# libstdc++ selects the policy used for std::shared_ptr reference counting based
# on _GLIBCXX_HAVE_ATOMIC_LOCK_POLICY. Mixing object files or prebuilt libraries
# that were compiled with different settings results in different _Lock_policy
# template instantiations being linked into the same image, which is an ODR
# violation and can cause undefined behavior at runtime.
#
# The policy is encoded in mangled symbol names, e.g. the substring
# "_Lock_policyE2" (atomic) or "_Lock_policyE1" (mutex). If the symbol table
# contains more than one distinct value, the policies are mixed.
#
# Invoked via "cmake -P" with:
# ELF_FILE - path to the linked executable
# CMAKE_OBJDUMP - objdump executable to inspect the symbol table
if(NOT DEFINED ELF_FILE)
message(FATAL_ERROR "check_cxx_atomic_policy.cmake: ELF_FILE is not set")
endif()
if(NOT CMAKE_OBJDUMP)
message(WARNING "check_cxx_atomic_policy.cmake: CMAKE_OBJDUMP is not set, skipping check")
return()
endif()
execute_process(
COMMAND "${CMAKE_OBJDUMP}" -t "${ELF_FILE}"
OUTPUT_VARIABLE objdump_output
ERROR_VARIABLE objdump_error
RESULT_VARIABLE objdump_result
)
if(NOT objdump_result EQUAL 0)
message(FATAL_ERROR
"check_cxx_atomic_policy.cmake: failed to run objdump on '${ELF_FILE}': ${objdump_error}")
endif()
string(REGEX MATCHALL "_Lock_policyE[0-9]" lock_policy_matches "${objdump_output}")
set(lock_policies "")
foreach(match IN LISTS lock_policy_matches)
string(REGEX REPLACE "^.*_Lock_policyE([0-9])$" "\\1" policy "${match}")
list(APPEND lock_policies "${policy}")
endforeach()
if(lock_policies)
list(REMOVE_DUPLICATES lock_policies)
list(SORT lock_policies)
endif()
list(LENGTH lock_policies num_policies)
if(num_policies GREATER 1)
string(REPLACE ";" ", " policies_str "${lock_policies}")
message(FATAL_ERROR
"Mixed libstdc++ atomic lock policies detected in '${ELF_FILE}' "
"(__gnu_cxx::_Lock_policy values: ${policies_str}).")
endif()
+3
View File
@@ -0,0 +1,3 @@
-
re: "Mixed libstdc\\+\\+ atomic lock policies detected"
hint: "Some object files or prebuilt libraries were compiled with a different _GLIBCXX_HAVE_ATOMIC_LOCK_POLICY setting than the rest of the project. std::shared_ptr reference counting may exhibit undefined behavior. Rebuild all C++ code and libraries with a consistent CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY setting."
+7
View File
@@ -0,0 +1,7 @@
if(NOT CONFIG_IDF_TARGET_LINUX)
if(CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY)
idf_toolchain_add_flags(CXX_COMPILE_OPTIONS "-D_GLIBCXX_HAVE_ATOMIC_LOCK_POLICY=1")
else()
idf_toolchain_remove_flags(CXX_COMPILE_OPTIONS "-D_GLIBCXX_HAVE_ATOMIC_LOCK_POLICY=1")
endif()
endif()
@@ -9,3 +9,10 @@ components/cxx/test_apps:
- cxx
- pthread
- freertos
components/cxx/test_apps/atomic_lock_policy:
enable:
- if: IDF_TARGET == "esp32c5"
disable:
- if: IDF_TARGET != "esp32c5"
reason: only built on ESP32-C5
@@ -0,0 +1,7 @@
# This is the project CMakeLists.txt file for the test subproject
cmake_minimum_required(VERSION 3.22)
set(COMPONENTS main)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(test_cxx_atomic_lock_policy)
@@ -0,0 +1,12 @@
| Supported Targets | ESP32-C5 |
| ----------------- | -------- |
# C++ atomic lock policy test
Build-only app. Validates ``CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY``: both
translation units use ``std::shared_ptr`` under the forced lock-free policy, and
the cxx post-build hook accepts the linked executable.
For the negative build-system check (mixed ``__gnu_cxx::_Lock_policy``), see
``tools/test_build_system/test_cxx_atomic_policy.py``, which builds this app with
``-DTEST_INVALID_LOCK_POLICIES=1``.
@@ -0,0 +1,12 @@
idf_component_register(SRCS "test_cxx_atomic_lock_policy.cpp"
"cxx_atomic_a.cpp"
"cxx_atomic_b.cpp")
# Default build: both TUs inherit CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY.
# With -DTEST_INVALID_LOCK_POLICIES=1, undefine the macro for one TU so the
# linked image mixes __gnu_cxx::_Lock_policy instantiations; the cxx post-build
# hook (check_cxx_atomic_policy.cmake) must then fail the build.
if(TEST_INVALID_LOCK_POLICIES)
set_source_files_properties("cxx_atomic_b.cpp" PROPERTIES
COMPILE_OPTIONS "-U_GLIBCXX_HAVE_ATOMIC_LOCK_POLICY")
endif()
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <memory>
// Compiled with the project-wide atomic lock policy (CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY).
// std::make_shared instantiates the __gnu_cxx::_Lock_policy-parameterized
// reference counting templates.
int cxx_atomic_use_a(void)
{
auto p = std::make_shared<int>(1);
return *p;
}
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <memory>
// By default compiled with the same project-wide atomic lock policy as
// cxx_atomic_a.cpp. When TEST_INVALID_LOCK_POLICIES is set, main/CMakeLists.txt
// adds -U_GLIBCXX_HAVE_ATOMIC_LOCK_POLICY so this TU instantiates a different
// __gnu_cxx::_Lock_policy.
int cxx_atomic_use_b(void)
{
auto p = std::make_shared<int>(2);
return *p;
}
@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <stdio.h>
int cxx_atomic_use_a(void);
int cxx_atomic_use_b(void);
extern "C" void app_main(void)
{
// Reference both translation units so the linker keeps their
// _Lock_policy instantiations (needed for the invalid-policy build check).
volatile int r = cxx_atomic_use_a() + cxx_atomic_use_b();
printf("cxx atomic lock policy: %d\n", r);
}
@@ -0,0 +1 @@
CONFIG_COMPILER_CXX_ATOMIC_LOCK_POLICY=y