# Copyright (c) 2025-2026, IB-Robot Group & openEuler Embedded SIG & openharmony-robot sig_RoboFrame.
# All rights reserved.
#
# DdsIdlGenCode.cmake - Generate C++ code from DDS IDL files using fastddsgen
#
# This module provides functions to generate C++ type support code from DDS IDL files.
# It replaces the protobuf-based code generation (ProtobufGenCode.cmake).
#
# Generated files:
# <TypeName>.hpp - Type definition (always usable)
# <TypeName>CdrAux.hpp - CDR serialization aux header (only needs fastcdr)
# <TypeName>CdrAux.ipp - CDR serialization aux implementation (only needs fastcdr)
# <TypeName>PubSubTypes.hpp/cxx - PubSub type support (needs FastDDS 3.x)
#
# Note: fastddsgen v4.x generates PubSubTypes that require FastDDS 3.x API.
# Since this build environment may have FastDDS 2.6.x (ROS2 Humble), we generate
# all files but only compile the CdrAux (which only depends on FastCDR).
# The PubSubTypes are generated but not compiled — they can be used when
# FastDDS 3.x is available.
#
# Usage:
# add_dds_idl_gencode_target(
# TARGET_NAME <target>
# IDL_FILES <file1.idl> [file2.idl ...]
# GENCODE_PATH <output_directory>
# [INCLUDE_DIRS <dir1> [dir2 ...]]
# [DEPENDENCIES <dep1> [dep2 ...]]
# )
# Cache this module's directory at file scope so that functions defined below
# can locate helper scripts (e.g. fastcdr_v2_to_v1.py) regardless of where
# the calling CMakeLists.txt is located. Inside a function, CMAKE_CURRENT_LIST_DIR
# resolves to the *caller's* directory, not this file's directory.
set(_DDS_IDL_GEN_CODE_CMAKE_DIR "${CMAKE_CURRENT_LIST_DIR}")
# Find fastddsgen tool
find_program(FASTDDSGEN_EXECUTABLE
NAMES fastddsgen
HINTS
$ENV{FASTDDS_HOME}/bin
$ENV{FASTRTPS_HOME}/bin
DOC "Path to fastddsgen IDL compiler"
)
if(NOT FASTDDSGEN_EXECUTABLE)
message(STATUS "fastddsgen not found. DDS IDL code generation will not be available.")
message(STATUS "Install Fast-DDS-Gen from: https://github.com/eProsima/Fast-DDS-Gen")
else()
message(STATUS "Found fastddsgen: ${FASTDDSGEN_EXECUTABLE}")
endif()
# Detect FastDDS version to decide whether to compile PubSubTypes
# FastDDS 3.x is required for the generated PubSubTypes; 2.x only supports CdrAux
set(IBMW_FASTDDS_V3_AVAILABLE FALSE)
if(NOT (CMAKE_CROSSCOMPILING AND IBMW_TARGET_ROS2_TIER STREQUAL "rosidl") AND NOT TARGET eProsima_atomic)
ibmw_find_target_package(fastdds VERSION 3 QUIET FEATURE dds-idl-pubsub-types)
if(fastdds_FOUND)
set(IBMW_FASTDDS_V3_AVAILABLE TRUE)
endif()
endif()
# Generate C++ code from DDS IDL files
#
# This function creates a library target containing the generated C++ code
# from the specified DDS IDL files.
#
# Arguments:
# TARGET_NAME - Name of the CMake target to create
# IDL_FILES - List of .idl source files
# GENCODE_PATH - Output directory for generated code
# INCLUDE_DIRS - Additional include directories for IDL compiler (optional)
# DEPENDENCIES - CMake targets that this target depends on (optional)
#
function(add_dds_idl_gencode_target)
cmake_parse_arguments(ARG "" "TARGET_NAME;GENCODE_PATH" "IDL_FILES;INCLUDE_DIRS;DEPENDENCIES;UPSTREAM_GENCODE_PATHS;WRAP_IDLS" ${ARGN})
# Select the FastCDR lane when the target is declared, after GetFastDDS.cmake
# has had a chance to populate fastcdr_VERSION. Preserve the historical v1
# compatibility transform when fastcdr_VERSION is unknown/empty or older than
# 2.0; FastCDR 2.x+ consumes fastddsgen's native v2 CdrAux output as-is.
set(IBMW_DDS_IDL_FASTCDR_V1_COMPAT TRUE)
if(DEFINED fastcdr_VERSION AND fastcdr_VERSION AND NOT fastcdr_VERSION VERSION_LESS 2.0)
set(IBMW_DDS_IDL_FASTCDR_V1_COMPAT FALSE)
endif()
if(IBMW_DDS_IDL_FASTCDR_V1_COMPAT)
if(DEFINED fastcdr_VERSION AND fastcdr_VERSION)
message(STATUS "DDS IDL CDR lane for ${ARG_TARGET_NAME}: FastCDR v1 compatibility transform (fastcdr_VERSION=${fastcdr_VERSION})")
else()
message(WARNING "DDS IDL CDR lane for ${ARG_TARGET_NAME}: FastCDR v1 compatibility transform (fastcdr_VERSION unknown)")
endif()
else()
message(STATUS "DDS IDL CDR lane for ${ARG_TARGET_NAME}: native FastCDR v2+ generated code (fastcdr_VERSION=${fastcdr_VERSION})")
endif()
# WRAP_IDLS: opt-in list of IDL stem names (without .idl) that should get an
# auto-generated CdrImpl.cc wrapper TU compiled into a STATIC archive. IDLs
# not in this list are still generated (headers, .ipp) but no wrapper TU is
# emitted for them, keeping the library effectively INTERFACE for those types.
# This guards us from py-transformer limitations on complex IDLs (unions,
# nested-struct vectors) until those patterns are supported.
# If WRAP_IDLS is empty, the resulting library is INTERFACE (header-only).
if(NOT FASTDDSGEN_EXECUTABLE)
message(FATAL_ERROR "fastddsgen is required but not found. Cannot generate code for target ${ARG_TARGET_NAME}")
endif()
if(NOT EXISTS ${ARG_GENCODE_PATH})
file(MAKE_DIRECTORY ${ARG_GENCODE_PATH})
endif()
# Determine the common IDL source directory for relative path computation.
# All IDL files in a single target share the same parent directory.
list(GET ARG_IDL_FILES 0 _first_idl)
get_filename_component(_IDL_BASE_DIR ${_first_idl} DIRECTORY)
# Build include path arguments using relative paths.
# fastddsgen v4.x has a quirk where absolute -I paths cause generated C++
# #includes to use the full path (minus leading /). Using relative paths
# produces clean basename-only #includes like "Header.hpp".
set(INCLUDE_ARGS)
foreach(INC_DIR ${ARG_INCLUDE_DIRS})
file(RELATIVE_PATH _rel_inc "${_IDL_BASE_DIR}" "${INC_DIR}")
if(_rel_inc STREQUAL "")
set(_rel_inc ".")
endif()
list(APPEND INCLUDE_ARGS "-I" "${_rel_inc}")
endforeach()
# Forward UPSTREAM_GENCODE_PATHS to the post-process script so cross-package
# CdrAux.ipp dependencies can be injected (e.g. sensor's CompressedImageCdrAux.ipp
# gains an `#include "HeaderCdrAux.ipp"` line resolving to the common gencode dir).
set(UPSTREAM_DIR_ARGS)
foreach(UP_DIR ${ARG_UPSTREAM_GENCODE_PATHS})
list(APPEND UPSTREAM_DIR_ARGS "--upstream-dir" "${UP_DIR}")
endforeach()
set(GEN_SRCS)
set(GEN_HDRS)
# cdr_impl wrapper TUs auto-generated below (one per IDL); compiled into
# the gencode static library so downstream consumers no longer need to hand-roll
# their own per-sample compressed_image_cdr_impl.cc files.
set(GEN_CDR_IMPL_TUS)
# Track per-IDL CdrAux.ipp outputs across the loop so each subsequent IDL's
# post-process (inject_dependent_ipp_includes) DEPENDS on previously-written
# .ipp files. Without this serialization, parallel custom-command execution
# (`make -j`) can run ExampleRpc's transform before ExampleCommon's
# fastddsgen finishes writing ExampleCommonCdrAux.ipp — the inject step then
# fails to discover the dependent .ipp and ExampleRpcCdrAux.ipp is missing
# the `#include "ExampleCommonCdrAux.ipp"` line, breaking compilation of
# downstream RPC types whose fields reference ExampleCommon types.
# See docs/contributing/testing.md.
set(_PREV_IDL_IPP_OUTPUTS)
foreach(IDL_FILE ${ARG_IDL_FILES})
get_filename_component(IDL_NAME ${IDL_FILE} NAME_WE)
get_filename_component(IDL_BASENAME ${IDL_FILE} NAME)
get_filename_component(IDL_DIR ${IDL_FILE} DIRECTORY)
# fastddsgen generates these files per IDL:
set(GEN_TYPE_HPP "${ARG_GENCODE_PATH}/${IDL_NAME}.hpp")
set(GEN_PUBSUB_CXX "${ARG_GENCODE_PATH}/${IDL_NAME}PubSubTypes.cxx")
set(GEN_PUBSUB_HPP "${ARG_GENCODE_PATH}/${IDL_NAME}PubSubTypes.hpp")
set(GEN_CDR_IPP "${ARG_GENCODE_PATH}/${IDL_NAME}CdrAux.ipp")
set(GEN_CDR_HPP "${ARG_GENCODE_PATH}/${IDL_NAME}CdrAux.hpp")
# Auto-generated 2-line wrapper TU that #includes the .ipp once so the
# inline FastCDR specializations get emitted into the static library.
set(GEN_CDR_IMPL_TU "${ARG_GENCODE_PATH}/${IDL_NAME}CdrImpl.cc")
# Per-IDL output list for this custom command
set(CUR_IDL_OUTPUTS ${GEN_TYPE_HPP} ${GEN_PUBSUB_CXX} ${GEN_PUBSUB_HPP} ${GEN_CDR_IPP} ${GEN_CDR_HPP})
# Accumulate headers and sources across all IDL files
list(APPEND GEN_HDRS ${GEN_TYPE_HPP} ${GEN_CDR_HPP} ${GEN_CDR_IPP})
# Only wrap IDLs explicitly opted-in via WRAP_IDLS. This avoids forcing
# the py CDR transformer to handle every IDL pattern (unions, nested-struct
# vectors, etc.) up-front; new types can be added to the list once their
# wrapper TU compiles cleanly.
set(_WRAP_THIS_IDL FALSE)
if(IDL_NAME IN_LIST ARG_WRAP_IDLS)
set(_WRAP_THIS_IDL TRUE)
list(APPEND GEN_CDR_IMPL_TUS ${GEN_CDR_IMPL_TU})
endif()
# Only compile PubSubTypes if FastDDS 3.x is available
if(IBMW_FASTDDS_V3_AVAILABLE)
list(APPEND GEN_SRCS ${GEN_PUBSUB_CXX})
list(APPEND GEN_HDRS ${GEN_PUBSUB_HPP})
endif()
set(_POSTPROCESS_COMMAND)
set(_GENCODE_COMMENT "Running fastddsgen on ${IDL_BASENAME} (native FastCDR v2+)")
if(IBMW_DDS_IDL_FASTCDR_V1_COMPAT)
# Post-process: convert FastCDR v2 API calls to v1-compatible code.
# fastddsgen v4.x generates v2-only CdrAux code, but older FastCDR lanes
# (including unknown fastcdr_VERSION, for historical compatibility) need
# the legacy transform. This script transforms member-function calls
# (begin_serialize_type, end_serialize_type, deserialize_type, etc.)
# into v1-compatible patterns. Also processes .hpp files to remove
# v2-only includes like fixed_size_string.hpp, and PubSubTypes.cxx
# to replace operator<</>> with explicit free-function serialize/deserialize.
# The --upstream-dir args allow cross-package CdrAux.ipp injection so a
# sensor-package wrapper TU can resolve common-package Header specializations.
set(_POSTPROCESS_COMMAND
COMMAND ${CMAKE_COMMAND} -E env python3
${_DDS_IDL_GEN_CODE_CMAKE_DIR}/fastcdr_v2_to_v1.py
${UPSTREAM_DIR_ARGS}
${GEN_CDR_IPP}
${GEN_TYPE_HPP}
${GEN_PUBSUB_CXX})
set(_GENCODE_COMMENT "Running fastddsgen on ${IDL_BASENAME} (with v1 compat)")
endif()
add_custom_command(
OUTPUT ${CUR_IDL_OUTPUTS}
COMMAND ${FASTDDSGEN_EXECUTABLE}
-d ${ARG_GENCODE_PATH}
-flat-output-dir
-no-typeobjectsupport
-no-dependencies
-typeros2
-cs
-replace
${INCLUDE_ARGS}
${IDL_BASENAME}
${_POSTPROCESS_COMMAND}
# DEPENDS the IDL source plus the .ipp outputs of all previously processed
# IDLs in this target. The latter forces serial execution under `make -j`
# so inject_dependent_ipp_includes can see same-target dependent .ipp
# files (e.g. ExampleRpc -> ExampleCommon). See §6.2.2.18.
DEPENDS ${IDL_FILE} ${_PREV_IDL_IPP_OUTPUTS}
WORKING_DIRECTORY ${IDL_DIR}
COMMENT "${_GENCODE_COMMENT}"
VERBATIM
)
# Append this IDL's .ipp to the running serialization chain.
list(APPEND _PREV_IDL_IPP_OUTPUTS ${GEN_CDR_IPP})
# Synthesize a tiny per-IDL wrapper TU at configure time only for opt-in IDLs.
# Compiling this TU into the gencode STATIC library emits the inline FastCDR
# specializations exactly once per process, so downstream samples need only
# link the gencode lib — no hand-rolled <type>_cdr_impl.cc + per-sample STATIC
# wrapper required. file(WRITE) at configure time avoids Makefile/Ninja issues
# with multi-line `cmake -E echo` shell-redirection commands.
if(_WRAP_THIS_IDL)
file(WRITE ${GEN_CDR_IMPL_TU}
"// Copyright (c) 2025-2026, IB-Robot Group & openEuler Embedded SIG & openharmony-robot sig_RoboFrame.
// All rights reserved.
//
// Auto-generated by add_dds_idl_gencode_target. DO NOT EDIT.
// Compiling this single TU emits the inline FastCDR serialize/deserialize
// specializations for ${IDL_NAME} into the gencode static library.
#include \"${IDL_NAME}CdrAux.ipp\"
")
endif()
endforeach()
# Create the library target
if(GEN_SRCS)
# FastDDS 3.x available: compile PubSubTypes (+ any opted-in CDR impl wrappers)
add_library(${ARG_TARGET_NAME} STATIC ${GEN_SRCS} ${GEN_CDR_IMPL_TUS})
target_include_directories(${ARG_TARGET_NAME}
PUBLIC
$<BUILD_INTERFACE:${ARG_GENCODE_PATH}>
$<INSTALL_INTERFACE:include/${ARG_TARGET_NAME}>
)
foreach(UP_DIR ${ARG_UPSTREAM_GENCODE_PATHS})
target_include_directories(${ARG_TARGET_NAME} PRIVATE $<BUILD_INTERFACE:${UP_DIR}>)
endforeach()
target_sources(${ARG_TARGET_NAME}
PUBLIC FILE_SET HEADERS
BASE_DIRS ${ARG_GENCODE_PATH}
FILES ${GEN_HDRS}
)
target_link_libraries(${ARG_TARGET_NAME} PUBLIC fastcdr fastdds)
if(IBMW_DDS_IDL_FASTCDR_V1_COMPAT)
# Transformed CdrAux.ipp helpers include dds_types/util/fastcdr_v1_compat.h.
# Use the schema-layer helper target rather than the DDS extension target
# so protocol/schema builds do not acquire a reverse dependency on
# IBMW_BUILD_DDS_EXTENSION.
target_link_libraries(${ARG_TARGET_NAME} PUBLIC ibmw::schema::dds_cdr_helpers)
endif()
elseif(GEN_CDR_IMPL_TUS)
# FastDDS 2.x with at least one opted-in IDL: compile the auto-generated CDR
# impl wrappers into a STATIC library so downstream consumers can drop their
# hand-written cdr_impl.cc files.
add_library(${ARG_TARGET_NAME} STATIC ${GEN_CDR_IMPL_TUS})
target_include_directories(${ARG_TARGET_NAME}
PUBLIC
$<BUILD_INTERFACE:${ARG_GENCODE_PATH}>
$<INSTALL_INTERFACE:include/${ARG_TARGET_NAME}>
)
foreach(UP_DIR ${ARG_UPSTREAM_GENCODE_PATHS})
target_include_directories(${ARG_TARGET_NAME} PRIVATE $<BUILD_INTERFACE:${UP_DIR}>)
endforeach()
target_sources(${ARG_TARGET_NAME}
PUBLIC FILE_SET HEADERS
BASE_DIRS ${ARG_GENCODE_PATH}
FILES ${GEN_HDRS}
)
target_link_libraries(${ARG_TARGET_NAME} PUBLIC fastcdr)
if(IBMW_DDS_IDL_FASTCDR_V1_COMPAT)
# The wrapper TU pulls in dds_types/util/fastcdr_v1_compat.h via the
# transformed .ipp. Link the schema-layer CDR-only helpers target (NOT the
# dds_extension dds_types target) to avoid a reverse dependency: schema
# must not require IBMW_BUILD_DDS_EXTENSION. See src/schema/CMakeLists.txt
# for the helpers target definition.
target_link_libraries(${ARG_TARGET_NAME} PUBLIC ibmw::schema::dds_cdr_helpers)
endif()
else()
# FastDDS 2.x with no opted-in IDLs: header-only INTERFACE library. Consumers
# that need CDR symbols for these types must still hand-roll their own
# per-sample cdr_impl.cc until those IDLs are added to WRAP_IDLS.
add_library(${ARG_TARGET_NAME} INTERFACE)
target_include_directories(${ARG_TARGET_NAME}
INTERFACE
$<BUILD_INTERFACE:${ARG_GENCODE_PATH}>
$<INSTALL_INTERFACE:include/${ARG_TARGET_NAME}>
)
target_sources(${ARG_TARGET_NAME}
INTERFACE FILE_SET HEADERS
BASE_DIRS ${ARG_GENCODE_PATH}
FILES ${GEN_HDRS}
)
target_link_libraries(${ARG_TARGET_NAME} INTERFACE fastcdr)
# Custom target to drive code generation when no compilable sources exist.
add_custom_target(${ARG_TARGET_NAME}_codegen ALL DEPENDS ${GEN_HDRS})
add_dependencies(${ARG_TARGET_NAME} ${ARG_TARGET_NAME}_codegen)
endif()
# Link dependencies. Use INTERFACE keyword for INTERFACE lib, PUBLIC otherwise.
if(TARGET ${ARG_TARGET_NAME})
get_target_property(_lib_type ${ARG_TARGET_NAME} TYPE)
if(_lib_type STREQUAL "INTERFACE_LIBRARY")
set(_link_keyword INTERFACE)
else()
set(_link_keyword PUBLIC)
endif()
foreach(DEP ${ARG_DEPENDENCIES})
target_link_libraries(${ARG_TARGET_NAME} ${_link_keyword} ${DEP})
endforeach()
endif()
# Set properties for downstream consumers (skip for INTERFACE libs which can't
# carry custom properties via set_target_properties without INTERFACE_ prefix).
if(NOT _lib_type STREQUAL "INTERFACE_LIBRARY")
set_target_properties(${ARG_TARGET_NAME} PROPERTIES
IDL_GENCODE_PATH ${ARG_GENCODE_PATH}
)
endif()
endfunction()