Local import std module on C++23 with Bazel
The evolution of C++ language brings periodic waves of innovation, but some of them are so disruptive that even stable tools need big…
Local import std module on C++23 with Bazel
The evolution of C++ language brings periodic waves of innovation, but some of them are so disruptive that even stable tools need big adjusments. The introduction of CXX Modules in C++20 required such hard adjustments, and specially after the standard library modules appeared in C++23 with the novel “import std” statement. Build tools like CMake and Bazel took some time to adjust to this new reality, so finally we are able to test them properly on CMake 4 and Bazel 9. For CMake 4 with compilers Clang and GCC, it has been recently covered in the text “It’s Time to Use CXX Modules on Modern C++”, but for Bazel 9 we still lack written material. This text tries to cover this gap, while including the warning that this topic is still very experimental and the best experience I’ve had so far only covers the compiler Clang with Bazel 9. So, let’s go!
CXX Modules and the Standard Library module
We review some fundamentals first. What are CXX Modules? Basically, they are isolated C++ units called Module Interfaces, usually with extension .cppm (or .cxx, or even .ixx) and with file content beggining with “module;” or “export module NAME;”. They can be compiled into Compiled Module Interfaces (CMIs), with extension .pcm for Clang (or .gcm for GCC), and then built together with other C++ sources (usually .cpp, .cc, etc). The modules are seen as a replacement for Header units (usually .h or .hpp) and specially for the Header Only paradigm, that has tried to simplify development and dependency management in C++ ecosystem. So, when some source file does “import XXX;”, this means that some module XXX exists somewhere.
This is also the case for “import std;”, meaning that module std (the standard library) has to be built specifically for your project and its binary has to be provided as a CMI during the build process. It means that by using import std from C++23, the user no longer needs to #include multiple headers like <string>, <vector>, etc, since all of them will be automatically provided by standard library module. This innovation aims to decrease the build times in C++ and also to reduce the binary sizes, by only really importing what is necessary. These are long-term goals, not yet fully achieved, but I have already seen huge benefits in build times on personal C++ projects, from minutes reduced to seconds. But how to automatically build the standard library and where to find its sources? And how to do it in Bazel 8 and in latest Bazel 9? This is the main topic of this text.
A Hello World module example
We consider the following hello world example from GitHub project igormcoelho/rules_cpp23_modules (adapted from rnburn/rules_cc_module), consisting of a C++ source main.cpp and a module interface hello.cppm:
// main.cpp
import hello;
int main() {
say_hello("world");
return 0;
}
// hello.cppm
export module hello;
import std;
export inline void say_hello(std::string_view name) {
std::println("Hello {}!", name);
}
The logic is pretty straightforward: the entrypoint main() requires a function say_hello from an imported module hello; and the exported module hello requires importing the module std to be able to std::print some function argument. The C++ code itself is very easy, so let’s try to build it on Bazel.
Building the Hello World with Bazel
The easiest way to build this example is by using latest Bazel (currently Bazel 9.1.0) with compiler Clang (my tests consider clang 21). Bazel 9 silently introduced support for CXX Modules, although still quite experimental (what really justifies the lack of strong advertisement, in my opinion), so do not assume that this is some sort of “official” recommendation, our goal here is just to be able to build and experiment C++23 import std with modules on Bazel. Although CXX Modules are supported in Bazel, we still need some way to expose our local standard library into the project, so here we will experiment with some alternatives.
Solution 1: exposing std modules with new_local_repository on Bazel 9.1 with clang
The proposed solution is to expose local standard library as a local repository with new_local_repository. This assumes that each project will have a different way to build its own std modules, so note that this can also become a challenge if different bazel projects give different names to this repository! In this example, we will assume the name std_modules for this repository and expose it by creating an extensions.bzl file and loading it in MODULE.bazel:
# extensions.bzl
load("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository")
def _libcxx_extension_impl(mctx):
path = None
for mod in mctx.modules:
for tag in mod.tags.configure:
path = tag.path
if not path:
path = mctx.os.environ.get("LIBCXX_MODULE_PATH", "/usr/lib/llvm-21/share/libc++/v1")
if not mctx.path(path).exists:
fail("libc++ module path '{}' does not exist. Install libc++-dev or set correct path.".format(path))
new_local_repository(
name = "std_modules",
path = path,
build_file_content = '''
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "std_modules",
deps = [
":std",
":std_compat",
],
visibility = ["//visibility:public"],
)
cc_library(
name = "std",
features = ["cpp_modules"],
srcs = glob(["std/*.inc"]),
module_interfaces = ["std.cppm"],
visibility = ["//visibility:public"],
)
cc_library(
name = "std_compat",
features = ["cpp_modules"],
srcs = glob(["std.compat/*.inc"]),
module_interfaces = ["std.compat.cppm"],
deps = [":std"],
visibility = ["//visibility:public"],
)
''',
)
configure = tag_class(attrs = {
"path": attr.string(mandatory = False),
})
local_libcxx_extension = module_extension(
implementation = _libcxx_extension_impl,
tag_classes = {"configure": configure},
)
# MODULE.bazel
module(name = "hello_world_project")
bazel_dep(name = "rules_cc", version = "0.2.17")
std_modules = use_extension("//:extensions.bzl", "local_libcxx_extension")
#std_modules.configure(path = "/usr/lib/llvm-21/share/libc++/v1")
use_repo(std_modules, "std_modules")
# .bazelrc
build --repo_env=BAZEL_COMPILER=clang
build --repo_env=BAZEL_CXXOPTS=-stdlib=libc++
build --repo_env=BAZEL_LINKOPTS=-stdlib=libc++
build --repo_env=LIBCXX_MODULE_PATH=/usr/lib/llvm-21/share/libc++/v1
build --experimental_cpp_modules
build --cxxopt=-std=c++23
This local std_modules repository assumes that libc++ module interfaces for standard library are located in folder /usr/lib/llvm-21/share/libc++/v1, but this depends on compiler version and also the operating system. So, each user will have to adjust this ENV variable LIBCXX_MODULE_PATH with the correct path, or if all users share the same environment, this path can be put directly into extensions.bzl as some default value, or even by manually passing it during the configure() step of new_local_repository.
Finally, the BUILD target can easily depend on @ std_module (remember to enable the experimental features = [“cpp_modules”]):
# BUILD
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")
cc_library(
name = "hello",
features = ["cpp_modules"],
module_interfaces = ["hello.cppm"],
deps = ["@std_modules"]
)
cc_binary(
name = "myproject",
srcs = ["main.cpp"],
deps = [":hello"],
features = ["cpp_modules"]
)
This solution works fine with Bazel 9.1.0 and clang 21 (as shown in igormcoelho/rules_cpp23_modules/demo9/hello-world-project):
$ bazel build ...
Starting local Bazel server (9.1.0) and connecting to it...
INFO: Analyzed 2 targets (86 packages loaded, 605 targets configured).
INFO: From Compiling std.cppm:
external/+local_libcxx_extension+std_modules/std.cppm:167:15: warning: 'std' is a reserved name for a module [-Wreserved-module-identifier]
167 | export module std;
| ^
1 warning generated.
INFO: From Compiling std.compat.cppm:
external/+local_libcxx_extension+std_modules/std.compat.cppm:83:15: warning: 'std' is a reserved name for a module [-Wreserved-module-identifier]
83 | export module std.compat;
| ^
1 warning generated.
INFO: Found 2 targets...
INFO: Elapsed time: 5.492s, Critical Path: 2.26s
INFO: 34 processes: 13 internal, 21 linux-sandbox.
INFO: Build completed successfully, 34 total actions
And then, run it:
$ bazel run :myproject
INFO: Analyzed target //:myproject (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //:myproject up-to-date:
bazel-bin/myproject
INFO: Elapsed time: 0.196s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/myproject
Hello world!
This approach was inspired by a suggestion made by github user unennhexium on the discussion https://github.com/rnburn/rules_cc_module/issues/19, but without the need to manually symlink the libc++ to the local folder. This works fine with Bazel 9.1.0, but it still requires manually introducing the libc++ path, so let’s explore other automated ways.
Solution 2: exposing std modules with rules_cc override on Bazel 9.0.0 with clang
When considering Bazel 9.0.0 it is currently possible to use the solution by PikachuHyA in branch support_std_module for project rules_cc. An example is given in repository PikachuHyA/bazel_cxx20_modules_demo. I managed to make it work with Bazel 9.0.0 and clang 21, but it broke after changes on Bazel 9.0.1, and also does not work in latest Bazel 9.1.0. I tried to fix it, but did not succeed, so this example will only work specifically with Bazel 9.0.0.
Solution is quite similar to the previous one, however it is more automated, so the location of libc++ standard modules does not need to be manually informed.
# BUILD
load("@rules_cc//cc:defs.bzl", "cc_binary")
cc_binary(
name = "hello",
srcs = ["main.cpp"],
features = ["cpp_modules"],
module_interfaces = ["hello.cppm"],
deps = ["@local_config_cc//:std_modules"],
)
# MODULE.bazel
module(name = "demo")
bazel_dep(name = "rules_cc")
git_override(
module_name = "rules_cc",
remote = "https://github.com/PikachuHyA/rules_cc.git",
branch = "support_std_module",
)
cc_configure = use_extension("@rules_cc//cc:extensions.bzl", "cc_configure_extension")
use_repo(cc_configure, "local_config_cc")
This solution creates some repository called local_config_cc that target std_modules by overriding the rules_cc with some fork provided by the author. The advantage of this solution is that the discovery of standard modules is fully automated, although some breaking changes on Bazel project after version 9.0.1 will currently generate errors.
Solution 3: manually compiling std modules on Bazel 8 with clang or GCC
If users cannot use clang or cannot use latest Bazel, it is always possible to manually compile the standard library modules and use them (very experimentally!) with project igormcoelho/rules_cpp23_modules. A previous version of this approach was discussed in the text Experimenting C++23 import std with Bazel and Clang (but it didn’t work for GCC at that time, only Clang was supported!). This project was inspired by rules_cc_module from user rnburn, that focused in C++20 modules and has been fully functional for many years already. The problem with this solution is that the rules are slightly different from rules_cc, so maintainance becomes much more complicated, and limiting Bazel version to 8 (it does not work anymore after breaking changes in Bazel 9!). So, an interesting use case for this strategy is to experiment C++23 import std on GCC 15 with older versions of Bazel.
# BUILD (Bazel 8 only!)
load("//cc_module:defs.bzl", "cc_module", "cc_compiled_module", "cc_module_binary")
cc_module(
name = "hello",
src = "hello.cppm",
deps = [":std"],
)
cc_module_binary(
name = "hello_world",
srcs = [ "main.cpp" ],
deps = [
":hello",
":std"
]
)
# If building std.gcm or std.pcm manually
#cc_compiled_module(
# name = "std",
# cmi = "gcm.cache/std.gcm",
# module_name = "std", # for both clang and gcc!
#)
# generating std.gcm automatically with g++
genrule(
name = "build_std_gcm",
srcs = [],
outs = ["std_generated.gcm"],
cmd = "g++ -std=c++23 -fmodules -fPIC -fstack-protector" +
" -U_FORTIFY_SOURCE -Wall -Wunused-but-set-parameter" +
" -Wno-free-nonheap-object -fno-omit-frame-pointer" +
" -c -fmodules -fsearch-include-path bits/std.cc" +
" && cp gcm.cache/std.gcm $@",
)
cc_compiled_module(
name = "std",
cmi = ":build_std_gcm",
module_name = "std",
)
# MODULE.bazel
module(name = "hello_world_project")
bazel_dep(name = "rules_cpp23_modules", dev_dependency = True)
git_override(
module_name = "rules_cpp23_modules",
remote = "https://github.com/igormcoelho/rules_cpp23_modules.git",
commit = "3693b2ed64eb4dbc6b310fc860da79bd271da101" # 0.3.0
)
# .bazelrc
build --action_env=CC=/usr/bin/g++-15
build --action_env=CXX=/usr/bin/g++-15
build --repo_env=BAZEL_COMPILER=gcc
build --experimental_cpp_modules
build --cxxopt=-std=c++23
Note that this solution requires usage of different rules like cc_module, cc_module_binary (instead of cc_binary) and also a semi-automated process to build the CMI using g++. Beware that mixing compilation flags on the CMI and the final binary generated by Bazel can generate crazy and unexpected problems with GCC, so this is solution quite experimental and risky! Unfortunately, I could not yet find another working solution for g++ 15 using bazel and standard modules.
Final words
This article discussed possible solutions for building C++ projects with Bazel on both compilers Clang and GCC, by using modern CXX Modules and also standard library modules from C++23. Each proposed solution has its advantages and limitations, and unfortunately no experiment was made with MSVC compiler at this time. Support for CXX Modules will certainly grow within C++ community and the tools are still under evolution, so feel free to experiment with the ideas discussed here and also to leave comments if some novel and more stable strategies are available.
Good luck!
Igor Machado Coelho is Computing Researcher and Adjunct Professor at the Fluminense Federal University (Niterói, Rio de Janeiro, Brazil).
메타데이터
- post_id
- 95b449a8e881
- slug
- local-import-std-module-on-c-23-with-bazel-95b449a8e881
- url
- https://medium.com/@igormcoelho/local-import-std-module-on-c-23-with-bazel-95b449a8e881
- canonical_url
- https://medium.com/@igormcoelho/local-import-std-module-on-c-23-with-bazel-95b449a8e881
- author_url
- https://medium.com/@igormcoelho
- status
- ok
- fetched_at
- 2026-06-21 23:24:37