1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-23 12:13:49 +01:00

[core] Add support for extern "C" includes (#11422)

This commit is contained in:
Jonathan Swoboda
2025-10-20 22:46:50 -04:00
committed by GitHub
parent 3b6ff615e8
commit a809a13729
3 changed files with 70 additions and 14 deletions

View File

@@ -471,6 +471,7 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
CONF_INC_PIN = "inc_pin"
CONF_INCLUDE_INTERNAL = "include_internal"
CONF_INCLUDES = "includes"
CONF_INCLUDES_C = "includes_c"
CONF_INDEX = "index"
CONF_INDOOR = "indoor"
CONF_INFRARED = "infrared"

View File

@@ -21,6 +21,7 @@ from esphome.const import (
CONF_FRIENDLY_NAME,
CONF_ID,
CONF_INCLUDES,
CONF_INCLUDES_C,
CONF_LIBRARIES,
CONF_MIN_VERSION,
CONF_NAME,
@@ -227,6 +228,7 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include),
cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean,
@@ -302,6 +304,17 @@ def _list_target_platforms():
return target_platforms
def _sort_includes_by_type(includes: list[str]) -> tuple[list[str], list[str]]:
system_includes = []
other_includes = []
for include in includes:
if include.startswith("<") and include.endswith(">"):
system_includes.append(include)
else:
other_includes.append(include)
return system_includes, other_includes
def preload_core_config(config, result) -> str:
with cv.prepend_path(CONF_ESPHOME):
conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
@@ -339,7 +352,7 @@ def preload_core_config(config, result) -> str:
return target_platforms[0]
def include_file(path: Path, basename: Path):
def include_file(path: Path, basename: Path, is_c_header: bool = False):
parts = basename.parts
dst = CORE.relative_src_path(*parts)
copy_file_if_changed(path, dst)
@@ -347,7 +360,14 @@ def include_file(path: Path, basename: Path):
ext = path.suffix
if ext in [".h", ".hpp", ".tcc"]:
# Header, add include statement
cg.add_global(cg.RawStatement(f'#include "{basename}"'))
if is_c_header:
# Wrap in extern "C" block for C headers
cg.add_global(
cg.RawStatement(f'extern "C" {{\n #include "{basename}"\n}}')
)
else:
# Regular include
cg.add_global(cg.RawStatement(f'#include "{basename}"'))
ARDUINO_GLUE_CODE = """\
@@ -377,7 +397,7 @@ async def add_arduino_global_workaround():
@coroutine_with_priority(CoroPriority.FINAL)
async def add_includes(includes: list[str]) -> None:
async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
# Add includes at the very end, so that the included files can access global variables
for include in includes:
path = CORE.relative_config_path(include)
@@ -385,11 +405,11 @@ async def add_includes(includes: list[str]) -> None:
# Directory, copy tree
for p in walk_files(path):
basename = p.relative_to(path.parent)
include_file(p, basename)
include_file(p, basename, is_c_header)
else:
# Copy file
basename = Path(path.name)
include_file(path, basename)
include_file(path, basename, is_c_header)
@coroutine_with_priority(CoroPriority.FINAL)
@@ -494,19 +514,25 @@ async def to_code(config: ConfigType) -> None:
CORE.add_job(add_arduino_global_workaround)
if config[CONF_INCLUDES]:
# Get the <...> includes
system_includes = []
other_includes = []
for include in config[CONF_INCLUDES]:
if include.startswith("<") and include.endswith(">"):
system_includes.append(include)
else:
other_includes.append(include)
system_includes, other_includes = _sort_includes_by_type(config[CONF_INCLUDES])
# <...> includes should be at the start
for include in system_includes:
cg.add_global(cg.RawStatement(f"#include {include}"), prepend=True)
# Other includes should be at the end
CORE.add_job(add_includes, other_includes)
CORE.add_job(add_includes, other_includes, False)
if config[CONF_INCLUDES_C]:
system_includes, other_includes = _sort_includes_by_type(
config[CONF_INCLUDES_C]
)
# <...> includes should be at the start
for include in system_includes:
cg.add_global(
cg.RawStatement(f'extern "C" {{\n #include {include}\n}}'),
prepend=True,
)
# Other includes should be at the end
CORE.add_job(add_includes, other_includes, True)
if project_conf := config.get(CONF_PROJECT):
cg.add_define("ESPHOME_PROJECT_NAME", project_conf[CONF_NAME])

View File

@@ -517,6 +517,35 @@ def test_include_file_cpp(tmp_path: Path, mock_copy_file_if_changed: Mock) -> No
mock_cg.add_global.assert_not_called()
def test_include_file_with_c_header(
tmp_path: Path, mock_copy_file_if_changed: Mock
) -> None:
"""Test include_file wraps header in extern C block when is_c_header is True."""
src_file = tmp_path / "c_library.h"
src_file.write_text("// C library header")
CORE.build_path = tmp_path / "build"
with patch("esphome.core.config.cg") as mock_cg:
# Mock RawStatement to capture the text
mock_raw_statement = MagicMock()
mock_raw_statement.text = ""
def raw_statement_side_effect(text):
mock_raw_statement.text = text
return mock_raw_statement
mock_cg.RawStatement.side_effect = raw_statement_side_effect
config.include_file(src_file, Path("c_library.h"), is_c_header=True)
mock_copy_file_if_changed.assert_called_once()
mock_cg.add_global.assert_called_once()
# Check that include statement is wrapped in extern "C" block
assert 'extern "C"' in mock_raw_statement.text
assert '#include "c_library.h"' in mock_raw_statement.text
def test_get_usable_cpu_count() -> None:
"""Test get_usable_cpu_count returns CPU count."""
count = config.get_usable_cpu_count()