The Wrapper Trick: How Boost.MySQL Got to 'import boost.mysql' Without Rewriting Everything
Source: isocpp
C++20 modules have been in the standard for more than five years. The major compilers support them: GCC 14+ removed the experimental flag requirement in GCC 15, Clang 16+ has stable named module support, and MSVC has been the most complete implementation for years. CMake 3.28, released in December 2023, added first-class dependency scanning via the FILE_SET CXX_MODULES syntax. By any technical measure, the infrastructure to ship modules exists.
And yet the C++ library ecosystem has not moved. Open any major library today and you will find #include at the top of every header, every source file, every example. Rubén Pérez Hidalgo’s talk catalogued on isocpp.org addresses this from a useful vantage point: he maintains Boost.MySQL, which is currently the only Boost library to ship native module support. Getting there required navigating a set of problems that are distinct from “does the compiler parse this correctly,” and those problems are why the ecosystem is still where it is.
The wrapper approach
The most consequential design decision Boost.MySQL made was not to rewrite its internals as a module. Instead, it ships a separate module interface unit that wraps the existing header tree using the global module fragment:
module; // global module fragment
#include <boost/mysql.hpp> // existing headers, preprocessor and all
export module boost.mysql; // named module begins
export namespace boost::mysql {
using boost::mysql::connection;
using boost::mysql::results;
using boost::mysql::statement;
using boost::mysql::error_code;
// ... remaining public API
}
The global module fragment is the section between the bare module; and the export module declaration. Everything included there is treated as legacy translation-unit content. Macros, transitive includes, implementation details, all of it compiles in that zone and stays there. The named module section that begins with export module boost.mysql; is the clean interface: it sees the symbols defined by the headers, and it exports exactly what it re-exports explicitly.
This pattern works because it separates two different problems. The first problem is that existing headers are full of macros, conditional compilation guards, and transitive dependencies that span thousands of files. Untangling any moderately sized library to make its internals genuinely modular is a multi-year rewrite with significant breakage risk. The second problem is that consumers of the library want a clean, macro-free interface unit they can import. The wrapper solves the second problem without touching the first.
The tradeoff is that the library itself does not gain the internal compilation benefits of modules. The headers still compile the same way they always have. The speedup only accrues on the consumer side, where importing a pre-compiled BMI is faster than re-parsing the header tree on every translation unit.
Why macros are the deepest problem
Modules do not export macros. This is deliberate: macros are a preprocessor feature, they operate before the module system sees anything, and the isolation that makes modules valuable depends on them not leaking. The global module fragment exists precisely to contain legacy macro-heavy code.
For Boost.MySQL, this is workable because the public API is types and functions. But a substantial portion of Boost libraries use macros as first-class public API. BOOST_ASSERT, BOOST_THROW_EXCEPTION, configuration macros that change library behavior, contract-checking macros: none of these survive the module boundary. A library that exports connect() but not BOOST_MYSQL_NO_TLS can provide a module interface. A library whose users depend on preprocessor conditionals to configure behavior fundamentally cannot, at least not without providing a separate configuration header that users still need to #include.
This means the realistic end state for C++ modules in an ecosystem like Boost is not replacement but layering. Headers remain the authoritative interface. Modules are an opt-in convenience layer for users who want cleaner translation unit boundaries and do not depend on macros. That is a useful thing to have, but it is different from the clean break that “modules replace headers” implies.
Build system: what works and what does not
CMake 3.28 made the dependency scanning problem tractable. Before it, build systems had to infer module dependency order manually or through fragile workarounds, because modules introduce a sequential constraint that header-based builds do not have: a translation unit importing boost.mysql cannot compile until the binary module interface (BMI) for boost.mysql exists. The BMI is produced by compiling the module interface unit. You cannot parallelize across that edge.
CMake 3.28 handles this by scanning source files before compilation to extract module dependency information, then generating the correct build order. The syntax is:
cmake_minimum_required(VERSION 3.28)
project(example)
add_library(boost_mysql)
target_sources(boost_mysql PUBLIC
FILE_SET CXX_MODULES FILES
src/boost.mysql.cppm
)
The FILE_SET CXX_MODULES annotation is what triggers scanning. CMake builds the dependency graph and sequences the BMI generation steps before the main compilation phase. This works, and it works reliably with GCC 14+, Clang 16+, and MSVC 19.34+.
The package manager side is much worse. As of late 2024 when Pérez Hidalgo documented the Boost.MySQL module support, neither Conan nor vcpkg had stable support for packages that expose module interfaces. The root cause is that BMI files are not portable. A .pcm file compiled with Clang 18.1 is not compatible with Clang 18.2. A .gcm file from GCC 14 is not compatible with GCC 15. Package registries distribute precompiled artifacts, but there is no standardized format for precompiled module interfaces, and the per-compiler-version fragmentation makes distribution essentially impossible.
In practice this means every consumer must compile the module interface from source. Package managers can distribute the .cppm source file alongside the headers, but the build integration to pick the right path and correctly invoke the scanning step requires tooling that has not shipped yet in the major package managers.
The organizational problem at Boost scale
Boost has roughly 180 libraries, each maintained independently by its own author or small team. There is no central authority, no shared release engineering team, and no uniform build system. The RFC Pérez Hidalgo published on the Boost developers list in late 2024 proposed a naming convention (boost.library_name), a wrapper file approach, and a minimum CMake version requirement, but adoption is voluntary.
The economics for any individual maintainer are unfavorable. Implementing module support means learning the wrapper pattern, writing and maintaining a .cppm file, testing across three compiler families (each with its own quirks and version constraints), and keeping pace as compiler module support continues to evolve. The maintenance burden is ongoing: a GCC version bump that fixes a module bug may also change behavior that your workarounds relied on. The users who benefit are a small fraction of the library’s total users, mostly those on modern toolchains and greenfield projects.
Boost.MySQL can absorb this cost because its maintainer is also the person proposing the ecosystem-wide approach, with a vested interest in proving the pattern works. That situation does not generalize to Boost.Filesystem, Boost.Spirit, or Boost.Asio.
What the import std; timeline reveals
C++23 standardized import std; and import std.compat;, which import the entire standard library as a single module. MSVC shipped this in Visual Studio 2022 17.5 in February 2023. GCC 15, released in early 2025, added support. Clang’s libc++ implementation is still incomplete.
The standard library is the best-case scenario for module adoption: a small number of well-resourced organizations, tight integration between compiler and library teams, and a clear standard target to implement against. It took roughly three years from C++20 standardization to reliable import std; across two of the three major compilers, and the third is still catching up in 2026.
Third-party library maintainers have none of those advantages. No compiler team integration, no standardized testing matrix, no budget for the implementation work. The standard library timeline is an optimistic lower bound for how long broad library adoption will take.
The realistic picture
Modules as a language feature are not broken. The wrapper pattern is sound. The CMake integration works. GCC 15 and Clang 16+ are stable enough to ship against. If you are starting a new C++ project today with full control over your toolchain, writing your own code as named modules is viable, and import std; on GCC or MSVC is a genuine improvement over header forests.
The gap is in the library ecosystem, and it is structural rather than technical. Libraries cannot rewrite their internals to be natively modular without breaking existing users. Package managers cannot distribute BMIs portably. Individual maintainers face a real cost with diffuse benefit. These are not problems that compiler implementers can fix; they require tooling investment from CMake, Conan, vcpkg, and individual library authors over a period of years.
Boost.MySQL’s path to import boost.mysql is a proof of concept for a concrete approach: wrapper files, global module fragments for the messy header layer, explicit re-exports for the clean interface, CMake 3.28 for build integration. It does not require rewriting anything. What it requires is time and maintainer attention, which are the genuinely scarce resources in open source C++ infrastructure.