前回、ヘッダオンリーライブラリをC++20のモジュールとして使用可能にする記事を書いた。謎にXのポストが伸びてしまったあたり、みんなモジュールについて「興味はなくもないけど使ってはいない」んだなぁとよく分かった。実際私もその一人だったのだから何も言えない。
さて、C++でのビルド環境といえば不幸にもCMakeがデファクトスタンダードとなってしまっているので、あの奇々怪々なコマンドに適応しなければモジュールを使えるようになったとは言えないだろう。2025年にもなってこの書き方の解説さえ満足に存在しない状況であったため、こちらも簡単にまとめておこうと思う。特にinstallやfind_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コマンドで別のプロジェクトから使用できるようにもしている。折角なのでモジュールもこれに対応させたい。さらに、本記事は前回に引き続きヘッダオンリーライブラリをモジュール化するという趣旨で書いている。よって、ここでもやはり#includeとimportのどちらからでも使用できるようにする。
まず、次のようなディレクトリ構造を想定しよう。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.hとmymodule.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
モジュールのソースコード.ixxやBMIファイル.gcmの配置については鉄板と言える場所を知らないので適当に設定した。また現行のビルドシステム的には、.gcmやlibMyModule.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年後には意味を成さなくなっていることを願いつつ、あるいは憂いつつ、せめて今この瞬間に必要な人の助けとなるよう。