公開日: 2024年5月24日

CMake 覚え書き (4): Pico SDK におけるインターフェース・ライブラリ

Raspberry Pi Pico SDK (以下 Pico SDK) は、静的ライブラリも動的ライブラリも生成しません。にもかかわらず、CMake のadd_library()コマンドを駆使して多数のライブラリらしきものを作っています。それらは (私が調べたかぎりすべて)「インターフェース・ライブラリ」と呼ばれるものです。そこで、インターフェース・ライブラリとは何なのか?Pico SDK においてどのように活用されているのか?について調べてみました。

なお、CMake はバージョン 3.26.1、Pico SDK はコミットハッシュ 6a7db34 時点のリポジトリで確認しています。

Pico SDK におけるライブラリ

はじめに、Pico SDK において (実体はさておき) ライブラリとして取り扱われているソフトウェア・モジュール (*) の例を挙げておきます。

(*)「モジュール」と呼ぶのが正しいのかどうかわかりませんが、静的ライブラリや動的ライブラリなどの「ライブラリ」と区別するために、そのように呼ぶことにします。

pico_stdlib

pico_stdlib は Pico SDK の最上位にあるモジュールです。この下に他のすべてのモジュールがぶら下がっています。Pico SDK を利用する実行可能バイナリ (例えば pico-examples の blink) は次のようにして Pico SDK をリンクします。

pico-examples/blink/CMakeLists.txt (抜粋)
target_link_libraries(blink pico_stdlib)

冒頭でも触れたように、pico_stdlib は静的ライブラリでも動的ライブラリでもありません。pico_stdlib に属するすべてのソースコードは、それぞれコンパイルされてそれぞれ blink にリンクされます。

ちなみにこの例ではtarget_link_libraries()コマンドにINTERFACEキーワードを指定していませんが、pico_stdlib の Usage Requirements は blink に適用されます [1]。Usage Requirements については前回の記事を参照してください。

pico_standard_link

pico_standard_link には C ランタイムライブラリや、C++ の new / delete 演算子の定義などが含まれます。また、実行可能バイナリのリンク時に使用するリンカスクリプトも、pico_standard_link の Usage Requirements として指定されます (そして、前回の記事で触れた Usage Requirements の伝播というしくみによって blink まで伝播します)。

インターフェース・ライブラリとは

インターフェース・ライブラリは、静的 / 動的ライブラリと同様add_library()コマンドで作成することができます。リファレンス・マニュアル [2] によれば、インターフェース・ライブラリには次のような特徴があります (これだけではわかりづらいので、次の次の節でインターフェース・ライブラリの例を挙げます)。

  • ソースファイルを持たない (*1 *2)
  • ビルドしても何も生成されない。よってLOCATIONプロパティ (*3) を持たない
  • 次の Usage Requirements を持たせることができる
    • INTERFACE_INCLUDE_DIRECTORIES
    • INTERFACE_COMPILE_DEFINITIONS
    • INTERFACE_COMPILE_OPTIONS
    • INTERFACE_LINK_LIBRARIES
    • INTERFACE_SOURCES
    • INTERFACE_POSITION_INDEPEND_CODE
  • 次のコマンドでは、INTERFACEモード (前回の記事を参照) のみ使用することができる
    • target_include_directories()
    • target_compile_definitions()
    • target_compile_options()
    • target_sources()
    • target_link_libraries()
(*1)target_sources()コマンド [3] を用いれば、インターフェース・ライブラリにもソースファイルを持たせることは可能です。
(*2)ちなみに、非インターフェース・ライブラリをソースファイルなしで作成しようとすると、エラー「No SOURCES given to target: <ターゲット名>」になってライブラリを作成することができません。
(*3)古い CMake との互換性のために残されているプロパティのようで、あまり気にしなくていいと思います。詳細はリファレンス・マニュアル [4] を参照してください。

インターフェース・ライブラリも通常のライブラリと同様に、target_link_libraries()コマンドを用いて「リンクする」ことができます。ただし、インターフェース・ライブラリはソースファイルを持たないので、一般的な意味でのリンクとは異なるように思います。「ターゲット間の依存関係を定義する」といったほうが適切かもしれません。ですが、文脈でだいたいわかると思うので、本稿では特に使い分けないことにします。

インターフェース・ライブラリの使いみち

ソースファイルを持たないライブラリが何の役に立つのかという例が、リファレンス・マニュアルに 2 つ記載されています [2]

例 1: ヘッダファイルのみからなるライブラリ

最初の例は、ヘッダファイルのみからなるライブラリです。この例のように、インターフェース・ライブラリとして Eigen を作成し、target_sources()コマンドでヘッダファイルを登録すると、そのインクルード・ディレクトリが自動的に Eigen の Usage Requirements に追加されます。

私が調べたかぎり、Pico SDK ではこの例のような使い方は見受けられませんでした。本稿でも以降は特に扱いません。

例 2: Usage Requirements のカプセル化

2 つめの例は、Usage Requirements のカプセル化です。モジュールごとにインターフェース・ライブラリを作成し、target_compile_definitions()target_compile_options()などのコマンドを使って、そのモジュールに関する Usage Requirements を、作成したインターフェース・ライブラリに集約します。

実行可能ターゲットのほうでは、作成したインターフェース・ライブラリをtarget_link_libraries()コマンドでリンクするだけで、自身のビルドに必要な情報 (Build Specification) を得ることができます (リンクしたインターフェース・ライブラリの Usage Requirements が実行可能ターゲットに伝播する)。

このように、モジュールに関する煩雑な Usage Requirements をインターフェース・ライブラリに閉じ込めておけば、あとはモジュール間の依存関係を定義するだけでよいので、プロジェクトの構成を簡潔に記述することができます。

Pico SDK でのインターフェース・ライブラリの用途は、おそらくすべてこちらのケースです。

サンプルプログラムで実験

インターフェース・ライブラリを使うとどんな Makefile が生成されるのか、例によってサンプルプログラムを作って実験してみます。

サンプルプログラムの概要

実験に使うサンプルプログラムは、Pico SDK の超簡易版といった感じの構成になっています。図で表すと、図 1 のようになります。

図 1
図 1

実行可能ターゲットである myapp が lib1 を、lib1 が lib2 を利用します。lib1 と lib2 はインターフェース・ライブラリです。lib1、lib2 に各ライブラリの Usage Requirements (マクロ、コンパイル・オプション、リンク・オプションなど) をカプセル化します。注意点として、Usage Requirements のうちインクルード・ディレクトリについては lib1_headers と lib2_headers にカプセル化してそれぞれ lib1、lib2 にtarget_link_libraries()コマンドでリンクします。lib1 以下のモジュールはすべてインターフェース・ライブラリになっています。

また、lib1 と lib2 には、次のような関係があります。

  • lib1 は lib1_headers に依存する
  • lib1_headers は lib2_headers に依存する
  • lib1 は lib2 に依存する
  • target_link_libraries()コマンドの スコープはすべてINTERFACEモード

このあたりは Pico SDK のpico_mirrored_target_link_libraries()関数の中身を展開した形になっていますので、そちらを参照してください。

図中の、点線から引っ張った四角い枠は、各インターフェース・ライブラリの Usage Requirements を表しています。

  • D: マクロ
  • I: インクルード・ディレクトリ
  • F: コンパイル・オプション
  • L: リンク・オプション
  • S: ソースファイル

プロジェクトのソースツリーは次のとおりです。

% cd ~/myapp/
% tree src
src
├── CMakeLists.txt
├── lib1
│     ├── CMakeLists.txt
│     ├── include
│     │     └── lib1.h
│     └── lib1.c
├── lib2
│     ├── CMakeLists.txt
│     ├── include
│     │     └── lib2.h
│     └── lib2.c
└── myapp.c

サンプルプログラムのソースコード

サンプルプログラムのソースコードを示します。lib1、lib2 それぞれ特に何の意味もない関数を1 つずつ持ち、main関数でその戻り値を表示して終了する、というだけのものです。

src/myapp.c
#include <stdio.h>
#include "lib1.h"

int main(int argc, char **argv)
{
    printf("func1 = %d\n", func1());
    printf("func2 = %d\n", func2());
    return 0;
}
src/lib1/include/lib1.h
#ifndef _LIB1_H_
#define _LIB1_H_

#include "lib2.h"

int func1();

#endif
src/lib1/lib1.c
# include "lib1.h"

int func1()
{
    return 1 + func2();
}
src/lib2/include/lib2.h
#ifndef _LIB2_H_
#define _LIB2_H_

int func2();

#endif
src/lib2/lib2.c
# include "lib2.h"

int func2()
{
    return 2;
}

ここからは CMakeLists.txt です。まずは最上位のものから。

src/CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(myapp)

add_subdirectory(lib1)
add_subdirectory(lib2)

add_executable(myapp myapp.c)

target_link_libraries(myapp lib1)

10 行目で myapp に lib1 をtarget_link_libraries()コマンドでリンクしています。INTERFACEキーワードは指定していませんが、上でも述べたとおり lib1 の Usage Requirements はmyapp に適用されます。

次に lib1 の CMakeLists.txt です。

src/lib1/CMakeLists.txt
# pico_add_library()
add_library(lib1_headers INTERFACE)
add_library(lib1 INTERFACE)
target_compile_definitions(lib1 INTERFACE USE_LIB1)
target_link_libraries(lib1 INTERFACE lib1_headers)

target_include_directories(lib1_headers INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)

target_sources(lib1 INTERFACE ${CMAKE_CURRENT_LIST_DIR}/lib1.c)

# pico_mirrored_target_link_libraries()
target_link_libraries(lib1 INTERFACE lib1_headers)
target_link_libraries(lib1_headers INTERFACE lib2_headers)
target_link_libraries(lib1 INTERFACE lib2)

target_link_options(lib1 INTERFACE "LINKER:-Map=memmap.map")

2 ~ 5 行目はpico_add_library関数、12 ~ 14 行目はpico_mirrored_target_link_libraries関数の中身を展開した形になっています。2、3 行目で lib1_headers および lib1 をインターフェース・ライブラリとして作成し、4 ~9、16 行目で Usage Requirements を設定し、12 ~ 14 行目で各ライブラリ間の依存関係を定義しています。

ちなみに 16 行目ではリンク・オプションの指定例としてメモリマップの出力ファイル名を指定しています。"LINKER:" と書かれている部分は開発環境ごとの差異を吸収するためのプレフィックスで、Clang では"-Xlinker -Map=memmap.map" に、GCC では "-Wl,-Map=memmap.map" ("-Wl" は GNU LD にオプションを渡すための GCC のオプション) に展開されます [5]

最後に lib2 の CMakeLists.txt です。

src/lib2/CMakeLists.txt
# pico_add_library()
add_library(lib2_headers INTERFACE)
add_library(lib2 INTERFACE)
target_compile_definitions(lib2 INTERFACE USE_LIB2)
target_link_libraries(lib2 INTERFACE lib2_headers)

target_include_directories(lib2_headers INTERFACE ${CMAKE_CURRENT_LIST_DIR}/include)

target_sources(lib2 INTERFACE ${CMAKE_CURRENT_LIST_DIR}/lib2.c)

target_compile_options(lib2 INTERFACE -v)

やっていることは lib1 の CMakeLists.txt とほぼ同じですが、11 行目はデバッグ用です。コンパイル時のログを標準エラー出力に表示するための Clang / GCC のオプションを指定しています。

サンプルプログラムのビルドと実行

ビルド方法、実行方法は前回までと同じです。

% cd ~/myapp/
% mkdir build
% cmake -S src -B build
% cmake --build build
% ./build/myapp

生成された Makefile を眺める

ビルドの結果、~/myapp/build/ のツリーは次のようになりました (treeコマンドの出力を抜粋しています)。

% tree build
build
├── CMakeFiles
│     ├── Makefile2
│     ├── myapp.dir
│     │     ├── build.make
│     │     ├── compiler_depend.ts
│     │     ├── flags.make
│     │     ├── lib1
│     │     │     ├── lib1.c.o
│     │     │     └── lib1.c.o.d
│     │     ├── lib2
│     │     │     ├── lib2.c.o
│     │     │     └── lib2.c.o.d
│     │     ├── link.txt
│     │     ├── myapp.c.o
│     │     ├── myapp.c.o.d
│
├── lib1
│     ├── cmake_install.cmake
│     ├── CMakeFiles
│     └── Makefile
├── lib2
│     ├── cmake_install.cmake
│     ├── CMakeFiles
│     └── Makefile
├── Makefile
├── memmap.map
└── myapp

主要な部分だけ前々回と同様に有向グラフらしく描くと、図 2 のようになります。

図 2
図 2

当然といえば当然ですが、前回までとは異なり、liblib1.a とか liblib2.a といったアーカイブファイルは生成されません。lib1 や lib2 の Usage Requirements はどこに行ったのかというと、build/CMakeFiles/myapp.dir/ の下にある flags.make と link.txt に記述されています。

flags.make では、マクロ、インクルード・ディレクトリ、コンパイル・オプションをそれぞれC_DEFINESC_INCLUDESC_FLAGSという名前の変数にセットしています。build/CMakeFiles/myapp.dir/build.make がこのファイルをインクルードしてこれらの変数を参照しています。

build/CMakeFiles/myapp.dir/flags.make
# compile C with /usr/bin/cc
C_DEFINES = -DUSE_LIB1 -DUSE_LIB2

C_INCLUDES = -I/home/mijinco/cmake/myapp/src/lib1/include -I/home/mijinco/cmake/myapp/src/lib2/include

C_FLAGS = -v

link.txt には、リンク・オプションとして指定しておいたマップファイルが、しっかり書かれていました。また、lib1、lib2 が静的ライブラリとしてではなく、それぞれオブジェクトファイル単独でリンクされていることも、このファイルからわかります (見やすいように整形していますが、実際は 1 行です)。

build/CMakeFiles/myapp.dir/link.txt
/usr/bin/cc -Xlinker -Map=memmap.map
CMakeFiles/myapp.dir/myapp.c.o
CMakeFiles/myapp.dir/lib1/lib1.c.o
CMakeFiles/myapp.dir/lib2/lib2.c.o
-o myapp

インターフェース・ライブラリでは INTERFACE モードのみ可

ところで、インターフェース・ライブラリを他のターゲットにリンクするときにtarget_link_libraries()コマンドのスコープをINTERFACE以外にするとどうなるでしょうか?せっかくサンプルプログラムを作ったので、確認してみました。lib2 の CMakeLists.txt を次のように書き換えて再度 CMake を実行します。

src/lib2/CMakeLists.txt (差分)
- target_link_libraries(lib2 INTERFACE lib2_headers)
+ target_link_libraries(lib2 PRIVATE lib2_headers)

結果は、シンプルにエラーになりました。

% cmake -S src -B build
...
CMake Error at lib2/CMakeLists.txt:5 (target_link_libraries):
  INTERFACE library can only be used with the INTERFACE keyword of
  target_link_libraries

というわけで、インターフェース・ライブラリに対してtarget_link_libraries()コマンドを使う場合、指定可能なスコープはINTERFACEのみとなります。

余談: ヘッダファイルを更新しても再コンパイルされる?

余談ですが、build ディレクトリの下に拡張子が .d のファイルがあります。これは、オブジェクトファイルとヘッダファイルの依存関係を記述するファイル (GCC に-MDオプションを付けると生成されるやつ) と思われます。ヘッダファイルを更新したときに、関連するソースファイルが再コンパイルされるようにするには、Makefile にそれなりの工夫が必要になりますが (以前の記事「Raspberry Pi Pico の L チカプログラムをビルドして動かす」を参照)、そのあたりも CMake がよきに計らってくれているようです。

まとめ: Pico SDK におけるインターフェース・ライブラリ

本稿の最初のほうで「Pico SDK におけるライブラリ」の例を挙げましたが、最後に改めて「Pico SDK におけるインターフェース・ライブラリ」について、わかったことをまとめてみます。

  • Pico SDK では、ソフトウェア・モジュールごとの煩雑な Usage Requirements をカプセル化するために、インターフェース・ライブラリを活用している。
  • Usage Requirements のうち、インクルード・ディレクトリについては <モジュール名 (pico_stdlib など)>_headers というインターフェース・ライブラリを作り、そこにカプセル化している。
  • それ以外の Usage Requirements については、モジュール名と同名のインターフェース・ライブラリを作り、そこにカプセル化している。
  • Pico SDK のソフトウェア・モジュール群はインターフェース・ライブラリによってヒエラルキーを形成し、その最上位に pico_stdlib がある。
  • Pico SDK を利用する側は、 pico_stdlib をtarget_link_libraries()コマンドでリンクすればよい。

参考資料

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