Evvy's blog

C++ Modules in 2024: Part 1

import std;

auto main() -> int {
  std::println("Hello, {}!", "world");
}

You wish you could write this kind of code! Let me teach you how.

Getting the stuff

This assumes you’re using Linux of some kind. I’ll probably cover macOS and Windows some time later.

What we need to get this to work is the following:

I use Arch btw, so my package manager already takes care of the first two items. If this is not the case for you, you can download binary releases for both CMake and Clang. Or if you’re feeling adventurous you can always build them from source.

Anyway, let’s get our hands on the LLVM sources and build libc++:

; git clone https://github.com/llvm/llvm-project.git
; cd ./llvm-project
; mkdir build-libcxx
; cd ./build-libcxx

The options to build the necessary components are fairly straightforward. I specify -DLIBCXX_ABI_UNSTABLE=ON since we’re going to be linking libc++ statically anyway and it doesn’t really make sense to care about ABI compatibility. The option -DLIBCXX_ENABLE_STD_MODULES=ON was required until recently, but it doesn’t seem to be the case for the latest version from the main branch of LLVM.

; cmake \
    -DLLVM_ENABLE_RUNTIMES="libcxx;libcxxabi;libunwind" \
    -DCMAKE_BUILD_TYPE=Release \
    -DLIBCXX_ABI_UNSTABLE=ON \
    -DCMAKE_C_COMPILER=clang \
    -DCMAKE_CXX_COMPILER=clang++ \
    -GNinja ../runtimes
; ninja

Yay! Now we have our binary files in ./lib, all of our headers in ./include and our module files (which basically just include the headers and export the necessary names) in ./modules.

Using the stuff

Now let’s create our project in a different directory.

First we’ll need a CMake file that defines the target for our std module. It expects the variable LIBCXX_PATH to point to our LLVM build directory.

It sets the C++ standard, copies the module definition files into the project build directory, so that CMake won’t complain about out-of-tree builds. Then it defines a function add_module that basically just adds a target as a static library and adds the provided files to it as C++ module sources.

This function is used to define the std target. Linking to it will do the following:

# file: ./modules.cmake

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_EXTENSIONS OFF)

file(COPY "${LIBCXX_PATH}/modules/c++/v1/"
     DESTINATION "${PROJECT_BINARY_DIR}/c++std/")

function(add_module target)
  add_library(${target} STATIC)
  target_sources(${target} PUBLIC FILE_SET CXX_MODULES FILES ${ARGN})
endfunction()

add_module(std "${PROJECT_BINARY_DIR}/c++std/std.cppm")

target_include_directories(std PUBLIC "${LIBCXX_PATH}/include/c++/v1")
target_compile_options(std PRIVATE
    -Wno-reserved-module-identifier
    -Wno-reserved-user-defined-literal
)
target_compile_options(std PUBLIC -nostdlib++ -nostdinc++)
target_link_options(std PUBLIC -nostdlib++)
target_link_libraries(std PRIVATE
  "${LIBCXX_PATH}/lib/libc++.a"
  "${LIBCXX_PATH}/lib/libc++abi.a"
  "${LIBCXX_PATH}/lib/libunwind.a"
)
set_target_properties(std PROPERTIES OUTPUT_NAME "c++std")

In CMakeLists.txt we just need to include the above file and link our program with the std target.

# file: ./CMakeLists.txt

cmake_minimum_required(VERSION 3.28)
project(cpp_modules_test)

include(modules.cmake)

add_executable(main main.cpp)
target_link_libraries(main PRIVATE std)

Our program looks the same:

// file: ./main.cpp

import std;

auto main() -> int {
  std::println("Hello, {}!", "world");
}

Now to compile it. Don’t forget to pass LIBCXX_PATH to CMake:

; mkdir build
; cd build
; cmake \
    -DLIBCXX_PATH="/path/to/your/llvm-project/build-libcxx" \
    -DCMAKE_C_COMPILER=clang \
    -DCMAKE_CXX_COMPILER=clang++ \
    -GNinja ..
; ninja

And what do you know? It actually works:

; ./main
Hello, world!

Extending the stuff

For now we’ve only used one module: std. As much as it pains me to admit it, sometimes we need to write more complex programs than a “Hello, world!”. This means we need to learn how to create and use our own modules. Luckily, it’s not very difficult.

We didn’t need to define our module for now only because the main function is not supposed to live inside of any module unit. But now, let’s say we have a module greeter:

// file: greeter.cppm

export module greeter;

import std;

export void greet(std::string_view who) {
  std::println("Hello, {}!", who);
}

The file doesn’t actually have to have a .cppm extension, but it’s a convention so let’s stick to it. If your IDE freaks out and doesn’t recognize .cppm file as C++, I hereby grant you permission to just not care and use .cpp.

Also note that the name of the file has absolutely nothing to do with the name of the module. You are allowed to name your files completely differently from the modules that they provide. But as usual, it’s best to keep it reasonable.

Let’s modify main.cpp to use this module instead:

// file: main.cpp

import greeter;

auto main() -> int {
  greet("world");
}

And let’s define a new CMake target for the module by calling our trusty add_module function. We’ll also need to tell CMake that greeter depends on std and main depends on greeter:

# file: CMakeLists.txt

cmake_minimum_required(VERSION 3.28)
project(cpp_modules_test)

include(modules.cmake)

add_executable(main main.cpp)
add_module(greeter greeter.cppm)
target_link_libraries(greeter PRIVATE std)
target_link_libraries(main PRIVATE greeter)

And that’s it, if you build and run it now it should work:

; ninja && ./main
Hello, world!

Our custom modules can of course depend on other modules than just std. Just be careful not to create a circular dependency, since that won’t work.

Partitioning the stuff

If you’re using module partitions you should add all the partitions as one CMake target (so use one add_module call). Module partitions are a bit different in this regard to standalone module units. Here’s an example:

add_module(big_module
  big_module.cppm
  big_module_partition_a.cppm
  big_module_partition_b.cppm
  big_module_partition_c.cppm
)

The partitions should look like this:

// file: big_module_partition_a.cppm

export module big_module:partition_a;

// ...

And the main module file like this:

// file: big_module.cppm

export module big_module;

export import :partition_a;
export import :partition_b;
export import :partition_c;

I hope this helps someone. It took me some time to figure out all the details :)

Stay tuned for a dive into generating module definition files automatically from headers! Also maybe about getting all of this to work on macOS or Windows. Or maybe about something else entirely! See ya.

#C++