公開日: 2024年5月7日

CMake 覚え書き (3): Usage Requirements とは

CMake 関係のドキュメントに度々登場する Usage Requirements について調べてみました。Usage Requirements とは何なのか?どのような場面で使うのか?などについてわかったことをまとめます (でも間違えている可能性もあるので、正しい情報は CMake 公式サイトなどから得るようにしてください)。なお、本稿での確認に用いた CMake のバージョンは 3.26.1 です。

プロパティとは

はじめに「プロパティ」の話から入りましょう。CMake のオブジェクト (ターゲット、ディレクトリ、ソースファイルなど) はプロパティを持つことができます [1]。例えば、ターゲットが持つプロパティのひとつに TYPE プロパティがあります。これはその名のとおりターゲットのバイナリ・タイプを表し、STATIC_LIBRARYMODULE_LIBRARYSHARED_LIBRARYEXECUTABLE などの値をとります。プロパティの値は get_property() コマンドで読み出したり、(読み取り専用でなければ) set_property() コマンドでセットしたりすることができます。どのようなプロパティがあるかはリファレンス・マニュアルを参照してください [2]

Build Specification とは

さて、ターゲットが持つプロパティの中には、そのターゲットのソースファイルをコンパイルするのに使われるものがあります。それがINCLUDE_DIRECTORIESCOMPILE_DEFINITIONSおよびCOMPILE_OPTIONSです。それぞれ次のような意味を持ちます。

  • INCLUDE_DIRECTORIES: -I-isystemオプションを使ってコンパイラに渡すインクルード・ディレクトリのリスト。
  • COMPILE_DEFINITIONS: -D/Dオプションを使ってコンパイラに渡すマクロのリスト。
  • COMPILE_OPTIONS: コンパイラに渡すオプションのリスト。

この 3 つのプロパティを Build Specification と呼びます (公式ドキュメントの中でそのように明確に定義されているわけではないのですが、文脈からはそのように読み取れます [3]。そこで、本稿でもこの 3 つをまとめて Build Specification と呼ぶことにします)。

Build Specification は次のコマンドを使って設定することができます。

  • INCLUDE_DIRECTORIEStarget_include_directories()コマンド
  • COMPILE_DEFINITIONStarget_compile_definitions()コマンド
  • COMPILE_OPTIONStarget_compile_options()コマンド

Usage Requirements とは

ところで、上で紹介した 3 つのプロパティには、それぞれ接頭辞INTERFACE_を付けたプロパティ、すなわちINTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONSINTERFACE_COMPILE_OPTIONSが存在し、それらを Usage Requirements と呼びます。日本語に訳せば「使用要件」でしょうか。ライブラリを使用する側に対して、そのライブラリを使用できるように適切にコンパイル・リンクするための要件を指定します。

Usage Requirements も Build Specification と同じコマンドを使って設定することができます。

  • INTERFACE_INCLUDE_DIRECTORIEStarget_include_directories()コマンド
  • INTERFACE_COMPILE_DEFINITIONStarget_compile_definitions()コマンド
  • INTERFACE_COMPILE_OPTIONStarget_compile_options()コマンド

これらのコマンドでは、プロパティの値に対して「モード」(「スコープ」とも呼ばれる) を指定することができ、モードによってその値がINTERFACE_プロパティにセットされるのか、非INTERFACE_プロパティにセットされるのかが決まります。モードにはPUBLICPRIVATEINTERFACEのいずれかを指定することができます。例えば 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 自身

INTERFACEINTERFACE_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 のリファレンス・マニュアルから見てみましょう。

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_LIBUSING_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を定義する。その際、モードをPUBLICPRIVATEINTERFACEと変化させて、lib2 自身や上位の lib1、myapp からどのように見えるか確認する。
  • lib1 に対してマクロUSING_LIB1を定義する。その際、モードをPUBLICPRIVATEINTERFACEと変化させて、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 を返します。

src/lib/lib2.c
int func2()
{
#ifdef USING_LIB2
    return 10;
#else
    return 0;
#endif
}
src/lib/lib2.h
#ifndef _LIB2_H_
#define _LIB2_H_

int func2();

#endif

次に lib1.c と lib1.h です。func1()が次の値を返すように仕込みます。

  • 一の位: USING_LIB1が定義されていたら1USING_LIB2が定義されていたら2、両方とも定義されていたら3、両方とも未定義なら0
  • 十の位: func2()の戻り値の十の位と同じ
src/lib/lib1.c
#include "lib2.h"

int func1()
{
    int ret = 0;

#ifdef USING_LIB1
    ret += 1;
#endif

#ifdef USING_LIB2
    ret += 2;
#endif

    return ret + func2();
}
src/lib/lib1.h
#ifndef _LIB1_H_
#define _LIB1_H_

int func1();

#endif

サンプルプログラムの本体 myapp.c です。USING_LIB1USING_LIB2が定義されていたらその旨を表示し、func1()の戻り値を表示して終わります。

src/myapp.c
#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 をリンクするように書いています。

src/CMakeLists.txt
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 です。

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 としてマクロを定義しています。PUBLICPRIVATEINTERFACEのいずれかの行を有効にし、それ以外の行はコメントアウトします。

11 ~ 13 行目で lib1 に lib2 をリンクしています。ここも Usage Requirements の伝播を確認するためにPUBLICPRIVATEINTERFACEのいずれかの行を有効にし、それ以外の行をコメントアウトします。

16 ~ 23 行目はデバッグ用です。lib1、lib2 それぞれのCOMPILE_DEFINITIONS(非INTERFACE_プロパティ) とINTERFACE_COMPILE_DEFINITIONS(INTERFACE_プロパティ) を表示します。

サンプルプログラムの紹介は以上です。ここで、このプログラムがどういう出力をするのかを考えてみましょう。例えば上の src/lib/CMakeLists.txt のように 2、7、11 行目を有効にした場合 (つまりすべてPUBLIC) の場合、次のようになると期待されます。

  • lib2 のCOMPILE_DEFINITIONSINTERFACE_COMPILE_DEFINITIONSはともにUSING_LIB2
  • lib1 のCOMPILE_DEFINITIONSUSING_LIB1USING_LIB2
  • lib1 のINTERFACE_COMPILE_DEFINITIONSUSING_LIB1(またはUSING_LIB1USING_LIB2?)
  • myapp のCOMPILE_DEFINITIONSUSING_LIB1USING_LIB2
  • func2()の戻り値は10
  • func1()の戻り値は13
  • プログラムの出力:
    using lib1.
    using lib2.
    func1 = 13
    

逆に、すべてPRIVATEにした場合は、次のようになると期待されます。

  • lib2 のCOMPILE_DEFINITIONSUSING_LIB2
  • lib2 のINTERFACE_COMPILE_DEFINITIONSは値なし
  • lib1 のCOMPILE_DEFINITIONSUSING_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

実験結果

実験の結果は、次の表のとおりです (表がでかくてすみません、適宜拡大してください)。

Usage Requirements の実験結果

表の見方:

  • 1、2 列目: USING_LIB1マクロ、USING_LIB2マクロのモード
  • 3、4 列目: lib1 のCOMPILE_DEFINITIONSINTERFACE_COMPILE_DEFINITIONSにセットされた値
  • 5、6 列目: lib2 のCOMPILE_DEFINITIONSINTERFACE_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_DEFINITIONSINTERFACE_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()の戻り値は10func1()の戻り値は11、プログラムの出力は次のとおり
    using lib2.
    func1 = 11
    

これを踏まえて結果を眺めてみると、おおむね前節までに書いた内容と一致する結果になったのではないかと思います。

ひとつ不明なのは、USING_LIB2のモードがINTERFACEのとき、上のほうで少し触れた「内部的には、MathFunctions のINTERFACE_INCLUDE_DIRECTORIESプロパティの値を読み出して Tutorial のINCLUDE_DIRECTORIESプロパティにセットする」という動作から考えると、lib1 のCOMPILE_DEFINITIONSUSING_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 では頻出の「インターフェース・ライブラリ」について調べようと思っています。

参考資料

Raspberry Pi Pico 実験室
前の記事 | 目次 | 次の記事 (工事中)