Back to Onetbb

C++20 Modules Support for oneTBB

rfcs/proposed/cxx20_modules/README.md

2023.1.09.9 KB
Original Source

C++20 Modules Support for oneTBB

Introduction

C++20 introduced modules as a modern alternative to the traditional header-based inclusion model. Modules offer several advantages over headers:

  • Modules are compiled once and their binary interface is reused, avoiding repeated parsing and preprocessing of header files.
  • Modules provide better encapsulation — macros, internal declarations, and implementation details do not leak into the consumer's translation unit.

As the C++ ecosystem gradually adopts modules, oneTBB should provide a module interface to allow users to use the library using import tbb; instead of #include <oneapi/tbb.h>.

Proposal

ABI non-breaking style with using-declaration

The proposed approach is to provide a wrapper module that includes the existing headers in the global module fragment and re-exports public symbols as suggested in LLVM documentation.

The module interface unit (e.g., tbb.cppm) would follow this structure:

cpp
module;

// Global module fragment — include all public headers
#include <oneapi/tbb.h>

export module tbb;

// Re-export public API via using-declarations
export namespace tbb {
    // Parallel algorithms
    using tbb::parallel_for;
    using tbb::parallel_reduce;
    using tbb::parallel_scan;
    using tbb::parallel_sort;
    using tbb::parallel_invoke;
    using tbb::parallel_pipeline;
    using tbb::parallel_for_each;

    // Flow Graph API
    namespace flow {
        using tbb::flow::graph;
        // ... other flow graph API
    }

    // other TBB API
}

Consumer code uses the module as follows:

cpp
import tbb;

int main() {
    // oneapi:: namespace is also available
    tbb::parallel_for(0, 100, [](int i) {
        // Nested namespaces are available as well
        tbb::this_task_arena::isolate([] {
            // ... do some work in isolation
         });
    });
}

The proposed approach has several advantages over other alternatives:

  • It does not require the modification of existing headers, as the module includes them as part of a global module fragment and then selectively exports the necessary using declarations. Thus, development can continue in headers.
  • Since the API is declared as part of a global module fragment, the name of the module will not participate in name mangling, making translation units compiled with modules, compatible with the units compiled with regular headers.

The disadvantage of this approach is an obligation to update the module interface unit each time new API is added.

CMake integration

The project policy requires C++11 as the minimum standard, hence the module source cannot be compiled as part of the oneTBB build because it would force C++20 standard on the library. With the ABI non-breaking approach, the library built with C++11 can be used together with the module interface unit compiled with C++20 or higher. Furthermore, a prebuilt module constrains the consumer to the exact C++ standard version used during the module build. Thus, the .cppm file is installed as a source file and compiled on the consumer side.

cmake
find_package(TBB REQUIRED)

add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE TBB::tbb)
# tbb.cppm file is located under include/oneapi
get_target_property(_tbb_include_dir TBB::tbb INTERFACE_INCLUDE_DIRECTORIES)
target_sources(myapp PRIVATE
   FILE_SET cxx_modules TYPE CXX_MODULES
   BASE_DIRS ${_tbb_include_dir}
   FILES ${_tbb_include_dir}/oneapi/tbb.cppm
)

In the future, TBBConfig.cmake could provide a helper function (e.g., tbb_enable_cxx20_modules(myapp)) to reduce this boilerplate.

The consumer compiles the .cppm as part of their own target with their own flags. Name mangling is unchanged, so the consumer links against the same libtbb.so / tbb.lib regardless of whether they use modules or headers.

Testing

Module-based API availability is tested by extending test_tbb_header. A C++20 module interface unit (test_tbb_header_module.cppm) either includes TBB headers in its global module fragment or imports the tbb module, then exports the test code. A consumer imports this module to verify public API accessibility. An ABI compatibility check, in particular, combining module and header based TBB usage across multiple translation units, can also be performed.

Running the whole TBB test suite with import tbb; seems to provide little value, since the module unit is implemented as a wrapper around headers. It may help catch some missing exported API, but implementing and maintaining such an "import" mode can be challenging and costly. Hence, the proposed testing approach seems to be a good balance between coverage and maintainability.

Alternatives Considered

1. ABI non-breaking style with language linkage block

Instead of re-exporting symbols via using-declarations, library headers are included inside an extern "C++" language linkage block within the module purview, and all third-party/system headers are included in the global module fragment.

cpp
module;

// Global module fragment — system and third-party headers only
#include <cstddef>
#include <atomic>
#include <functional>
// ... all standard/third-party headers used by TBB

export module tbb;

#define __TBB_IN_MODULE_USE
extern "C++" {
    #include "oneapi/tbb.h"
}

Declarations within an extern "C++" block inside the module purview are actually attached to the global module fragment by the standard. An important detail here is that TBB headers will need to define some macro (e.g. __TBB_CXX20_EXPORT) if __TBB_IN_MODULE_USE is defined. The definition of __TBB_IN_MODULE_USE would also mean that all inclusions of third-party headers should be excluded from TBB headers.

Pros:

  • No need to maintain explicit using-declarations for every public symbol.
  • To export an API you simply add __TBB_CXX20_EXPORT to the declaration.
  • ABI-compatible with header-based consumers.

Cons:

  • Requires careful separation of system/third-party headers from TBB headers. If TBB header includes a system header that was not already in the GMF, it can cause compilation errors.
  • Requires the modification of all TBB headers to add conditional export keyword to public declarations and stripping the inclusion of third-party headers from them.

2. ABI breaking style

Same header-inclusion approach, but library headers are included directly in the module purview without an extern "C++" block. This causes the module name to participate in symbol mangling.

cpp
module;

// Global module fragment — system and third-party headers only
#include <cstddef>
#include <atomic>
#include <functional>
// ... all standard/third-party headers used by TBB

export module tbb;

#define __TBB_IN_MODULE_USE
#include <oneapi/tbb.h>

Pros:

  • No need to maintain explicit using-declarations for every public symbol.
  • To export an API you simply add __TBB_CXX20_EXPORT to the declaration.
  • The library could be compiled to include symbols with and without module name mangled. That would require the consumer of the library to consistently use either TBB headers, or TBB module.

Cons:

  • The previously mentioned advantage can be seen as disadvantage. The user must choose either modules or headers because they cannot coexist in the same program. It also might cause problems when the consumer's application uses TBB as module and other third-party that uses TBB, but as headers, or vice versa.
  • Same cons as for ABI non-breaking style described above.

Open Questions

  1. Besides the single tbb module, should fine-grained submodules (e.g., tbb.flow_graph, tbb.containers) be provided so that consumers can precompile and import only what they need?

  2. How to provide preview functionality with modules? TBB preview features are currently gated by TBB_PREVIEW_* macros that are defined before including headers. Macros defined by the consumer before import tbb; cannot influence the module's already-compiled interface. One option is to provide a separate tbb.preview module. However, some preview features are extensions to existing classes and exporting them would create distinct types in a tbb.preview module, which raises questions about whether tbb and tbb.preview modules can be used together.

    • Current workaround is to require the consumer to compile tbb.cppm with desired TBB_PREVIEW_* macros defined.
  3. How to support public macros such as TBB_VERSION or feature-test macros? Modules do not export preprocessor definitions, so these macros are not visible after import tbb;. Some options:

    • Replace them with inline variables where possible or provide exported functions.
    • Require the consumer to #include <tbb/version.h> alongside the import.
  4. Should the module be split into partitions (e.g., tbb:algorithms, tbb:containers, tbb:flow_graph) to organize the using-declarations? Partitions are internal to the module and not importable by consumers, but could improve maintainability of the module interface.

  5. Currently there is a limitation with transitive inclusion of the STL headers. Certain combinations of standard headers inclusions and C++23 standard module imports can cause compilation errors. It in unclear whether this is a compiler limitation or a limitation of modules in general. Regardless, the question is how to address this issue (e.g. allow the consumer to switch between includes and imports in TBB module unit) and how should the testing suite be extended to cover these problematic scenarios.

Exit Criteria

The following conditions should be met before this feature graduates from experimental to fully supported:

  1. The open questions above are resolved.
  2. CMake and compiler support should improve to the point where modules support is not at experimental stage.