CMake 覚え書き (3): Usage Requirements とは
CMake 関係のドキュメントに度々登場する Usage Requirements について調べてみました。Usage Requirements とは何なのか?どのような場面で使うのか?などについてわかったことをまとめます (でも間違えている可能性もあるので、正しい情報は CMake 公式サイトなどから得るようにしてください)。なお、本稿での確認に用いた CMake のバージョンは 3.26.1 です。
目次
プロパティとは
はじめに「プロパティ」の話から入りましょう。CMake のオブジェクト (ターゲット、ディレクトリ、ソースファイルなど) はプロパティを持つことができます [1]。例えば、ターゲットが持つプロパティのひとつに TYPE プロパティがあります。これはその名のとおりターゲットのバイナリ・タイプを表し、STATIC_LIBRARY
、MODULE_LIBRARY
、SHARED_LIBRARY
、EXECUTABLE
などの値をとります。プロパティの値は get_property()
コマンドで読み出したり、(読み取り専用でなければ) set_property()
コマンドでセットしたりすることができます。どのようなプロパティがあるかはリファレンス・マニュアルを参照してください [2]。
Build Specification とは
さて、ターゲットが持つプロパティの中には、そのターゲットのソースファイルをコンパイルするのに使われるものがあります。それがINCLUDE_DIRECTORIES
、COMPILE_DEFINITIONS
およびCOMPILE_OPTIONS
です。それぞれ次のような意味を持ちます。
INCLUDE_DIRECTORIES
:-I
や-isystem
オプションを使ってコンパイラに渡すインクルード・ディレクトリのリスト。COMPILE_DEFINITIONS
:-D
や/D
オプションを使ってコンパイラに渡すマクロのリスト。COMPILE_OPTIONS
: コンパイラに渡すオプションのリスト。
この 3 つのプロパティを Build Specification と呼びます (公式ドキュメントの中でそのように明確に定義されているわけではないのですが、文脈からはそのように読み取れます [3]。そこで、本稿でもこの 3 つをまとめて Build Specification と呼ぶことにします)。
Build Specification は次のコマンドを使って設定することができます。
INCLUDE_DIRECTORIES
はtarget_include_directories()
コマンドCOMPILE_DEFINITIONS
はtarget_compile_definitions()
コマンドCOMPILE_OPTIONS
はtarget_compile_options()
コマンド
Usage Requirements とは
ところで、上で紹介した 3 つのプロパティには、それぞれ接頭辞INTERFACE_
を付けたプロパティ、すなわちINTERFACE_INCLUDE_DIRECTORIES
、INTERFACE_COMPILE_DEFINITIONS
、INTERFACE_COMPILE_OPTIONS
が存在し、それらを Usage Requirements と呼びます。日本語に訳せば「使用要件」でしょうか。ライブラリを使用する側に対して、そのライブラリを使用できるように適切にコンパイル・リンクするための要件を指定します。
(2024年5月23日 追記) 上記の 3 つのほか、INTERFACE_LINK_LIBRARIES
、INTERFACE_SOURCES
、INTERFACE_POSITION_INDEPENDENT_CODE
なども Usage Requirements に含まれるようです [5]。
Usage Requirements も Build Specification と同じコマンドを使って設定することができます。
INTERFACE_INCLUDE_DIRECTORIES
はtarget_include_directories()
コマンドINTERFACE_COMPILE_DEFINITIONS
はtarget_compile_definitions()
コマンドINTERFACE_COMPILE_OPTIONS
はtarget_compile_options()
コマンド
これらのコマンドでは、プロパティの値に対して「モード」(「スコープ」とも呼ばれる) を指定することができ、モードによってその値がINTERFACE_
プロパティにセットされるのか、非INTERFACE_
プロパティにセットされるのかが決まります。モードにはPUBLIC
、PRIVATE
、INTERFACE
のいずれかを指定することができます。例えば lib1 というターゲットに対してUSING_LIB1
というマクロを定義したいときはtarget_compile_definitions()
コマンドを使って次のように書きます。
target_compile_definitions(lib1 xxxx USING_LIB1)
xxxx
の部分がモードです。ここの指定の仕方によって、USING_LIB1
がセットされるプロパティが決まります。PRIVATE
を指定すると、USING_LIB1
は非INTERFACE_
プロパティであるCOMPILE_DEFINITIONS
にセットされ、前節でも触れたようにターゲット自身 (ここでは lib1) のコンパイルに使われます。INTERFACE
を指定すると、INTERFACE_
プロパティであるINTERFACE_COMPILE_DEFINITIONS
にセットされ、そのターゲットを使用する、別のターゲットのコンパイル・リンクに使用されます。PUBLIC
を指定すると、非INTERFACE_
プロパティとINTERFACE_
プロパティの両方にセットされます。
上記の例を表にまとめると、次のようになります。
モード | プロパティ | セットされる値 | プロパティを見る人 |
---|---|---|---|
PRIVATE | COMPILE_DEFINITIONS | USING_LIB1 | lib1 自身 |
INTERFACE | INTERFACE_COMPILE_DEFINITIONS | USING_LIB1 | lib1 の使用者 |
PUBLIC | 両方 | USING_LIB1 | 両者 |
Usage Requirements の使いみち
さて、Build Specification な (非INTERFACE_
な) プロパティ群は、プロパティの持ち主であるターゲット自身のコンパイルに使われる、と説明しました。これはまあわかります。では、Usage Requirements な(INTERFACE_
な) プロパティ群はいったいどのように使われるのでしょうか?ざっくりいうと Usage Requirements は、そのターゲットを使用する別のターゲットに対して、使用する際の要件を指定するために使われます。
例えば、CMake チュートリアルの Step3 の例 [4] では、MathFunctions ライブラリを使用するターゲット (Tutorial) が必要とするヘッダファイル MathFunctions.h の場所を、MathFunctions の使用要件として指定しています。
target_include_directories(MathFunctions
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
)
モードがINTERFACE
になっていることに注目してください。これによりライブラリの使用者である Tutorial が必要とする情報 (ここでは MathFunctions.h の場所) が MathFunctions のINTERFACE_INCLUDE_DIRECTORIES
プロパティにセットされます。
このようにしておくと、MathFunctions の使用者である Tutorial 自身は、必要なヘッダファイルがどこにあるのかを気にする必要はなく、単にtarget_link_libraries()
コマンドで MathFunctions ライブラリをリンクしさえすれば、あとは CMake がよきに計らってくれます (内部的には、MathFunctions のINTERFACE_INCLUDE_DIRECTORIES
プロパティの値を読み出して、Tutorial のINCLUDE_DIRECTORIES
プロパティにセットする、といったことが行われる [3])。
Usage Requirements の推移的伝播 (Transitive propagate)
Usage Requirements は推移的に伝播させることができます。例えば実行可能バイナリ X がライブラリ A を使用し、ライブラリ A がライブラリ B を使用するという状況で、B の Usage Requirements を X に伝播させることができる、ということです。伝播させるかどうかはtarget_link_libraries()
コマンドで制御します。具体例を CMake のリファレンス・マニュアルから見てみましょう。
add_library(archive archive.cpp)
target_compile_definitions(archive INTERFACE USING_ARCHIVE_LIB)
add_library(serialization serialization.cpp)
target_compile_definitions(serialization INTERFACE USING_SERIALIZATION_LIB)
add_library(archiveExtras extras.cpp)
target_link_libraries(archiveExtras PUBLIC archive)
target_link_libraries(archiveExtras PRIVATE serialization)
# archiveExtras is compiled with -DUSING_ARCHIVE_LIB
# and -DUSING_SERIALIZATION_LIB
add_executable(consumer consumer.cpp)
# consumer is compiled with -DUSING_ARCHIVE_LIB
target_link_libraries(consumer archiveExtras)
出典: CMake Reference Manuals - cmake-buildsystem(7)この例では、実行可能バイナリ consumer が archiveExtras ライブラリを使用し、archiveExtras ライブラリが archive、serialization という 2 つのライブラリを使用しています。archive、serialization には Usage Requirements としてそれぞれUSING_ARCHIVE_LIB
、USING_SERIALIZATION_LIB
というマクロを定義しています。8、9 行目でtarget_link_libraries()
コマンドを用いてこの 2 のライブラリを archiveExtras にリンクしていますが、このとき archive のほうはPUBLIC
、serialization のほうはPRIVATE
というキーワードを指定しています。
ここにPUBLIC
(またはINTERFACE
) を指定すると、archive の Usage Requirements が archiveExtras を使用する consumer にまで伝播します。一方で、serialization の Usage Requirements は、PRIVATE
の指定により consumer には伝播しません。よって、consumer はUSING_ARCHIVE_LIB
が定義され、かつUSING_SERIALIZATION_LIB
が未定義の状態でコンパイルされることになります。
ちなみに、INTERFACE
を指定すると、USING_*_LIB
は consumer には伝播します (定義された状態でコンパイルされる) が、archiveExtras のコンパイルにおいては未定義となります。
まとめると、実行可能バイナリ X、ライブラリ A、ライブラリ B の依存関係を次のように書いたとき、
target_link_libraries(A xxxx B)
target_link_libraries(X A)
xxxx
の部分は次のように指定します。
- A でのみ B を使用するなら
PRIVATE
(B の Usage Requirements を X に伝播させない) - X でも A でも B を使用するなら
PUBLIC
(B の Usage Requirements を X に伝播させる) - X でのみ B を使用するなら
INTERFACE
(B の Usage Requirements を X に伝播させる)
INTERFACE
がちょっとわかりづらいですが、「A の実装では B を必要としないが、A のヘッダファイルが B を必要としている」といったケースが該当します。
Usage Requirements の挙動を実験で確かめる
ここまで書いてきたことを実験で確かめてみます。備忘録のようなものですので、不要であれば読み飛ばしてください。
サンプルプログラムの概要
実験に用いるサンプルプログラムの概要は、次のとおりです。
- 実行可能バイナリ myapp と 2 つのライブラリ lib1、lib2 からなる。
- myapp は lib1 を使用する。lib1 は lib2 を使用する。
- lib2 に対してマクロ
USING_LIB2
を定義する。その際、モードをPUBLIC
、PRIVATE
、INTERFACE
と変化させて、lib2 自身や上位の lib1、myapp からどのように見えるか確認する。 - lib1 に対してマクロ
USING_LIB1
を定義する。その際、モードをPUBLIC
、PRIVATE
、INTERFACE
と変化させて、lib1 自身や上位の myapp からどのように見えるか確認する。
サンプルプログラムのソースコード
サンプルプログラムのソースコードは ~/myapp/src/ の中に作ることにします。ソースツリーは次のとおりです。
src
├── CMakeLists.txt
├── lib
│ ├── CMakeLists.txt
│ ├── lib1.c
│ ├── lib1.h
│ ├── lib2.c
│ └── lib2.h
└── myapp.c
ソースコードは大した量ではないので、ここにすべて掲載します。まず lib2.c と lib2.h から。func2()
という関数で、USING_LIB2
が定義されていたら 10 を、未定義だったら 0 を返します。
int func2()
{
#ifdef USING_LIB2
return 10;
#else
return 0;
#endif
}
#ifndef _LIB2_H_
#define _LIB2_H_
int func2();
#endif
次に lib1.c と lib1.h です。func1()
が次の値を返すように仕込みます。
- 一の位:
USING_LIB1
が定義されていたら1
、USING_LIB2
が定義されていたら2
、両方とも定義されていたら3
、両方とも未定義なら0
- 十の位:
func2()
の戻り値の十の位と同じ
#include "lib2.h"
int func1()
{
int ret = 0;
#ifdef USING_LIB1
ret += 1;
#endif
#ifdef USING_LIB2
ret += 2;
#endif
return ret + func2();
}
#ifndef _LIB1_H_
#define _LIB1_H_
int func1();
#endif
サンプルプログラムの本体 myapp.c です。USING_LIB1
、USING_LIB2
が定義されていたらその旨を表示し、func1()
の戻り値を表示して終わります。
#include <stdio.h>
#include "lib1.h"
int main(int argc, char* argv[])
{
#ifdef USING_LIB1
printf("using lib1.\n");
#endif
#ifdef USING_LIB2
printf("using lib2.\n");
#endif
printf("func1 = %d\n", func1());
return 0;
}
src ディレクトリ直下の CMakeLists.txt です。myapp に lib1 をリンクするように書いています。
cmake_minimum_required(VERSION 3.10)
project(myapp)
add_subdirectory(lib)
add_executable(myapp myapp.c)
target_link_libraries(myapp PRIVATE lib1)
target_include_directories(myapp PRIVATE "${PROJECT_SOURCE_DIR}/lib")
src/lib 直下の CMakeLists.txt です。
add_library(lib1 lib1.c)
target_compile_definitions(lib1 PUBLIC USING_LIB1)
#target_compile_definitions(lib1 PRIVATE USING_LIB1)
#target_compile_definitions(lib1 INTERFACE USING_LIB1)
add_library(lib2 lib2.c)
target_compile_definitions(lib2 PUBLIC USING_LIB2)
#target_compile_definitions(lib2 PRIVATE USING_LIB2)
#target_compile_definitions(lib2 INTERFACE USING_LIB2)
target_link_libraries(lib1 PUBLIC lib2)
#target_link_libraries(lib1 PRIVATE lib2)
#target_link_libraries(lib1 INTERFACE lib2)
# for debug
get_target_property(LIB1_COMPILE_DEF lib1 COMPILE_DEFINITIONS)
get_target_property(LIB1_IF_COMPILE_DEF lib1 INTERFACE_COMPILE_DEFINITIONS)
get_target_property(LIB2_COMPILE_DEF lib2 COMPILE_DEFINITIONS)
get_target_property(LIB2_IF_COMPILE_DEF lib2 INTERFACE_COMPILE_DEFINITIONS)
message("lib1: COMPILE_DEFINITIONS = ${LIB1_COMPILE_DEF}")
message("lib1: INTERFACE_COMPILE_DEFINITIONS = ${LIB1_IF_COMPILE_DEF}")
message("lib2: COMPILE_DEFINITIONS = ${LIB2_COMPILE_DEF}")
message("lib2: INTERFACE_COMPILE_DEFINITIONS = ${LIB2_IF_COMPILE_DEF}")
2 ~ 4 行目、7 ~ 9 行目で lib1 、lib2 それぞれの Usage Requirements としてマクロを定義しています。PUBLIC
、PRIVATE
、INTERFACE
のいずれかの行を有効にし、それ以外の行はコメントアウトします。
11 ~ 13 行目で lib1 に lib2 をリンクしています。ここも Usage Requirements の伝播を確認するためにPUBLIC
、PRIVATE
、INTERFACE
のいずれかの行を有効にし、それ以外の行をコメントアウトします。
16 ~ 23 行目はデバッグ用です。lib1、lib2 それぞれのCOMPILE_DEFINITIONS
(非INTERFACE_
プロパティ) とINTERFACE_COMPILE_DEFINITIONS
(INTERFACE_
プロパティ) を表示します。
サンプルプログラムの紹介は以上です。ここで、このプログラムがどういう出力をするのかを考えてみましょう。例えば上の src/lib/CMakeLists.txt のように 2、7、11 行目を有効にした場合 (つまりすべてPUBLIC
) の場合、次のようになると期待されます。
- lib2 の
COMPILE_DEFINITIONS
とINTERFACE_COMPILE_DEFINITIONS
はともにUSING_LIB2
- lib1 の
COMPILE_DEFINITIONS
はUSING_LIB1
とUSING_LIB2
- lib1 の
INTERFACE_COMPILE_DEFINITIONS
はUSING_LIB1
(またはUSING_LIB1
とUSING_LIB2
?) - myapp の
COMPILE_DEFINITIONS
はUSING_LIB1
とUSING_LIB2
func2()
の戻り値は10
func1()
の戻り値は13
- プログラムの出力:
using lib1. using lib2. func1 = 13
逆に、すべてPRIVATE
にした場合は、次のようになると期待されます。
- lib2 の
COMPILE_DEFINITIONS
はUSING_LIB2
- lib2 の
INTERFACE_COMPILE_DEFINITIONS
は値なし - lib1 の
COMPILE_DEFINITIONS
はUSING_LIB1
- lib1 の
INTERFACE_COMPILE_DEFINITIONS
は値なし - myapp の
COMPILE_DEFINITIONS
は値なし func2()
の戻り値は10
func1()
の戻り値は11
- プログラムの出力:
func1 = 11
サンプルプログラムのビルドと実行
ビルド方法、実行方法は前回と同じです。
% cd ~/myapp/
% mkdir build
% cmake -S src -B build
% cmake --build build
% ./build/myapp
実験結果
実験の結果は、次の表のとおりです (表がでかくてすみません、適宜拡大してください)。
表の見方:
- 1、2 列目:
USING_LIB1
マクロ、USING_LIB2
マクロのモード - 3、4 列目: lib1 の
COMPILE_DEFINITIONS
、INTERFACE_COMPILE_DEFINITIONS
にセットされた値 - 5、6 列目: lib2 の
COMPILE_DEFINITIONS
、INTERFACE_COMPILE_DEFINITIONS
にセットされた値 - 7 列目: lib1 に lib2 をリンクする際の Usage Requirements の伝播設定
- 8、9 列目: "using lib1"、"using lib2" の表示有無 (表示されたら〇、されなかったら×と表記)
- 10 列目:
func1()
の戻り値
表の見方の例をひとつ取り上げると、12 行目 (1 列目がPRIVATE
、2 列目がPUBLIC
、7 列目がINTERFACE
になっている行) では次のようになります。
USING_LIB2
のモードがPUBLIC
なので、lib2 のCOMPILE_DEFINITIONS
、INTERFACE_COMPILE_DEFINITIONS
ともにUSING_LIB2
がセットされるUSING_LIB1
のモードがPRIVATE
なので、lib1 のCOMPILE_DEFINITIONS
にはUSING_LIB1
がセットされるが、INTERFACE_COMPILE_DEFINITIONS
にはセットされない- lib2 の Usage Requirements の伝播設定を
INTERFACE
にしたので、myapp から見るとUSING_LIB2
が定義されている。USING_LIB1
はモードがPRIVATE
なので、myapp から見ると未定義となる - 上記 3 項目より、
func2()
の戻り値は10
、func1()
の戻り値は11
、プログラムの出力は次のとおりusing lib2. func1 = 11
これを踏まえて結果を眺めてみると、おおむね前節までに書いた内容と一致する結果になったのではないかと思います。
ひとつ不明なのは、USING_LIB2
のモードがINTERFACE
のとき、上のほうで少し触れた「内部的には、MathFunctions のINTERFACE_INCLUDE_DIRECTORIES
プロパティの値を読み出して Tutorial のINCLUDE_DIRECTORIES
プロパティにセットする」という動作から考えると、lib1 のCOMPILE_DEFINITIONS
にUSING_LIB2
が追加されてもいいように思いますが、結果はそうなっていません。私がドキュメントの解釈を間違えているのか、実験方法に問題があるのかわかりませんが、ここでは深追いしないことにします。何かわかったら追記します。
それから、3 行目 (1、2 列目がPUBLIC
、7 列目がINTERFACE
になっている行) は注記が必要かもしれません。func1()
の戻り値は11
ではなく13
になりそうな気がしませんか?USING_LIB2
のモードがPUBLIC
なので、lib1 でもUSING_LIB2
が定義されているのではないか?と。これは、lib2 の Usage Requirements の伝播設定がINTERFACE
になっているので、myapp にはUSING_LIB2
が伝播するのに対して、lib1 では未定義になるためだと考えられます。
まとめ
Usage Requirements について次のことがわかりました。
- Usage Requirements とは「あるターゲットが、そのターゲットを適切に使用するために指定する要件」である
- より具体的には、そのターゲットを使用するために必要なインクルード・ディレクトリやマクロ定義のことである
- Usage Requirements は
target_compile_definitions()
などのコマンドを用いて指定する - Usage Requirements は
target_link_libraries()
コマンドを用いて上位のターゲットに伝播させることができる
次回は、Pico SDK の CMakeLists.txt では頻出の「インターフェース・ライブラリ」について調べようと思っています。
参考資料
[1] | Mastering CMake - Key Concepts https://cmake.org/cmake/help/book/mastering-cmake/chapter/Key%20Concepts.html |
[2] | CMake Reference Manuals - cmake-properties(7) https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html#manual:cmake-properties(7) |
[3] | CMake Reference Manuals - Build Specification and Usage Requirements https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#build-specification-and-usage-requirements |
[4] | CMake Tutorial - Step 3: Adding Usage Requirements for a Library https://cmake.org/cmake/help/latest/guide/tutorial/Adding%20Usage%20Requirements%20for%20a%20Library.html |
[5] | CMake Reference Manuals - Interface Libraries https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#interface-libraries |