CMakeを使ってみた (7) find_packageとpkg_check_modulesによるライブラリ探索

久しぶりにCMakeの話。

外部の依存ライブラリがあるC/C++のコードをCMakeでビルドする場合、インクルードパスやライブラリパスを指定する必要がある。パスを直接指定する方法以前書いた。しかし、そこで書いたのはパスやライブラリ名を直接指定するもので、それらがすでに分かっている必要がある。

しかし、システムにインストールされたライブラリを使うような場合はそのパスを探し出して指定する必要がある。システムによってインストールされた場所が異なることがあるためだ。Unix系OSでよく使われるGNU Autotools環境では、ライブラリの探索はAutoconfの役目で、`./configure && make`の`./configure`スクリプトが行う。では同様のことをCMakeでやるにはどうすれば良いだろうか。

ということで今回はCMakeのライブラリ探索の話。方法はいくつかあるので、順番に見ていこう。

find_packageコマンドを使う

CMakeをインストールするとcmakeコマンドだけでなくたくさんのモジュールをインストールしており、よく知られたライブラリの探索モジュールも含まれている。利用可能なモジュールの一覧は`cmake --help-module-list`で表示できる。多数のモジュールが表示されるが、その中でFindXXXという名前になっているのが探索用のモジュールだ。

ここにあるライブラリであればfind_packageコマンドを使って探すことができる。まずはこれを使ってみよう。

例としてGTK2を使うアプリを考える。以下のような、GTK2を使うソースファイル (main.c) を作った。GTK自体の実験ではないので単にgtk_init()を呼び出すだけで何もしないプログラムだが、ビルドにはインクルードパスなどの指定が必要だ。

#include <gtk/gtk.h>

int main(int argc, char **argv){
    gtk_init(&argc, &argv);
}

これをビルドするCMakeLists.txtは以下のようになる。

cmake_minimum_required(VERSION 2.6)
project(Hello C)
add_executable(hello main.c)
find_package(GTK2 REQUIRED)
include_directories(${GTK2_INCLUDE_DIRS})
target_link_libraries(hello ${GTK2_LIBRARIES})

find_packageコマンドを実行すると、指定したライブラリの探索モジュール(FindGTK2)を探し、それを実行する。FindGTK2はそのシステムにあるGTK2を探し、その結果を変数に入れる。この場合、インクルードパスやリンクするべきライブラリがGTK2_INCLUDE_DIRSとGTK2_LIBRARIESにセットされる。あとはinclude_directoriesやtarget_link_librariesを使って指定すればよい。なお、このプログラムにとってGTK2は必須のライブラリなのでREQUIREDを付けている。

find_packageコマンドがセットする変数

モジュールXXXに対し、find_packageがセットする変数はだいたい以下のようになる。意味は見れば分かるだろう。

  • XXX_FOUND
  • XXX_INCLUDE_DIRSまたはXXX_INCLUDES
  • XXX_LIBRARIESまたはXXX_LIBS
  • XXX_DEFINITIONS

XXXの部分はGTK2_FOUND, CURSES_FOUNDなどのように全て大文字であることが多いが、Boost_FOUNDなどのようにキャメルケースになっている場合もある。具体的にどのような変数がセットされるかは、cmake --help-module FindGTK2 とすれば分かる。

find_packageの動作の詳細

find_packageの内部動作には2種類あり、モジュールモードとコンフィグモードと呼ばれている。

find_package(XXX)を呼び出すと、cmakeはまずFindXXX.cmakeというファイルを探す。最初に${CMAKE_MODULE_PATH}で指定されたディレクトリを探し、なければ/share/cmake-x.y/Modules以下(例えば/usr/share/cmake-3.5/Modules以下)を探す。それでもなければXXXConfig.cmakeかxxx-config.cmakeを探す。

FindXXX.cmakeを使うのがモジュールモードで、XXXConfig.cmake/xxx-config.cmakeを使うのがコンフィグモードだ。先の例ではFindGTK2.cmakeを使ったので、モジュールモードを使ったことになる。

モジュールモードとコンフィグモードの違いは単なる優先度の差だけではなく、作成者と処理の内容が(通常は)異なる。モジュールモードは、そのファイル名であるFindXXXという名前が示す通り、指定したライブラリを探すものであり、つまりそのライブラリの作成/インストールした人間以外が書くものだ。

一方コンフィグモードのファイルは探す対象のライブラリ自身によって置かれることを想定している。多分そのライブラリのインストーラなどが置く場合だろう。そのため、XXXConfig.cmake/xxx-config.cmakeは通常はライブラリを「探す」ことはしない。そのライブラリ自身が置いたのであれば、ライブラリがどこにインストールされたのかは知っているはずなので、単にハードコードされた値が書かれているだろう。

ただし、現状はコンフィグモードの.cmakeを置くライブラリはあまりないそうだ。実際、手元のUbuntu-16.04を見てもそのようなファイルは見当たらない。そういうわけで、コンフィグモードを使うのはCMakeがもっと普及したら、あるいは自分で作る場合だけになりそうだ。

pkg_check_modulesを使う

find_packageが使えない場合は探索作業を自分で行うことになる。例えば、手元の環境のcmakeにはFindGTK(GTK1用)とFindGTK2(GTK2用)はあるが、GTK3を探すモジュールがない。仕方ないのでGTK3を自分で探すわけだが、pkg_check_modulesが使えるならそんなに難しくない。

とここでpkg_check_modulesの説明の前に、それが内部で使うpkg-configを説明する。

pkg-configとは

pkg-configはインクルードパスやライブラリパスといった、そのライブラリを使うアプリケーションをビルドするのに必要な情報を提供してくれるツールだ。freedesktop.orgという、Unix系OSのデスクトップ環境の共通仕様・ツールを提供する団体が作ったものらしい(なのでpkg-config自体はCMakeとは無関係)。Linux, *BSD, Mac OS X, WindowsのMSYSといった多くの環境で使えるため、これを使えば簡単にポータブルなビルド環境を作ることができる。

使い方の詳細はmanを見て欲しいが、`pkg-config --cflags `でそのライブラリの利用に必要なコンパイラオプションが表示され、--libsオプションでリンカオプションが表示される。

$ pkg-config --cflags gtk+-3.0
-pthread -I/usr/include/gtk-3.0 -I/usr/include/at-spi2-atk/2.0 -I/usr/include/at-spi-2.0 -I/usr/include/dbus-1.0 -I/usr/lib/x86_64-linux-gnu/dbus-1.0/include -I/usr/include/gtk-3.0 -I/usr/include/gio-unix-2.0/ -I/usr/include/mirclient -I/usr/include/mircommon -I/usr/include/mircookie -I/usr/include/cairo -I/usr/include/pango-1.0 -I/usr/include/harfbuzz -I/usr/include/pango-1.0 -I/usr/include/atk-1.0 -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/freetype2 -I/usr/include/libpng12 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/libpng12 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include

$ pkg-config --libs gtk+-3.0
-lgtk-3 -lgdk-3 -lpangocairo-1.0 -lpango-1.0 -latk-1.0 -lcairo-gobject -lcairo -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0

GTK3を使うアプリなら以下のようにすればビルドできる。

$ gcc `pkg-config --cflags gtk+-3.0` main.c `pkg-config --libs gtk+-3.0`

なお、指定可能なライブラリ一覧は`pkg-config --list-all`で表示できる。

CMakeでのpkg-configの利用

で、この便利なpkg-configをCMakeから使えるようにしたのがPkgConfigモジュールだ。使い方はサンプルを見れば分かるだろう。

cmake_minimum_required(VERSION 2.6)
project(Hello C)
add_executable(hello main.c)

find_package(PkgConfig REQUIRED)
pkg_check_modules(GTK3 gtk+-3.0 REQUIRED)
include_directories(${GTK3_INCLUDE_DIRS})
target_link_libraries(hello ${GTK3_LIBRARIES})

まずはfind_packageでこのモジュールを見つける。実際にpkg-configを使うのはpkg_check_modulesコマンドで、pkg_check_modules( )のように指定する。は`pkg-config --list-all`で出てくる名前で、は結果を格納する変数の接頭辞だ。ここでは"GTK3"としたので、GTK3_INCLUDE_DIRSなどの変数がセットされる。セットされる変数一覧はここ

見ての通り、FindXXXを使った場合とたいして違いはない。実際、デフォルトでインストールされるFindXXX.cmakeも内部ではpkg_check_modulesを使っているものもある。

FindXXX.cmakeをコピーして使う

find_packageが使えない場合のもう1つの解決法として、他人が作ったコマンドをコピーして使うというのがある。ということでネット上を探してみる。

あるとすれば恐らくFindGTK3.cmakeという名前なので、その名前でググってみるといくつかそれっぽいのもが出てくる。中でもChromiumに含まれているものなら信頼できそうな感じがするので、これをコピーしてFindGTK3.cmakeという名前で保存しよう。

(念のため書くが、コードをパクるときはライセンスを確認しよう。とは言っても、出来上がるバイナリには含まれないビルドツール用のコードをコピーした場合はどうなるんだ?)

とりあえず、プロジェクトのトップにcmakeディレクトリを作り、その中に置いてみた。

test/
  +- CMakeLists.txt
  +- main.c
  +- cmake/
       +- FindGTK3.cmake

CMakeLists.txtは以下のようになる。

cmake_minimum_required(VERSION 2.6)
project(Hello C)
add_executable(hello main.c)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
find_package(GTK3 REQUIRED)
include_directories(${GTK3_INCLUDE_DIRS})
target_link_libraries(hello ${GTK3_LIBRARIES})

自分で置いたFindXXX.cmakeを使う場合、CMAKE_MODULE_PATHを指定する必要がある。上の方にも書いたとおり、モジュールの探索はCMAKE_MODULE_PATHとCMakeがインストールされたディレクトリのModules以下のみで、たとえCMakeLists.txtと同じディレクトリに置いたとしても自動的に読んではくれない。

REQUIREDとQUIET

find_package, pkg_check_modules共通のオプションにREQUIREDとQUIETがある。find_packageやpkg_check_modulesにREQUIREDを指定しないと、見つからなかった場合にメッセージは出すが処理は続行する。REQUIREDを指定すると見つからなかった場合は処理がそこで止まり、cmakeコマンド自体の戻り値も1になる。また、QUIETを指定すると見つからなかった場合のメッセージ出力が抑制される。

なお、find_packageにREQUIREDを指定しなかった場合でも、FindXXX.cmake自体が見つからなかった場合はエラーとなり、そこで処理は中止される。

以下に例を挙げる。なお手元の環境にはQt4が入っていない。

find_package(PkgConfig REQUIRED)
pkg_check_modules(XXX xxx)          # xxxというモジュールはないためエラーメッ
                                    # セージが出力されるが処理は続行
pkg_check_modules(XXX xxx QUIET)    # エラーメッセージを出さずに続行
pkg_check_modules(XXX xxx REQUIRED) # エラーとなり処理が中断される
find_package(Qt4)                   # エラーメッセージを出力して処理続行
find_package(Qt4 QUIET)             # エラーメッセージを出さずに続行
find_package(Qt4 REQUIRED)          # エラーとなり処理が中断される
find_package(YYY)                   # FindYYY.cmakeがないため、REQUIREDがついて
                                    # いなくてもエラー終了

参考資料

CMake:How To Find Libraries
https://cmake.org/Wiki/CMake:How_To_Find_Libraries
find_packageの公式マニュアル
https://cmake.org/cmake/help/v3.0/command/find_package.html
pkg_check_modulesの公式マニュアル
https://cmake.org/cmake/help/v3.0/module/FindPkgConfig.html

C++のマングルとextern "C" {

C++とCが混在したプログラムを書くとときどき定義したはずの関数がundefinedだと言われることがある。そんなときの対処法とマングルの話。

その前にまずはC言語だけの場合を考える。例えば以下のようなCのプログラムを書いてみる。

/* main.c */
#include "foo.h"

int main(){
    func1();
    return 0;
}
/* foo.h */
#ifndef FOO_H

void func1();

#endif
/* foo.c */
#include "foo.h"
#include <stdio.h>

void func1(){
    puts("ok");
}

見てのとおり、main.cはfunc1関数を呼び出しており、foo.cはfunc1関数を定義している。当然、main.cをコンパイルしてできたmain.oもfunc1を参照し、foo.cをコンパイルしてできたfoo.oはfunc1を定義する。

オブジェクト中で使われているシンボルはnmコマンドで見ることができる。

$ gcc -c main.c
$ nm main.o
                 U func1
0000000000000000 T main
$ gcc -c foo.c
$ nm foo.o
0000000000000000 T func1
                 U puts

"U"になっているのは定義されていないもの、つまりそのシンボルを外部参照している。"T"になっているのはそのシンボルが定義されていることを意味する(正確には、"T"は「そのシンボルがテキスト(コード)セクションにある」という意味だが、細かいことは今回は関係ないので省略)。

f:id:wagavulin:20170209214327p:plain

このため、main.oとfoo.oをリンクさせてやれば、func1関数の呼び出しが可能になる。

$ gcc main.o foo.o
$ ./a.out
ok

C++の場合

C++の場合もCと同じような方法でシンボルの解決を行うが、Cとは違ってソースコード中の関数/変数名をそのままオブジェクトファイル中のシンボルにすることはできない。というのは、C++では同名の関数を複数作ることができるからだ。引数の型が異なれば同じ名前を使えるし(オーバーロード)、クラスや名前空間が異なればやはり同じ名前を使える。以下のような場合を考えよう。

// main.cpp
#include "foo.h"

int main(){
    func1();
    func1(10);
    Foo::func1();
}
// foo.h
#ifndef FOO_H
#define FOO_H

void func1();
void func1(int);

class Foo {
public:
    static void func1();
};

#endif // FOO_H
// foo.cpp
#include "foo.h"

void func1(){}

void func1(int){}

void Foo::func1(){}

このソースでは、func1という名前の関数が3つ存在する。そのため、func1という名前だけでは同定することができず、正しくリンクできなくなってしまう。そのため、C++コンパイラは、引数の型やクラス・名前空間名を使って修飾された一意な名前を作成し、オブジェクトファイルにはその修飾された名前を使う。これをマングル(mangle)と呼ぶ。

実際にコンパイルして見て見ると以下のようになる。

$ g++ -c main.cpp
$ nm main.o
                 U _Z5func1i
                 U _Z5func1v
                 U _ZN3Foo5func1Ev
0000000000000000 T main

$ g++ -c foo.cpp
$ nm foo.o
0000000000000007 T _Z5func1i
0000000000000000 T _Z5func1v
0000000000000012 T _ZN3Foo5func1Ev

f:id:wagavulin:20170209214328p:plain

なにやら複雑な名前になっているが、よく見ると"func1"や"Foo"という文字が見える。なお、マングルされた名前から元に戻すことをデマングルと呼び、nmコマンドでは--demangleでデマングルできる。

$ nm --demangle main.o
                 U func1(int)
                 U func1()
                 U Foo::func1()
0000000000000000 T main
$ nm --demangle foo.o
0000000000000007 T func1(int)
0000000000000000 T func1()
0000000000000012 T Foo::func1()

C++からCの関数を呼ぶ場合

C++からCの関数を呼ぶ場合、このマングル処理が問題になる。以下の例を考えよう。

// main.cpp
#include "foo.h"

int main(){
    func1();
}
/* foo.h */
#ifndef FOO_H
#define FOO_H

void func1();

#endif // FOO_H
/* foo.c */
#include "foo.h"

void func1(){}

main.cppはC++、foo.cはCで書かれている。これをビルドしてみよう。

$ g++ -c main.cpp
$ gcc -c foo.c
$ g++ main.o foo.o
main.o: In function `main':
main.cpp:(.text+0x5): undefined reference to `func1()'
collect2: error: ld returned 1 exit status

と、こんな風にエラーになってしまった。エラーメッセージによれば、func1()が見つからないらしいが、それはfoo.oにあるはずだ。

リンクできない理由はnmを使えば分かる。

$ nm main.o
                 U _Z5func1v
0000000000000000 T main
$ nm foo.o
0000000000000000 T func1

main.oはC++コンパイラが作ったので関数名がマングル,され"_Z5func1v"を参照しているが、foo.oはCコンパイラが作ったのでマングルされず、"func1"になっているのだ。

f:id:wagavulin:20170209214329p:plain

これを解決するには、C++コンパイラに対して、「func1はCの関数だからマングルしない名前で参照せよ」と命じる必要がある。これを行うのがextern "C" { ... }だ。これで囲まれた中で宣言された関数はCの関数とみなされ、マングルされずに使われる。従って、foo.hにある宣言をexter "C" { ... }で囲めば良い。

ただし、この構文はCコンパイラは理解できずエラーになる。そのため、__cplusplusマクロを使い、C++コンパイラから読まればときだけ有効になるようにする。

/* foo.h */
#ifndef FOO_H
#define FOO_H

#ifdef __cplusplus
extern "C" {
#endif

void func1();

#ifdef __cplusplus
}
#endif

#endif // FOO_H

これでようやくビルドできる。このextern "C" { ... }とインクルードガードはCのヘッダには必ず入れるようにしよう。

Cのヘッダにextern "C" {}がない場合

Cのヘッダにextern "C" {}が書かれておらず、かつそれが他人の作ったやつで変更できないような場合は、include文を囲むことで回避できる。上の例でfoo.hがC++対応していない場合、main.cppのinclude文を

extern "C" {
#include "foo.h"
}

とすれば良い。

Emacsで制御コードを入力する

Emacsを使っているときでも時々制御コードを直接入力したいことがある。

  • 文字列を置換するときに改行を入れたい
  • 強制的にタブを入力したい(たいていのモードではタブはインデントの設定になる)

ちょっとググれば、Ctrl-qを使うことで改行やタブが入力できることが分かる。改行はCtrl-q Ctrl-j、タブはCtrl-q Ctrl-iだ。しかし、なぜ改行がCtrl-jでタブがCtrl-iなのかはあまり説明がない。こういうのは理屈が分かっていないと覚えられないし、何より応用が効かない。例えばForm Feed (0x0C) を入力したい場合はどうすれば良いだろう?

"emacs ctrl-q"でググったところ、Google Booksにある"Unix Power Tools"という本の一部が引っかかった。曰く:

Ctrl-q tells Emacs that the next character you types is text, not a part of some command. So the sequence Ctrl-q Ctrl-l inserts the character Ctrl-l into your file;

適当な訳

Ctrl-qは、あなたがタイプしたのがテキストで、コマンドの一部ではないことをEmacsに伝える。そのため、Ctrl-q Ctrl-lというシーケンスは文字Ctrl-lをファイルに入力する。

うーん、これだけだとちょっと分からない。と、ここでWikipediaの「コントロールキー」の項目を見ると、コントロールキーの歴史として以下のようなことが書いてある。

テレタイプ端末、及び初期のコンピュータキーボードでは、コントロールキーを押しながら他のキーを押すと、生成されるASCIIの下位5ビット以外がゼロとなった。これによりユーザーはASCIIの非表示文字である最初の32文字 (0x00 - 0x1f) を生成、入力できる。

これで何となく分かった気がする。小文字のjはASCIIコードで0x6A、2進数なら01101010。下位5ビット以外を0にすると00001010 = 0x0A、つまり改行を表すLFになる。また、小文字のi (0x69 = 01101001) についても同様に処理すれば00001001 = 0x09 (タブ) となる。つまり、入力されたキーのASCIIコードと0x1fとのANDを取ればよい。

f:id:wagavulin:20170209212057p:plain

ということで、原理が分かればあとはASCIIコード表と簡単な計算でどんな制御コードでも入力できるようになる。と思ったのだが、実際にはCtrl-qの後に入力できる文字は限られているようだ。例えば、Ctrl-q Ctrl-, を打っても"C-, is not a valid character"と言われてしまう。試した結果、Ctrl-q Ctrlの次に以下のキーを押せば0x00-0x1Fを入力することができる。

Ctrl-q Ctrlの後に押すキー そのキーの16進表記 上位3ビットを0 (=入力される制御コード)
@ 0x40 0x00
a 0x61 0x01
b 0x62 0x02
c 0x63 0x03
d 0x64 0x04
e 0x65 0x05
f 0x66 0x06
g 0x67 0x07
h 0x68 0x08
i 0x69 0x09
j 0x6A 0x0A
k 0x6B 0x0B
l 0x6C 0x0C
m 0x6D 0x0D
n 0x6E 0x0E
o 0x6F 0x0F
p 0x70 0x10
q 0x71 0x11
r 0x72 0x12
s 0x73 0x13
t 0x74 0x14
u 0x75 0x15
v 0x76 0x16
w 0x77 0x17
x 0x78 0x18
y 0x79 0x19
z 0x7A 0x1A
[ 0x5B 0x1B
\ 0x5C 0x1C
] 0x5D 0x1D
^ 0x5E 0x1E
_ 0x5F 0x1F

GNOME Planner for Windows 独自ビルド版

リポジトリ構成を変更することにしたので一時公開停止中。

最近ちょいとプロジェクト管理ツールが使いたくなった。プロジェクト管理ツールと言えばMicrosoft Projectが有名だが、如何せん値段が高いし、そこまで高機能である必要はない。代わりのツールを色々検索してみたが、この手の作業はブラウザでやるのが流行りのようで、多くはWebアプリになっている。できればオフラインで使えて、かつ余計なもの(Javaランタイムとか)を入れなくて良いのがいい。あと、インストーラなしでフォルダコピーだけで使えるともっといい。

そうなると候補になるのはGNOME Plannerくらい。ただ、公式のWindows版を試したところファイル書き込み時にクラッシュする問題があって使えない。公式のメーリングリストにも似たようなレポートが来ているが、まだ修正されてないようだ。というより、ここ最近は各国語の翻訳に関するコミットはあるものの、ソースコードの修正はほとんど行われていないようだ。

ということで、自分で直してみたら一応動かすことに成功。

f:id:wagavulin:20170121221653p:plain

せっかくなのでGitHubに上げたので使ってみて欲しい。

ダウンロード

ダウンロードはGitHubのリリースページから。なお、ソースコードはここ

インストールと起動

インストールはZipファイルを展開するのみ。展開したフォルダ中のbin/planner.exeを起動すれば使えるはず。インターネット上からダウンロードしたexeを実行しようとすると多分メッセージが出るが、元のコードから余計な機能を加えたりはしてないので大丈夫。もちろんそれを信じるかは各人の判断だが。

制限

とりあえず動くようにしただけなのでまだ色々制限はある。

  • UIは英語のみ
  • 日本語の入力はできるが、IMEのウィンドウが別途画面左上に表示されるので見にくい
  • ファイル名・パスに日本語があると開けない
  • ユーザガイドは入ってない
  • ビルドオプションで指定できる機能はほぼすべてオフ(pythonプラグインやデータベースサポートなど)
  • インストーラはなくファイルの関連付けはしないので、.plannerファイルのダブルクリックで開くことはできない

そのうち直そうと思うが、現状でもとりあえず使う分にはそんなに困らないと思う。

バグ報告など

自分の環境(Windows 10 64bit)でしか動作チェックしていないので他の環境で動くかは分からない。もし問題があったらこの記事のコメントかGitHubのIssuesにでも挙げて欲しい。まあ手元にない環境での問題は多分直せないが。

あと、無事動いた場合は「Windows 7で動いた」などというだけでも助かるのでコメントしてもらえればと思う。