[C++][CMake]モジュールを使用するCMakeプロジェクトの作成。

前回、ヘッダオンリーライブラリをC++20のモジュールとして使用可能にする記事を書いた。謎にXのポストが伸びてしまったあたり、みんなモジュールについて「興味はなくもないけど使ってはいない」んだなぁとよく分かった。実際私もその一人だったのだから何も言えない。

さて、C++でのビルド環境といえば不幸にもCMakeがデファクトスタンダードとなってしまっているので、あの奇々怪々なコマンドに適応しなければモジュールを使えるようになったとは言えないだろう。2025年にもなってこの書き方の解説さえ満足に存在しない状況であったため、こちらも簡単にまとめておこうと思う。特にinstallfind_packageの周りがモジュールではどうなっているのか、そのあたりを掘り下げてみる。

なお、CMakeは3.28でモジュールに本格対応したのだが、残念ながらここで解説する使い方には3.29以上が必要だと思われる。今回紹介する方法は、私の手元の環境だと、CMake 3.28ではエラーが出て失敗し、CMake 3.31では成功した。ネット上で調べた限りCMake 3.29でエラーが解消されたという報告があったので、少なくともそれ以上を勧めておく。
(本当はこの記事は前回の記事と統合して上げるつもりだったのだが、このCMakeのバグに引っかかり原因を理解できず、やむなくCMake部分だけ後回しにしたものだったりする。)

1. 自身のCMakeプロジェクトにモジュールのソースコードが存在する場合

もしモジュールのソースコードが全てプロジェクト内に存在するのなら、次のように書けば良い。今回、mymodule.ixxをモジュールのソースコードとして使用し、これをMyAppにリンクする形で使用している。 target_sources(... FILE_SET modules TYPE CXX_MODULES ...)が肝で、他は通常のライブラリを使用する場合と大差ない。

add_library(MyModule)

target_compile_features(MyModule PRIVATE cxx_std_20)#PUBLICでもいいだろうとは思うが。

target_sources(
    MyModule PUBLIC
    FILE_SET modules
    TYPE CXX_MODULES
    FILES "mymodule.ixx")

add_executable(MyApp "myapp.cpp")

target_compile_features(MyApp PRIVATE cxx_std_20)

target_link_libraries(MyApp PRIVATE MyModule)

2. モジュールを配布する/配布されているモジュールを使用する場合

もちろん、1.のような状況ばかりではないだろう。モジュールを配布したり、配布されているモジュールを導入したい場合だってあるはずだ。例えば私は普段、自作ライブラリをinstallコマンドで特定の場所に配置させるようにしているし、find_packageコマンドで別のプロジェクトから使用できるようにもしている。折角なのでモジュールもこれに対応させたい。さらに、本記事は前回に引き続きヘッダオンリーライブラリをモジュール化するという趣旨で書いている。よって、ここでもやはり#includeimportのどちらからでも使用できるようにする。

まず、次のようなディレクトリ構造を想定しよう。MyLib/MyLibはライブラリのヘッダファイルmylib.hがあり、MyLib/MyModuleにはそのヘッダをモジュール化したmymodule.ixxが入っている。

MyLib/
    MyLib/
        CMakeLists.txt
        mylib.h
    MyModule/
        CMakeLists.txt
        mymodule.ixx
    CMakeLists.txt

最終目標は、find_package(MyLib REQUIRED)としてこれらのライブラリを発見し、かつtarget_link_libraries(... MyLib::MyLib)とすればヘッダオンリーライブラリとして、target_link_libraries(... MyLib::MyModule)とすればモジュールとして使用できることだ。このようにすれば、単一のパッケージで#include版とimport版を同時に配布することができる。mylib.hmymodule.ixxの中身は前回の記事を参照してほしい。

project(MyLibProject)

#MyModuleはデフォルトでは無効。
#USE_MODULE=ONというオプションを与えた場合のみ、MyModuleを有効にする。
option(USE_MODULE "use MyLib module" OFF)

add_subdirectory ("MyLib")
if(USE_MODULE)
    add_subdirectory ("MyModule")
endif()

install(
    EXPORT MyLibConfig
    DESTINATION cmake
    NAMESPACE MyLib::)
#MyLib/CMakeLists.txt
project(MyLib)

add_library(MyLib INTERFACE)
add_library(MyLib::MyLib ALIAS MyLib)

target_include_directories(MyLib
    INTERFACE
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..>
    $<INSTALL_INTERFACE:include/MyLib>)
set_target_properties(
    MyLib
    PROPERTIES
        PUBLIC_HEADER "mylib.h")
install(
    TARGETS MyLib
    EXPORT MyLibConfig
    INCLUDES DESTINATION include/MyLib
    PUBLIC_HEADER DESTINATION include/MyLib/MyLib)
#MyModule/CMakeLists.txt
project(MyModule)

add_library(MyModule)

target_compile_features(MyModule PRIVATE cxx_std_20)

target_link_libraries(MyModule PUBLIC MyLib)

target_sources(
    MyModule PUBLIC
    FILE_SET modules
    TYPE CXX_MODULES
    FILES "mymodule.ixx")

add_library(MyLib::MyModule ALIAS MyModule)

install(
    TARGETS MyModule
    EXPORT MyLibConfig
    RUNTIME DESTINATION bin/${CMAKE_BUILD_TYPE}
    LIBRARY DESTINATION lib/${CMAKE_BUILD_TYPE}
    ARCHIVE DESTINATION lib/${CMAKE_BUILD_TYPE}
    CXX_MODULES_BMI DESTINATION module/${CMAKE_BUILD_TYPE}
    FILE_SET modules DESTINATION include/MyLib/MyModule)

C++17以下であったりコンパイラが対応できていなかったりなどの理由でモジュールをビルドできない環境にも対応するため、-USE_MODULE=ONというオプションを与えた場合のみMyModuleがビルド、インストールされるようにしている。特に重要なのはinstall(... FILE_SET modules DESTINATION include/MyLib/MyModule)の部分である。現状、外部からモジュールを使用するためには.ixxなどのモジュールのソースコードが不可欠で、これをインストールして他のプロジェクトから参照可能にしておく必要がある。

このパッケージをインストールした場合、例えばGCCでDebugビルドした場合なら、次のようにファイルが配置される。

install_dst/
    cmake/
        MyLibConfig.cmake
        MyLibConfig-debug.cmake
    include/
        MyLib/
            MyLib/
                mylib.h
            MyModule/
                mymodule.ixx
    lib/
        Debug/
            libMyModule.a
    module/
        Debug/
            mymodule.gcm

モジュールのソースコード.ixxBMIファイル.gcmの配置については鉄板と言える場所を知らないので適当に設定した。また現行のビルドシステム的には、.gcmlibMyModule.aは使用されているのか謎である。特にBMIに関しては、コンパイラのバージョンなどで完全に統一的なファイルを出力することが困難であるため、実効的には用いられておらず、ユーザーが都度.ixxから生成する必要がある、とのことである1。モジュールの魅力が半減してしまう気がするが……。

こうしてインストールしたライブラリを別のCMakeプロジェクトから使用する場合は以下のように書く。

find_package(MyLib REQUIRED)

add_executable(MyApp "myapp.cpp")

target_compile_features(MyApp PRIVATE cxx_std_20)

#ここでMyLib::MyModuleを指定すれば`import`で、
#MyLib::MyLibを指定すれば`#include`で使用可能になる。
target_link_libraries(MyApp PRIVATE MyLib::MyModule)

特にモジュールであることを意識する必要はない。なおCMake 3.28を使っている場合、MyAppからmymodule.ixxコンパイルする最中にコンパイルエラーとなった。

余談

そろそろCMakeより圧倒的に使いやすいビルドシステム、パッケージマネージャに出てきてほしい。というより、それが実現しなければRustに立場を奪われてC++が滅び去るだけだと思う。そう思わずにはいられないくらい、CMakeのコマンドは理解しがたい。
本記事の内容が5年後には意味を成さなくなっていることを願いつつ、あるいは憂いつつ、せめて今この瞬間に必要な人の助けとなるよう。