Stop linking the Python bindings library into C++ consumers of interface packages (fixes dyld _PyExc_RuntimeError / ros-kilted#76) - #51
Conversation
…on bindings
Every interface package exports its Python C bindings library
(lib<pkg>__rosidl_generator_py) in <pkg>_TARGETS, so plain C++ consumers
link it. That library has unresolved CPython symbols, so the consumer
aborts at startup on macOS ("symbol not found in flat namespace
'_PyExc_RuntimeError'") and fails to link or load on Linux when the
library links Python3::Module (RoboStack/ros-kilted#76).
The test builds a C++ executable against ${std_msgs_TARGETS} and runs it
(unix only), and checks that the Python bindings still work from Python.
std_msgs is bumped to build 26 so CI rebuilds it and runs the test.
This commit is expected to fail the new test on macOS. On Linux the
library currently links libpython (Python3::Python), which hides the
problem: the consumer runs, but loads libpython.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Maurice <mauricepurnawan@gmail.com>
a084fe4 to
2ed71d4
Compare
|
Macos failing with: After adding the tests (1st commit) |
…odules Replace the macOS-only patch with a cross-platform one that removes the shared lib<pkg>__rosidl_generator_py library: - The generated C conversion code is compiled (as an OBJECT library) into each <pkg>_s__rosidl_typesupport_* Python extension module, which links Python3::Module. Nothing is installed or exported, so <pkg>_TARGETS only contains C/C++ libraries and no library has unresolved CPython symbols. - Conversion functions of nested types from other packages are looked up (and cached) from those packages' Python message classes (_CONVERT_FROM_PY / _CONVERT_TO_PY capsules, already used by rclpy) instead of linking the other package's library. rosidl_generator_py is bumped to build 26 so CI rebuilds it. So are rosidl_core_generators and rosidl_default_generators: std_msgs only depends on the generator through them, and without rebuilding them rattler-build does not know to build the generator before std_msgs. The regression test added in the previous commit now passes. Message packages built by the old generator link their dependencies' lib<dep>__rosidl_generator_py, so all interface packages must be rebuilt together (e.g. in the next full rebuild). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Maurice <mauricepurnawan@gmail.com>
73971ba to
7d891c3
Compare
The earlier CI runs of this PR cached a std_msgs build 26 built with the old generator; with --skip-existing it would be reused instead of being rebuilt with the patched generator. Drop this commit before merging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Maurice <mauricepurnawan@gmail.com>
|
All green after adding the patch (2nd commit) |
|
@traversaro I added a few tests related to RoboStack/ros-kilted#76, ros2/rosidl_python#253, and a longstanding issue I have encountered when developing ROS 2 on macOS. Let me first describe the macOS issue. So far I have been using ROS Humble, Lyrical, and Rolling on macOS for ROS 2 development, for example compiling Nav2 from source and working on personal projects. Across all of these distros, whenever I start a new project I eventually run into errors involving symbols such as My workaround has usually been to add something like this where needed: component_container_env = {}
if sys.platform == 'darwin':
python_library = f'libpython{sysconfig.get_python_version()}.dylib'
component_container_env['DYLD_INSERT_LIBRARIES'] = os.path.join(
sys.prefix, 'lib', python_library
)I believe something similar could also be added through the Pixi activation environment, but either way this feels more like a workaround than a good development experience. Based on my investigation, this problem does not normally show up in the RoboStack prebuilt binaries. The reason seems to be that those binaries are built in an environment where the linker drops unused libraries, so the Python bindings library does not remain as a runtime dependency. A more detailed explanation from my agent is:
After investigating this, I ended up with two possible fixes. The larger fix is the one currently proposed in this PR. It makes the generated conversion code part of the Python extension directly and performs cross-package converter lookup at runtime through the Python message classes. This also means everything uses Python3::Module. A smaller alternative would be: diff --git a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake
index 2810984..f815eab 100644
--- a/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake
+++ b/rosidl_generator_py/cmake/rosidl_generator_py_generate_interfaces.cmake
@@ -38,6 +38,11 @@ if(NOT TARGET Python3::Module OR NOT TARGET Python3::NumPy)
find_package(Python3 REQUIRED COMPONENTS Interpreter Development NumPy)
endif()
+# The Python C bindings library of an interface package is exported in
+# <pkg>_TARGETS__rosidl_generator_py instead of <pkg>_TARGETS, so that C/C++
+# consumers of the interface package don't link it.
+set(rosidl_generator_py_suffix "__rosidl_generator_py")
+
# Get a list of typesupport implementations from valid rmw implementations.
rosidl_generator_py_get_typesupports(_typesupport_impls)
@@ -165,10 +170,18 @@ add_dependencies(
${rosidl_generate_interfaces_TARGET}__rosidl_typesupport_c
)
+# On macOS, link Python3::Module (-undefined dynamic_lookup): linking libpython
+# would load a second interpreter into a python executable that links it
+# statically (e.g. conda-forge), which crashes.
+if(APPLE)
+ set(_python_target Python3::Module)
+else()
+ set(_python_target Python3::Python)
+endif()
target_link_libraries(
${_target_name_lib} PRIVATE
Python3::NumPy
- Python3::Python
+ ${_python_target}
)
target_include_directories(${_target_name_lib}
PRIVATE
@@ -260,9 +273,20 @@ if(NOT rosidl_generate_interfaces_SKIP_INSTALL)
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
- # Export this target so downstream interface packages can depend on it
- rosidl_export_typesupport_targets("${rosidl_generator_py_suffix}" "${_target_name_lib}")
- ament_export_targets(export_${_target_name_lib})
+ # Export this target so downstream interface packages can depend on it.
+ # Not with ament_export_targets(), which would add it to <pkg>_TARGETS.
+ install(
+ EXPORT export_${_target_name_lib}
+ DESTINATION share/${PROJECT_NAME}/cmake
+ NAMESPACE "${PROJECT_NAME}::"
+ FILE "export_${_target_name_lib}Export.cmake")
+ set(_py_extras_file
+ "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_py/${_target_name_lib}-extras.cmake")
+ file(WRITE "${_py_extras_file}"
+ "include(\"\${${PROJECT_NAME}_DIR}/export_${_target_name_lib}Export.cmake\")\n"
+ "list(APPEND ${PROJECT_NAME}_TARGETS${rosidl_generator_py_suffix}\n"
+ " \"${PROJECT_NAME}::${_target_name_lib}\")\n")
+ list(APPEND ${PROJECT_NAME}_CONFIG_EXTRAS "${_py_extras_file}")
endif()
if(BUILD_TESTING AND rosidl_generate_interfaces_ADD_LINTER_TESTS)
I tested this smaller version as well, and it appears to work. The main difference is:
Since I do not have much experience with Empy and the code-generation side of rosidl_generator_py, I would appreciate it if you could take a look before I spend more time validating them. In particular, I would be interested to know which of these two directions you would prefer |
|
Thanks a lot for the clear tests and PR description, finally I fully understand the problem behind ros2/rosidl_python#253 . It is a bit late now here in Europe, I will post my thought on this tomorrow. Interestingly, I think the problem is quite similar to PixarAnimationStudios/OpenUSD#3577 . |
…on bindings
Every interface package exports its Python C bindings library
(lib<pkg>__rosidl_generator_py) in <pkg>_TARGETS, so plain C++ consumers
link it. That library has unresolved CPython symbols, so the consumer
aborts at startup on macOS ("symbol not found in flat namespace
'_PyExc_RuntimeError'") and fails to link or load on Linux when the
library links Python3::Module (RoboStack/ros-kilted#76).
The test builds a C++ executable against ${std_msgs_TARGETS} and runs it
(unix only), and checks that the Python bindings still work from Python.
std_msgs is bumped to build 26 so CI rebuilds it and runs the test.
This commit is expected to fail the new test on macOS. On Linux the
library currently links libpython (Python3::Python), which hides the
problem: the consumer runs, but loads libpython.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Maurice <mauricepurnawan@gmail.com>
(cherry picked from commit 2ed71d4 of #51, without the pkg_additional_info.yaml build-number bump)
…odules Replace the macOS-only patch with a cross-platform one that removes the shared lib<pkg>__rosidl_generator_py library: - The generated C conversion code is compiled (as an OBJECT library) into each <pkg>_s__rosidl_typesupport_* Python extension module, which links Python3::Module. Nothing is installed or exported, so <pkg>_TARGETS only contains C/C++ libraries and no library has unresolved CPython symbols. - Conversion functions of nested types from other packages are looked up (and cached) from those packages' Python message classes (_CONVERT_FROM_PY / _CONVERT_TO_PY capsules, already used by rclpy) instead of linking the other package's library. rosidl_generator_py is bumped to build 26 so CI rebuilds it. So are rosidl_core_generators and rosidl_default_generators: std_msgs only depends on the generator through them, and without rebuilding them rattler-build does not know to build the generator before std_msgs. The regression test added in the previous commit now passes. Message packages built by the old generator link their dependencies' lib<dep>__rosidl_generator_py, so all interface packages must be rebuilt together (e.g. in the next full rebuild). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Maurice <mauricepurnawan@gmail.com> (cherry picked from commit 7d891c3 of #51, without the pkg_additional_info.yaml build-number bump)
The earlier CI runs of this PR cached a std_msgs build 26 built with the old generator; with --skip-existing it would be reused instead of being rebuilt with the patched generator. Drop this commit before merging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Maurice <mauricepurnawan@gmail.com> (cherry picked from commit 82d0021 of #51)
…odules Port of RoboStack/ros-rolling#51 (without its pkg_additional_info.yaml build-number bumps): stop exporting the lib<pkg>__rosidl_generator_py library, which has unresolved CPython symbols, to C++ consumers of interface packages. The generated conversion code is compiled as an OBJECT library into each Python extension module, and conversion functions of nested types from other packages are looked up from those packages' Python message classes (_CONVERT_FROM_PY/_CONVERT_TO_PY). Adds the std_msgs C++-consumer regression test and a CI cache eviction for std_msgs so it is rebuilt and tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…odules Port of RoboStack/ros-rolling#51 (without its pkg_additional_info.yaml build-number bumps): stop exporting the lib<pkg>__rosidl_generator_py library, which has unresolved CPython symbols, to C++ consumers of interface packages. The generated conversion code is compiled as an OBJECT library into each Python extension module, and conversion functions of nested types from other packages are looked up from those packages' Python message classes (_CONVERT_FROM_PY/_CONVERT_TO_PY). Adds the std_msgs C++-consumer regression test and a CI cache eviction for std_msgs so it is rebuilt and tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
Every ROS 2 interface package exports its Python C bindings library,
lib<pkg>__rosidl_generator_py, in<pkg>_TARGETS(viaament_export_targetsinrosidl_generator_py). So a plain C++ node that doestarget_link_libraries(node ${std_msgs_TARGETS})links it. That library calls CPython functions but is only meant to be loaded by a Python interpreter:Python3::Module, i.e.-undefined dynamic_lookup): the C++ node aborts at startup:-Wl,-dead_strip_dylibsfrom the compiler activation.compilers2.x (clang 21) no longer setsLDFLAGS, so downstream projects now hit it.Python3::Module: undefined references at link time, orsymbol lookup errorat load time. This is Static linking issue with _rosidl_generator_py.so in build _17 ros-kilted#76. LinkingPython3::Pythonon Linux hides it, but then every C++ node loads libpython.Linking
Python3::Pythonon macOS isn't a fix: it loads a second interpreter into conda-forge's statically linkedpython(see ros2/rosidl_python#253). Upstream #253 is stuck on the Linux side for the same reason: ROS CI links shared libraries with--no-undefined.Fix
This removes the shared library instead of patching around it. It replaces
ros-rolling-rosidl-generator-py.osx.patchwith a cross-platformros-rolling-rosidl-generator-py.patch:_*_s.c) is compiled as anOBJECTlibrary into each<pkg>_s__rosidl_typesupport_*Python extension module, which already linksPython3::Module. Nothing is installed or exported, so<pkg>_TARGETSonly contains C/C++ libraries. No library is left with unresolved CPython symbols, and no linker flags are needed.builtin_interfaces/Timeinstd_msgs/Header): the conversion functions are read (and cached) from the dependency's Python message class. They come from the_CONVERT_FROM_PY/_CONVERT_TO_PYPyCapsules, which rclpy already uses and which the parent's__import_type_support__already loads. This replaces linkinglib<dep>__rosidl_generator_py. Same-package types still call the functions directly.Commits
tests/ros-rolling-std-msgs.yaml). It builds and runs a C++ executable against${std_msgs_TARGETS}(unix only), withLDFLAGScleared so--as-needed/-dead_strip_dylibscan't hide the problem. It also checks that the Python bindings still work (rclpyserialize round-trip).std_msgs→ build 26 so CI runs the test. Expected to fail on macOS. On Linux the library currently links libpython (Python3::Python), which hides the leak: the test passes, but every C++ consumer loads libpython.rosidl_generator_py,rosidl_core_generatorsandrosidl_default_generators→ build 26.std_msgsonly depends on the generator through the latter two, so they have to be rebuilt too; otherwise rattler-build may buildstd_msgsfirst, against the old generator from the channel. The test is expected to pass.testpr.ymlline that drops thestd_msgsbuild 26 cached by this PR's earlier CI runs, which was built with the old generator. Please drop this commit before merging.Local results (rattler-build, this repo)
dyld: symbol not found in flat namespace '_PyExc_RuntimeError'libstd_msgs__rosidl_generator_py→ libpythonPython3::Moduleonly (kilted #76 setup)undefined reference to PyObject_GetAttrString(and friends)std_msgs,geometry_msgs,test_msgs,builtin_interfaces,service_msgsThings to be aware of
lib<dep>__rosidl_generator_py, which a dependency rebuilt with this patch no longer ships. I reproduced this locally: an oldservice_msgsfailed to import against a rebuiltbuiltin_interfaces, and worked after rebuilding it. The other direction (new packages on top of old dependencies) works. This PR only bumps the two packages needed to exercise the test. Pure C++ consumers are unaffected.rosidl_python/rclpytest suites, and code outside the message packages that links<pkg>__rosidl_generator_pydirectly (none found in the rolling env).ros2/rosidl_python.This PR description was AI-generated with Claude Opus 5.5 (
claude-opus-5-5).🤖 Generated with Claude Code