CMAKE_OPTIMIZE_DEPENDENCIES
(31-Oct-2021 追記)
CMake-3.19 から 変数 CMAKE_OPTIMIZE_DEPENDENCIES およびターゲットプロパティ OPTIMIZE_DEPENDENCIES が導入されました。
たいていの場合はこれを有効にしておけば、target_link_libraries(PRIVATE) でもライブラリ間の依存を断ち切ることができます。つまりたいていの用途において キーワードは PRIVATE 一択でじゅうぶん となります。
set(CMAKE_OPTIMIZE_DEPENDENCIES ON)
しかし PRIVATE による依存は add_custom_command() への依存を伝播してしまいます。そのような伝播をどうしても断ち切りたいのであれば INTERFACE を用いることになります。
以下の記事は CMAKE_OPTIMIZE_DEPENDENCIES 導入前に書かれたものです。
私は CMake を愛しています。どれくらい愛してるかっつーと、たとえ刺し違えてもトドメを刺したいくらいに愛してるぞっと。この世に CMake 好きなんて存在するのか?!
閑話休題。
target_link_libraries()
そのものの説明については https://cmake.org/cmake/help/v3.9/command/target_link_libraries.html をご覧ください。
前提--キレイな依存関係を書きましょう
target_link_libraries()
に指定するライブラリは "当該ターゲットで実際に用いられている" ものを指定します。何も考えずに target_link_libraries(foo <このプロジェクトで生成されるすべてのライブラリ>)
などとするのは愚かなことです。詳しくは共有ライブラリ(PRIVATE
)の節にて説明します。
本稿では例として以下のファイルで構成にて説明します。各ファイル lib*.c は対応するインタフェイスヘッダ lib*.h を持っているものとします。
lib1.c
-
lib2.c
#include "lib1.h"
-
lib3.c
#include "lib2.h"
-
foo.c
#include "lib1.h"
#include "lib3.h"
この例でのライブラリ依存関係は概ね以下のようになります。
-
lib1
は依存ナシ -
lib2
はlib1
に依存 -
lib3
はlib2
に依存 -
foo
はlib1
lib3
に依存
STATIC
-- スタティックライブラリ(*.a, *.lib)
INTERFACE
を用います。
# STATIC は省略可能
add_library(lib1 STATIC lib1.c)
add_library(lib2 STATIC lib2.c)
target_link_libraries(lib2 INTERFACE lib1)
add_library(lib3 STATIC lib3.c)
target_link_libraries(lib3 INTERFACE lib2)
add_executable(foo foo.c)
# キーワードは省略可
# あえて指定するなら PRIVATE もしくは PUBLIC
# ただし INTERFACE はここではダメ
target_link_libraries(foo lib1 lib3)
lib3.c
をコンパイルする際、キーワード INTERFACE
がない場合(PUBLIC
と同様の振る舞い)には lib3.o
コンパイル前に lib1
lib2
のビルド(コンパイルおよびアーカイヴ)完了を待つ依存が発生します。lib2.c
においても同様です。つまり、以下のビルドはすべて直列化されます。
lib1.o: lib1.c
lib1.a: lib1.o
lib2.o: lib2.c
lib2.a: lib2.o
lib3.o: lib3.c
lib3.a: lib3.o
foo.o: foo.c
-
foo: lib3.a lib2.a lib1.a
(最終実行形式)
INTERFACE
が指定されていれば、ターゲット lib3
のビルドは lib1
lib2
に影響されなくなります。もちろん lib2
も lib1
に影響されなくなります。つまり lib1
lib2
lib3
のビルドは並列化されます。
残念ながら foo.c
のコンパイルは(コンパイル自体が lib*.a
に依存していないにもかかわらず) lib*
の完了に依存してしまいます。(ただし CMake-3.9 -GNinja では改良されています。そのうち記事書きます。)
INTERFACE
は被依存ターゲットに対して推移的(transitive)に働きます。foo
に対しては lib3
が lib2
lib1
を引き連れて来るため、以下のように依存ライブラリが列挙されます。
-o foo lib3.a lib2.a lib1.a
依存関係が DAG である限りは被依存順に列挙されます。ただし CMake の実装の都合上、実際の列挙は -o foo lib1.a lib3.a lib2.a lib1.a
のように lib1
が重複してしまうかもしれません(さいきんは未確認)。
INTERFACE
のまとめ
- INTERFACE なライブラリどうしに依存は発生しないので並列度高まる。
- 非INTERFACE なターゲット(上記の例では
foo
)にはINTERFACE
ライブラリが連鎖してリンクされるのでリンク順で試行錯誤することはなくなる。
SHARED
-- 共有ライブラリ(*.so, *.dll)
できる限り PRIVATE
を用います。
add_library(lib1 SHARED lib1.c)
add_library(lib2 SHARED lib2.c)
target_link_libraries(lib2 PRIVATE lib1)
add_library(lib3 SHARED lib3.c)
target_link_libraries(lib3 PRIVATE lib2)
add_executable(foo foo.c)
# キーワードは省略可もしくはPRIVATE/PUBLIC
# foo は lib1 にも依存しているのでここで明示
target_link_libraries(foo lib1 lib3)
PRIVATE
は推移的に作用しません。上記の例では、lib3
ダイナミックリンク時に列挙されるライブラリは lib2
のみとなります。つまり lib2
は依存ライブラリである lib1
を伝播しません。同様に foo
には lib2
が列挙されず lib1
lib3
のみがリンクされます。
なお Win32 DLL の場合、依存 DLL の孫 DLL がエクスポートしているシンボルは解決されません。上記の例でいうと foo.exe
からは lib3.dll
が依存している lib2.dll
がエクスポートしているシンボルは不可視です(ニホンゴムズカシイ)。また、lib3
が依存している lib2
が依存している lib1
のシンボルは foo.exe
から不可視のため、lib1
への依存を明示しないと foo.exe
のリンクに失敗します(ニホンゴサイアクニムズカシイ)。この挙動は CMake の target_link_libraries(PRIVATE)
ととても相性がいいのです。
ニホンゴムズカシイのでコマンドライン例で書きますが、以下のリンクは成功します。
i686-w64-mingw32-gcc -o foo.exe foo.o lib1.dll lib3.dll
しかし lib1
を指定しなかった場合 lib1
lib2
が列挙されないため、以下のようになりリンクは失敗します。
i686-w64-mingw32-gcc -o foo.exe foo.o lib3.dll
私の経験上、Win32 DLL(例:mingw)で PRIVATE
を用いつつキレイに依存関係を書くことができれば、他のプラットフォームでもクリーンな依存関係となります。ライブラリ依存関係は Win32 DLL が最も厳しくなります。もし PRIVATE
でなく PUBLIC
を用いていると、動きはする(just works)ものの依存関係が壊れたときに発見が遅くなります。
余談(というワケではない)ですが、私が i686-w64-mingw32-gcc
を用いて LLVM を BUILD_SHARED_LIBS=ON
にてビルドしているのは、依存関係を常にクリーンに保つためという理由があります。
PRIVATE
のまとめ
- 自分の依存ライブラリを子に伝播しない。
- Win32 DLL(mingw) でこそ活用すべきだ!
- もちろん *.so でも有用。
- 依存関係をクリーンに保つために積極的に起用せよ!
ところで PUBLIC
の使い途は…?
要らない子。 以下、イラナイコの例です。
Michał の名誉のために言っておきますが、彼が好き好んで PUBLIC
を導入したのではなく、言うことを聞かない外部プロジェクトのために仕方なくこうしているのです。察しろ。
PUBLIC
のまとめ
- 要らない子
さいごに--キレイな依存関係を書きましょう
キレイな依存関係を保つことにより、多人数が参加するプロジェクトにおいて発生しがちな突発的なリンクエラーの発見・修復が速やかに行えるようになります。私が LLVM にて libdeps ポリスをやってるのは決して宗教的な理由ではなかったのです!
あ、マルチコア時代なんだからできるだけ依存関係を断ち切りまくってなるべくビルドは並列化しような! (Ryzen challenge には非参加でした…)