From f578844537def0d1913ce406cc1191ce51fc33d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domagoj=20=C5=A0ari=C4=87?= Date: Sun, 20 Sep 2026 23:57:56 +0200 Subject: [PATCH] feat: optional C++20 module support (PSI_FUNCTIONOID_MODULE, opt-in) Adds PsiFunctionoidModule (alias Psi::FunctionoidModule), exporting the library's public headers as `import psi.functionoid;` instead of textual includes, for toolchains with usable C++20 module support (clang-cl, GNU-driver clang). Off by default and purely additive: the existing PsiFunctionoid interface target and every header-only consumer are unaffected regardless of whether the option is set. Same shape as psiha/vm's own module support: the global module fragment carries the STL plus the two macro-only psiha deps these headers use (boost/assert.hpp, boost/config_ex.hpp) textually - modules never export macros, so a consumer wanting BOOST_ASSERT after importing still includes it directly, same as consuming Boost anywhere else. The purview exports every real header via file(GLOB_RECURSE), so a header added later is picked up automatically. Verified with a smoke test (test/module_smoke_test.cpp, gated the same way) that imports the module and exercises real callable<> usage, linked against a real executable producing the correct result - and by running the existing 27-test suite unmodified with the option at its default (off). --- CMakeLists.txt | 1 + module.cmake | 91 ++++++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 13 ++++++ test/module_smoke_test.cpp | 23 ++++++++++ 4 files changed, 128 insertions(+) create mode 100644 module.cmake create mode 100644 test/module_smoke_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 09d3e78..eaadc94 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -66,6 +66,7 @@ endif() ################### include( ${CMAKE_CURRENT_LIST_DIR}/functionoid.cmake ) +include( ${CMAKE_CURRENT_LIST_DIR}/module.cmake ) ################### ## Tests diff --git a/module.cmake b/module.cmake new file mode 100644 index 0000000..abf16af --- /dev/null +++ b/module.cmake @@ -0,0 +1,91 @@ +# Optional C++20 named module support for psi::functionoid (opt-in, off by default). +# +# Adds a target, `PsiFunctionoidModule` (alias `Psi::FunctionoidModule`), exporting every +# functionoid public header as `import psi.functionoid;` instead of textual `#include`. Purely +# additive: PsiFunctionoid (the existing header-only INTERFACE target) is untouched, so any +# consumer vendoring these headers directly is unaffected regardless of whether this option exists. +# +# Shape: matches psiha/vm's own module.cmake (feat/module-support there) - the global module +# fragment carries the STL plus the two macro-only psiha deps these headers use (boost/assert.hpp, +# boost/config_ex.hpp) textually, since modules never export macros; a consumer wanting +# BOOST_ASSERT after `import psi.functionoid;` still includes boost/assert.hpp itself. The purview +# (`export extern "C++"`) carries every real header via file(GLOB_RECURSE), so a header added +# later is picked up automatically. +# +# Toolchain: clang-cl and the GNU-driver clang only (see psiha/vm's module.cmake for why); MSVC +# and GCC are left alone, a warning rather than a hard error when the option is set there anyway. + +option( PSI_FUNCTIONOID_MODULE "Build psi.functionoid as a C++20 named module (opt-in; requires clang-cl or clang)" OFF ) + +if ( PSI_FUNCTIONOID_MODULE ) + if ( NOT ( CMAKE_CXX_COMPILER_ID MATCHES Clang ) ) + message( WARNING "PSI_FUNCTIONOID_MODULE requires clang-cl or clang; ignored on ${CMAKE_CXX_COMPILER_ID}." ) + else() + if ( CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC" ) + cmake_path( GET CMAKE_CXX_COMPILER PARENT_PATH _psi_functionoid_llvm_bin ) + find_program( PSI_FUNCTIONOID_CLANG_SCAN_DEPS clang-scan-deps HINTS "${_psi_functionoid_llvm_bin}" REQUIRED ) + string( CONCAT CMAKE_CXX_SCANDEP_SOURCE + "\"${PSI_FUNCTIONOID_CLANG_SCAN_DEPS}\" -format=p1689 -- " + " /TP /c /Fo /clang:-MT /clang: /clang:-MD /clang:-MF /clang:" + " > .tmp && \"${CMAKE_COMMAND}\" -E rename .tmp " ) + set( CMAKE_CXX_MODULE_MAP_FORMAT clang ) + set( CMAKE_CXX_MODULE_MAP_FLAG "@" ) + set( CMAKE_CXX_COMPILE_BMI " /clang:-o /clang:--precompile /TP " ) + endif() + + file( GLOB_RECURSE psi_functionoid_module_headers RELATIVE "${CMAKE_CURRENT_LIST_DIR}/include" "${CMAKE_CURRENT_LIST_DIR}/include/*.hpp" ) + + set( _psi_functionoid_module_purview "${CMAKE_CURRENT_BINARY_DIR}/psi_functionoid_module_purview.hpp" ) + set( _psi_functionoid_module_content "// Generated by module.cmake - every psi::functionoid public header, cumulative.\n" ) + foreach( h ${psi_functionoid_module_headers} ) + string( APPEND _psi_functionoid_module_content "#include <${h}>\n" ) + endforeach() + file( WRITE "${_psi_functionoid_module_purview}" "${_psi_functionoid_module_content}" ) + + set( _psi_functionoid_module_unit "${CMAKE_CURRENT_BINARY_DIR}/psi_functionoid_module.cppm" ) + file( WRITE "${_psi_functionoid_module_unit}" "\ +module; +#if defined( __x86_64__ ) || defined( _M_X64 ) +#include +#endif +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include +#undef ERROR +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +export module psi.functionoid; +export extern \"C++\" +{ +#include \"${_psi_functionoid_module_purview}\" +} +" ) + + add_library( PsiFunctionoidModule STATIC ) + add_library( Psi::FunctionoidModule ALIAS PsiFunctionoidModule ) + target_compile_features( PsiFunctionoidModule PUBLIC cxx_std_23 ) + target_sources( PsiFunctionoidModule PUBLIC FILE_SET CXX_MODULES BASE_DIRS "${CMAKE_CURRENT_BINARY_DIR}" FILES "${_psi_functionoid_module_unit}" ) + target_include_directories( PsiFunctionoidModule PUBLIC "${_functionoid_include}" ) + if ( TARGET Boost::boost ) + target_link_libraries( PsiFunctionoidModule PUBLIC Boost::boost ) + endif() + target_compile_options( PsiFunctionoidModule PRIVATE + $<$:/clang:-std=gnu++2c /clang:-Wno-include-angled-in-module-purview /clang:-Wno-reserved-module-identifier> + $<$>:-Wno-include-angled-in-module-purview -Wno-reserved-module-identifier> + ) + endif() +endif() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index df07632..45714bc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -34,3 +34,16 @@ target_link_libraries( functionoid_vtable_attrs PRIVATE GTest::gtest_main Psi::F set_target_properties( functionoid_vtable_attrs PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/test" ) add_test( NAME functionoid_vtable_attrs COMMAND functionoid_vtable_attrs ) + +# Module correctness smoke test (module.cmake, PSI_FUNCTIONOID_MODULE) — only built when the +# option is on, so it adds nothing to the default (off) CI matrix. +if ( PSI_FUNCTIONOID_MODULE AND TARGET PsiFunctionoidModule ) + add_executable( functionoid_module_smoke_test EXCLUDE_FROM_ALL "${CMAKE_CURRENT_SOURCE_DIR}/module_smoke_test.cpp" ) + target_link_libraries( functionoid_module_smoke_test PRIVATE Psi::FunctionoidModule ) + set_target_properties( functionoid_module_smoke_test PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/test" + CXX_SCAN_FOR_MODULES ON + DISABLE_PRECOMPILE_HEADERS ON + ) + add_test( NAME functionoid_module_smoke_test COMMAND functionoid_module_smoke_test ) +endif() diff --git a/test/module_smoke_test.cpp b/test/module_smoke_test.cpp new file mode 100644 index 0000000..5950739 --- /dev/null +++ b/test/module_smoke_test.cpp @@ -0,0 +1,23 @@ +// Standalone correctness check for the psi.functionoid module (module.cmake, +// PSI_FUNCTIONOID_MODULE): builds a small executable that only `import`s psi.functionoid and +// exercises real callable<> usage, so a broken module boundary shows up as a build or runtime +// failure here rather than surfacing only downstream in a consumer project. +import psi.functionoid; + +#include +#include + +int main() +{ + int captured{ 10 }; + psi::functionoid::callable fn{ [&]() noexcept -> int { return captured + 32; } }; + int const result{ fn() }; + BOOST_ASSERT( result == 42 ); + + fn = []() noexcept -> int { return 0; }; + int const reassigned{ fn() }; + + int const sum{ result + reassigned }; + std::printf( "%d\n", sum ); + return sum == 42 ? 0 : 1; +}