CMake: [deprecated] target_link_libraries(PUBLIC/PRIVATE/INTERFACE) の実践的な解説


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 は依存ナシ
  • lib2lib1 に依存
  • lib3lib2 に依存
  • foolib1 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 においても同様です。つまり、以下のビルドはすべて直列化されます

  1. lib1.o: lib1.c
  2. lib1.a: lib1.o
  3. lib2.o: lib2.c
  4. lib2.a: lib2.o
  5. lib3.o: lib3.c
  6. lib3.a: lib3.o
  7. foo.o: foo.c
  8. 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 に対しては lib3lib2 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 には非参加でした…)