Project: Exposing HPX using C++20 Modules
Contributor: Haokun Wu
Org: STE||AR Group
Primary PR: STEllAR-GROUP/hpx#6761
Move HPX toward C++20 modules while keeping backward compatibility:
- Existing users can keep
#include <hpx/...>with no changes. - Users can alternatively write
import HPX.Core;and use the same APIs.
Secondary goals:
- export both template and non-template symbols correctly;
- establish minimal, maintainable build/macro mechanics to expose more libraries over time.
HPX is a large and template-heavy codebase, so exposing everything in one summer is unrealistic. Instead, this project focused on solving the hard design problems (exporting symbols correctly, avoiding ODR violations, macro migration, etc.), and delivering a working proof-of-concept: the version module.
A common approach is duplicating headers into .ixx files. That adds maintenance overhead. Instead, each HPX library gets a single umbrella .ixx that re-exports its headers. For core:
// libs/core/hpx_core.ixx
module; // global fragment
#include <algorithm>
#include <string>
#include <cstdint>
#define HPX_BUILD_MODULE
#include <hpx/config.hpp>
export module HPX.Core;
// headers that define the public surface
#include <hpx/modules/version.hpp>This keeps the tree lean, preserves header inclusion, and allows import HPX.Core;.
Header builds rely on HPX_CORE_EXPORT (DLL/export visibility). For modules:
- Templates can be exported with
export. - Non-templates must use
export extern "C++"so the linker finds the compiled symbol. - To avoid double attributes, when building a module we undefine and empty out
HPX_CORE_EXPORT.
Minimal macro surface (from export_definitions.hpp):
#if defined(HPX_BUILD_MODULE)
# if defined(HPX_HAVE_ELF_HIDDEN_VISIBILITY)
# undef HPX_CORE_EXPORT
# define HPX_CORE_EXPORT /* empty */
# endif
# define HPX_CORE_MODULE_EXPORT_EXTERN export extern "C++"
#else
# define HPX_CORE_MODULE_EXPORT_EXTERN extern "C++"
#endif
#define HPX_CORE_MODULE_EXPORT \
HPX_CORE_MODULE_EXPORT_EXTERN HPX_CORE_EXPORTUse HPX_CORE_MODULE_EXPORT in place of HPX_CORE_EXPORT when a declaration is exposed through a module.
Before (headers):
HPX_CORE_EXPORT std::uint8_t agas_version();After (module):
HPX_CORE_MODULE_EXPORT std::string build_date_time();a) Register the .ixx as a C++ module interface and install BMIs
Essentials only (from libs/CMakeLists.txt):
if(HPX_WITH_CXX_MODULES)
target_sources(hpx_${lib}
PUBLIC FILE_SET hpx_${lib}_public_sources TYPE CXX_MODULES FILES
${CMAKE_CURRENT_SOURCE_DIR}/${lib}/hpx_${lib}.ixx
)
set(module_installation
FILE_SET hpx_${lib}_public_sources DESTINATION ${CMAKE_INSTALL_LIBDIR}/cxx/miu
CXX_MODULES_BMI DESTINATION ${CMAKE_INSTALL_LIBDIR}/cxx/bmi
COMPONENT runtime
)
endif()
install(TARGETS hpx_${lib}
EXPORT HPXInternalTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT runtime
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT runtime
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime
${module_installation}
)What this does:
- declares the
.ixxas aCXX_MODULESfile set so compilers produce BMIs; - installs the module interface units and BMIs alongside libraries for consumers.
b) Generated global header chooses #include or import at build time
Essentials only (from cmake/templates/global_module_header_modules.hpp.in):
#pragma once
#include <hpx/config/defines.hpp>
#if !defined(HPX_HAVE_CXX_MODULES) || \
defined(HPX_APPLICATION_DOESNT_USE_CXX_MODULES) || \
defined(HPX_CORE_EXPORTS) || defined(HPX_FULL_EXPORTS) || \
defined(HPX_LIBRARY_EXPORTS) || defined(HPX_COMPONENT_EXPORTS) || \
defined(HPX_BUILD_MODULE)
@module_headers@ // expands to traditional #includes
#else
import HPX.Core; // module path when consumers opt into modules
#endifEffect:
- Libraries building HPX itself (or consumers that opt out) keep using
#include. - Consumers with modules enabled transparently switch to
import HPX.Core;. - Tests and examples are kept untouched and CI passed on both modes
HPX.Coreumbrella module (libs/core/hpx_core.ixx) added.- Version API exposed through modules with the new macros.
- Build system produces and installs module interfaces and BMIs.
- Backward compatibility validated: headers continue to work;
import HPX.Core;also works. - Tested across major compilers used in CI for the PR.
- Backward compatibility drives the shape of both macros and build logic.
- Non-template exports require
export extern "C++"to yield linkable symbols. - System headers must stay in the global module fragment to avoid ODR and diagnostic issues.
- Avoiding duplication with the umbrella pattern simplifies maintenance while enabling gradual migration.
- Continue exposing additional
libs/coremodules using the same pattern. - Tighten CMake integration so newly exposed modules require minimal boilerplate.
- Measure build and runtime effects of imports vs includes in representative HPX applications.
- Main work: PR #6761 — includes
hpx_core.ixx, macro updates inexport_definitions.hpp, version API changes, and CMake additions.
This work establishes a module-aware export scheme, an umbrella interface for HPX.Core, and CMake plumbing that installs module interfaces and BMIs. Most importantly, it preserves backward compatibility: existing #include code continues to compile unchanged, and users can opt in to import HPX.Core;. I will keep extending module coverage across HPX using this approach.
I would like to thank my mentors, Dr. Hartmut Kaiser and Panagiotis Syskakis, for their guidance and feedback throughout the project. Their expertise in both C++ and HPX design decisions was crucial for navigating the challenges of symbol export, module boundaries, and build system integration. I also thank the STE||AR Group community for code reviews, discussions, and support during GSoC.

