Advice on C++20 modules (Boost)

Hi all,

I’m a Boost author exploring the possibilities offered by C++20 modules for our codebase. My objective is enabling my users to import my libraries, as per import boost.xyz. I’ve done some research (C++20 modules and Boost: an analysis) and the topic seems to have gained some traction on the Boost mailing list.

Our idea is to create a module interface unit per library, exporting what we currently have in our headers. We’d distribute both the headers (as we do today) and the new module code. Since BMIs seem pretty non-portable, we’d ask users to build these module unit themselves (as the standard libraries currently do).

Our official way of consuming Boost code is using CMake, so I’ve gone ahead and played with the new FILE_SET CXX_MODULES feature (which works great). I’m trying to figure out the best way to use this feature for our project:

  • My first attempt was to build a library with a PUBLIC FILE_SET in our CMake, and then install() it, creating a package consumable with find_package. When the consumer uses it (using find_package, then linking against the IMPORTED module target), the module is built (good!). However, the module is built only once - if the users has two executables with BMI-incompatible build flags, the module can’t be used for both.
  • I then wrapped the code required to build a module library in a function that would be made available using find_package (C++20 modules and Boost: an analysis). This looks like a workaround, but allows for building the module as many times as required.
  • While the latter point works great for libraries without dependencies (I used standalone Asio as a playground), I don’t think it scales well to our complex dependency tree. As a user, I’d like to say “I need Boost.Beast” and get all of Beast’s dependent modules built automatically.

Do you have any advice the best way to achieve this?

Many thanks,
Ruben.

1 Like

Please note C++20 Modules, CMake, And Shared Libraries - Crascit

Known issue; on the roadmap. I’ll try to remember to report back here or loop you in when I have something testable.

1 Like

That’s great. I’m happy to help by testing the feature early if that helps.

Will you support building the entire module depency chain (i.e. linking to Boost::beast builds Boost::asio, Boost::system and so on)? Or would dependencies need to be specified manually? Is there an ETA for this?

Many thanks!
Ruben.

Dependencies are specified by target_link_libraries. If beast needs asio, it needs to list it in its link interface.

Nothing I can commit to, sorry. Work needed for it is on the roadmap for this year (hopefully this summer), but “going all the way” isn’t yet planned out.

That’s great, thanks for your help!

This is the issue to watch: https://gitlab.kitware.com/cmake/cmake/-/issues/25539

Thanks. Just to make sure I understood correctly (since I see a lot of seemingly unrelated stuff in this issue), a consumer code like the following:

# Boost::beast is an imported library target, only composed of headers and a C++20 module interface unit
find_package(Boost REQUIRED COMPONENTS Beast)
set(CMAKE_CXX_EXTENSIONS OFF)

add_executable(exe1 exe1.cpp)
target_compile_features(exe1 PRIVATE cxx_std_20)
target_link_libraries(exe1 PRIVATE Boost::beast)

add_executable(exe2 exe2.cpp)
target_compile_features(exe2 PRIVATE cxx_std_23)
target_link_libraries(exe2 PRIVATE Boost::beast)

Will compile the Boost.Beast module and their dependents twice, using the consumer flags (-std=c++20 for exe1 and -std=c++23 for exe2)?

That is the end goal, yes.

1 Like

Another point: some libraries (like Asio) are infamous by their number of configuration macros. For instance, some third-party libraries rely on BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT for compatibility, or on BOOST_ASIO_DISABLE_THREADS for optimizations. Enabling users to build such configurations would be a killing point.

Thanks.

Only ABI-compatible usages can be serviced as the object files are only built once; everything else will use BMI-only outputs and not have a way to satisfy non-inline symbols not provided by the main library. The only real rule that’s going to have to be enforced is that source-specific flags will be assumed to not affect BMI consumption (otherwise we have a BMI set per source file flag set, not just target flag set).

I forgot to mention - all these libraries with lots of configuration macros are header-only. As a user, I’d currently just include the Asio headers in my application, and define macros like BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT to customize behavior.

In a module world, the user would need to have a way to specify which of these macros need to be defined when building their Asio module. There’s a high chance that these macros won’t have been defined when we built and exported the Asio module - since some of them are incompatible. If it helps CMake anyhow, we could list for each library which macros represent configuration options affecting the BMI.

As long as any possible ABI symbol is present in the library the boost_asio library provides (including module initializers), things should be fine. If everything is inline, then the library just needs to provide module initializers for every possible module.

I’m not sure how this will look in CMake, but I suspect that something needs to be done as the only place is -D on the command line. Maybe something like:

#if __has_include(<boost/asio/user/config.hpp>)
#include <boost/asio/user/config.hpp>
#endif
// Polyfills anything not specified with defaults.
#include <boost/asio/default/config.hpp>

?