Ubuntu 18.04 GNOME設定

以前にUbuntu 16.04のUnityの設定についての記事を書いたが、今回はUbuntu-18.04の話。

Ubuntuを最初に触ったのは8.04の頃だったが、その頃のデスクトップ環境のデフォルトはGnome2だった。Gnome2のデフォルト状態も色々不満があったが、たいていのことは設定変更で解決できた。特にGConfという、Windowsのレジストリみたいなものがあり、それを使うとかなり細かいこと(例えばタスクバーが表示されるときのアニメーションの速度など)まで設定できたので重宝した。

それが11.04になるとUnityという独自のデスクトップ環境になったが、もはや過去の遺物なので詳細は省くが、これがまた使いにくい代物だった。「Unityの開発者も新たなデスクトップ環境を開発するにあたっては使いやすさを当然考えているはず。それが受け入れられないのは自分が古いものに囚われているからかもしれない」と考えて何度も慣れようとしたが結局挫折した。幸いUnityが使いにくいと思っていたのは自分だけではなかったようで、Unity Tweak Toolといったツールも現れたことでようやく状況は改善され、これを使ってUnity環境を改善したのが前回の記事だ。

その後CanonicalはUnityの開発を中止し、17.04からデフォルトのデスクトップ環境がGnomeに戻った。しかしそれはGnome2ではなくGnome3である。個人的な感想としてはGnome3はUnityと同じくらい使いにくい。それを何とかしようというのが今回の目標である。

基礎知識と準備

Gnome3の設定・改良は次の3種類を使う。

設定
Gnome3に最初から入っている「設定」
Gnome Tweak Tool
設定からは変更できない細かい設定を行うもの。デフォルトでは入っていないので`sudo apt install gnome-tweak-tool`でインストールしておく。
Gnome Shell拡張
Gnome Shellは設定変更だけでなく、ユーザが機能を拡張することができるようになっている。それらの多くはhttps://extensions.gnome.org に置かれており、ここから使いたい拡張機能をダウンロードしてインストーする。なお、拡張機能のインストールには事前に準備が必要なので後述する。

Gnome Shell拡張のための準備

Gnoem Shell拡張を使うにはchrome-gnome-shellとブラウザの拡張が必要だ。chrome-gnome-shellはsudo apt install chrome-gnome-shellでインストールできる。なお名前に"chrome"が入っているがGoogle Chromeとは関係ない。

またFirefoxで何らかの拡張機能のページ、例えばhttps://extensions.gnome.org/extension/15/alternatetab/ に行くと、ブラウザ拡張が入っていない場合は以下のようなメッセージが出ているだろう。このメッセージの中の"Click here to install browser extension"をクリックすればインストールできる。

f:id:wagavulin:20190428192319p:plain

ブラウザ拡張がインストールされていれば以下のようにOn/Offのトグルボタンになっているだろう。これをOnにすればインストールできる。

f:id:wagavulin:20190429091112p:plain

これで準備ができたので実際に設定を変更していく。

Alt-tabをウィンドウ単位にする

Unityもそうだったが、Gnome ShellでもAlt-tabの動作は「ウィンドウの切り替え」ではなく「アプリのの切り替え」だ。なので、同じアプリのウィンドウが複数ある場合は最初にアプリを選択し、その後にウィンドウを選択するという2段階の動作が必要である。下の例ではNautilus(標準のファイルマネージャ)のウィンドウが2つあるため、2段階目で選択している。

f:id:wagavulin:20190429092550g:plain

これをウィンドウ単位にするにはGnome Shell拡張のAlternateTabを使えば良い。インストールは上述のとおり、トグルボタンをOnにすれば良い。これでAlt-tabの動作が以下のようになる。

f:id:wagavulin:20190429092735g:plain

なお、以下のようなメッセージが出た場合はchrome-gnome-shellがインストールされていないのでインストールする。

f:id:wagavulin:20190429091601p:plain

アプリケーションメニュー

Windowsのスタートメニューに相当するものがGnome Shellにはなく不便なので追加する。これもGnome Shell拡張のApplicaitons Menuで追加できる。インストールすると左上が以下のように「アプリケーション」に変わる。元々は「アクティビティ」だったはずだ。

f:id:wagavulin:20190429093421p:plain

が、デフォルト設定のままではこれを押しても何も起きないという問題がある。Gnome Tweak Toolから「トップバー」を選び、そこにある「Activities Overview Hot Corner」を「オン」にすれば動くようになる。

f:id:wagavulin:20190429094524p:plain

うまくいけば以下のようなメニュー表示できるようになるはずだ。

f:id:wagavulin:20190429094618p:plain

ワークスペース(仮想デスクトップ)の設定

ワークスペース数の固定化

Gnome Shellはデフォルトでワークスペース(仮想デスクトップなどとも呼ばれているもの)に対応しているが、ワークスペースは必要に応じて自動的に増減するようになっている。具体的には最初はワークスペースは1つだけだが、何かのウィンドウのタイトルバーを右クリックして「下側のワークスペースへ移動する」を選べば自動的に2つ目のワークスペースができる。

はじめから決まった数だけ作る場合はGnome Tweak Toolの→「ワークスペース」から「静的ワークスペース」を選択する。

f:id:wagavulin:20190429095007p:plain

ショートカットキーの設定

ワークスペースの切り替えはキーボードでできた方が便利なので設定する。「設定」→「デバイス」→「キーボード」からショートカットキーを設定する。自分の場合は「ワークスペース{1,2,3,4}へ切り替える」に対してそれぞれ「Alt+1」「Alt+2」「Alt+3」「Alt+4」を設定している。ここら辺は好みと、自分がよく使うアプリと衝突しないか、というあたりとの兼ね合いなのでよく考えて決めてほしい。

f:id:wagavulin:20190429095314p:plain

アニメーションの無効化

これでキーボードでの切り替えができるようになったが、実際に使ってみると切り替え時の表示が目障りに感じる。

f:id:wagavulin:20190429095937g:plain

まず画面全体がスライドするアニメーションが目障りだ(キャプチャー時のフレームレートが低いためこの動画だと分かりにくいかもしれないが)。もう一つ、画面中央に表示されるインジケータも大きくて目立つため目がチカチカする。この2つを何とかしよう。

まず画面全体のスライドアニメーションだが、これはGnome Tweak Toolの「外観」→「アニメーション」をオフにすれば無効化できる。ただし他のアニメーション(例えばウィンドウを最大化・最小化したときなど)も無効化されるので好みによるところかもしれない。

インジケータの無効化は拡張機能で行う。Disable Workspace Switcher Popupを入れれば無効化できる。

f:id:wagavulin:20190429100426g:plain

ワークスペース番号の表示

インジケータを無効化したことで今度は今いるワークスペースがどこかを知るのが面倒になったので、ワークスペース番号をツールバーに表示する Workspace Indicatorを入れる。

f:id:wagavulin:20190429110444p:plain

これでワークスペースが快適に使えるようになった。

テーマの設定

UnityからGnome3に変わっても相変わらず見た目はオレンジと紫を基調にしたやつのままだ。Ubuntu開発者にとってはこれが良いのかもしれないが、個人的にはあまり好きではないので変更する。

もちろん壁紙の部分の変えるのは簡単で、デスクトップを右クリックして「背景を変更する」を選べば良い。今回変えたいのはウィンドウ右上のボタンやトグルボタンといったUI部品の色だ。

f:id:wagavulin:20190429135442p:plain

これらを変えるにはテーマを設定する。Gnome Tweak Toolの「外観」→「テーマ」から変更できる(なぜこの程度のことが標準の「設定」からできないのか)。「アプリケーション」「アイコン」「カーソル」「Gnome Shell」の4つがあり、それぞれ変更箇所が異なる。「アプリケーション」と「Gnome Shell」の区分ははっきりとは知らないが、試したところトップバーとサイドバーはGnome Shellの範囲、UI部品の形や色などは「アプリケーション」になるようだ。

f:id:wagavulin:20190429135545p:plain

f:id:wagavulin:20190429135702p:plain

Gnome Shellテーマ変更の有効化

上の図を見て分かるとおり、Gnome Shellのテーマ変更はこのままではできない。Gnome Shell拡張のUser Themesを入れればこの部分が使えるようになる。あらかじめ入れておこう。

テーマのダウンロードと設置

テーマはgnome-look.orgにたくさんあるのでここから探すのが良いだろう。昔はこの手のサイトにはエロテーマも結構あったので会社からアクセスするのは躊躇したものだが、最近はそうでもないようで随分と健全になったものだ(とは言っても今でも微エロ程度の画像はチラホラ見えるので会社からアクセスするときは一応注意しよう)。

テーマはCursors, Gnome Shell Themesなどいくつかの分類があるが、今回やりたいUI部品についてはGTK3 Themesになるようだ。今見たところ、Flat Remix GTK/Elementary themeというのが評価が高いようなのでまずはこれで試してみよう(特に好みというわけではないが)。下の方にある「Files」を選択してダウンロードする。

f:id:wagavulin:20190429135803p:plain

テーマの置き場はいくつかあり、全ユーザ共通にするなら/usr/share/themes以下で、すでにAdwaitaやAmbianceといったインストール済みのテーマがあるだろう。自分用であれば$HOME/.themes以下でもよい。いちいちsudoしなくても良いためこちらの方が便利かもしれない。$HOME/.themesはデフォルトでは作られていないので自分で作り、そこでダウンロードしたアーカイブを解凍する。アーカイブは.tar.gzだったり.zipだったりするが、.tar.gz, .tar.bz2, .tar.xzあたりならtar xvf xxx.tar.gz解凍できる。.zipならunzip xxx.zipだ。

ただし時折解凍後にフォルダを作らずその場所にファイルをばら撒くようになっているものもあるので(最近はあまり見なくなったが)、解凍後にフォルダ・ファイル一覧を確認してからの方が良いかもしれない。tarのときはtar tvf xxx.tar.gzunzipならunzip -t xxx.zipで実際に解凍せずにファイル一覧を見ることができる。今ダウンロードしたFlat-Remix-GTK-Blue_2.16.tar.xzは問題ないようだ。

これで$HOME/.themes/Flat-Remix-GTK-BlueフォルダができればGnome Tweak Toolからテーマを選択できるようになる。Gnome Tweak Toolが既に起動済みであれば一度終了してから再度開く。テーマを選択すれば以下のようになるだろう。

f:id:wagavulin:20190429135842p:plain

Vertexテーマのインストール

いくつかテーマをインストールして試してみたが、今のところVertexを使っている。ただしこのテーマはすぐに使えるアーカイブはなく、ソースコードを自分でビルドする必要がある。ビルド手順はテーマのGithubページに書かれており、特別難しいことがあるわけではないが、Linuxの開発環境にある程度馴染みがないと大変かもしれない。面倒なので今回はそこまで説明しないが、結果は以下のような感じである。

f:id:wagavulin:20190429140037p:plain

テーマ選定について

テーマを選ぶときはデフォルトとあまり大きく変わるものでない方が良いかもしれない。最近はナイトモードやダークモードといった名前で黒基調にする機能がWindowsやmacOSにも見られ、実際個人的には黒基調の方が好きだが、GTK3テーマを黒基調にするとアプリのよっては外観が損なわれることがある。例えば以下はVertexに含まれているVertex-Darkにしたもので、この画面だけでは特に問題ないが、これを適用するとWebページの表示も大きく影響を受ける。

f:id:wagavulin:20190429140132p:plain

以下は<textarea>要素を持つウェブページを表示したところだ。CSSの類は一切設定していないので通常は白背景で黒文字になるが、Vertex-Darkを適用するとこのように黒背景白文字になる。こんな感じでウェブページの外観にも影響を与えるので場合によっては見た目が変な感じになることがある。

f:id:wagavulin:20190429140236p:plain

その他雑多な設定

面倒なのでスクリーンショットは貼らないが、今のところ以下の設定をしている。

  • ツールバーに日付を表示する
    • デフォルトでは「月曜日 01:23」のような感じだが、ここに日付を追加できる。
    • Gnome Tweak Tool -> 「トップバー」 -> 「日付」で設定できる。
  • ロックとスクリーンセーバー
    • VMwareの仮想マシンとして使っているので画面ロック・スクリーンセーバーは(必要あれば)ホストであるWindows側でやるためUbuntu側には必要ない。これらを無効にする。
    • 「設定」→「プライバシー」→「画面ロック」→「画面オフ後にロックするまでの時間」で時間を設定できる。
    • また「電源」→「ブランクスクリーン」を「しない」にする。

Apache Arrowのビルド

先日OSS Gate東京ミートアップ for Red Data Tools in Speeeに参加して、Apache Arrowの開発にデビューしました。自分の専門でない分野に一人で飛び込むのはなかなか大変なので、こういうイベントがあるのは助かります。

と言っても2時間の中でできたのは、公式サイトのミスの修正案のPullRequestを送ったくらいで(無事マージされました)、その後Arrowをビルドにトライし、cpp (Arrow C++) はすぐにできたものの、c_glib (Arrow C++のglibラッパー) ビルドの途中でいくつかエラーがあり、時間切れという感じでした。

その後家で続きをやったのでまとめ。試したのはUbuntu-16.04とmacOS Sierraで、Arrowはそのときgithubから持ってきたもの(5cda6934999f9f79368f3fc3f68895fc0f4e0b24)です。

Ubuntu-16.04

cpp

cppのビルドはcpp/README.mdの手順通りで問題なし。ただし、ビルドだけでなくsudo make installしておかないとc_glibのビルドに失敗する。また、初めてインストールしたときはsudo ldconfigしておかないとやはりc_glibのビルドに失敗する。

% git clone https://github.com/apache/arrow.git
% cd arrow/cpp
% mkdir debug
% cd debug
% cmake ..
% make unittest
% sudo make install
% sudo ldconfig

c_glib

ビルド手順はc_glib/README.mdに載っている。今回はgithubから取ってきたソースなので、「How to build by users」ではなく「How to build by developers」に従ってビルドする。
c_glibのビルドもほぼc_glib/README.mdの手順通り。

% cd c_glib
% ./autogen.sh
% ./configure
% make
% sudo make install

なお、以下のようなエラーが出た場合は多分cppビルド後のmake install, ldconfigができていない。

make[3]: ディレクトリ '/home/wagavulin/arrow/c_glib/arrow-glib' に入ります
  GISCAN   Arrow-1.0.gir
/home/wagavulin/arrow/c_glib/arrow-glib/tmp-introspectxbzARl/.libs/lt-Arrow-1.0: error while loading share$
 libraries: libarrow.so.0: cannot open shared object file: No such file or directory

macOS Sierra

cpp

cpp/README.mdの通りだが、ビルド後にsudo make installする。なお、Linuxとは異なりldconfigは必要ない(macOSにはldocnfig自体ない)。

c_glib

基本的にはc_glib/REAMDE.mdの「How to build by developers」に従ってビルドするが、いくつかトラブった。

AX_CXX_COMPILE_STDCXX_11マクロの問題

configureを実行したところ途中で失敗。原因はAX_CXX_COMPILE_STDCXX_11マクロに関するもの。調べてみると、このマクロを使用するにはautoconf-archiveを入れる必要があるらしいが、すでに手元のマシンには入っている。試しにアンインストールしてもう一度インストールしたら以下のようなメッセージが出ていた。

$ brew install autoconf-archive
Warning: autoconf-archive 2017.03.21 is already installed, it's just not linked.
You can use `brew link autoconf-archive` to link this version.

どうやらインストールはされたもののリンクが作られていないため見つけられない状態のようだ。メッセージに従ってbrew link autoconf-archiveすると今度は以下のようになった。

$ brew link autoconf-archive
Linking /usr/local/Cellar/autoconf-archive/2017.03.21... 
Error: Could not symlink share/aclocal/ax_check_enable_debug.m4
Target /usr/local/share/aclocal/ax_check_enable_debug.m4
is a symlink belonging to gnome-common. You can unlink it:
  brew unlink gnome-common

To force the link and overwrite all conflicting files:
  brew link --overwrite autoconf-archive

To list all files that would be deleted:
  brew link --overwrite --dry-run autoconf-archive

リンクが作られていないのはgnome-commonというパッケージと衝突するかららしい。仕方ないのでbrew unlink gnome-commonした後brew link autoconf-archiveしたら成功し、AX_CXX_COMPILE_STDCXX_11マクロに関するエラーも解決した。

なお、gnome-commonを入れてなければautoconf-archiveインストール時に自動的にリンクが作られているので、この問題に遭遇することはなさそう。

libffiの問題

AX_CXX_COMPILE_STDCXX_11のエラーは解決したが、今度はgobject-introspectionがないと言われた。

checking for gobject-introspection... configure: error: gobject-introspection-1.0 is not installed

gobject-introspectionは入ってるはずなんだが...。config.logを見てみると、gobject-introspectionが見つからないのではなく、libffiが見つからないのが原因のようだ。

configure:16847: checking for gobject-introspection
configure:16856: $PKG_CONFIG --exists --print-errors "gobject-introspection-1.0"
Package libffi was not found in the pkg-config search path.
Perhaps you should add the directory containing `libffi.pc'
to the PKG_CONFIG_PATH environment variable
Package 'libffi', required by 'gobject-introspection-1.0', not found
configure:16859: $? = 1
configure:16863: error: gobject-introspection-1.0 is not installed

libffiはkeg onlyというやつで、インストールしてもリンクが作られないためpkg-configが見つけられない。brew linkに--forceを付けて強制的にリンクすることもできるようだが、どういう副作用があるか分からない。pkg-configの探索パスに追加するだけでも回避できるようなのでその方向でやろう。

$ export PKG_CONFIG_PATH="/usr/local/Cellar/libffi/3.2.1/lib/pkgconfig"
$ ./configure

これでビルドできた。ただし、ビルドの途中で以下のようなエラーがずらずらと出てきた。

  GISCAN   Arrow-1.0.gir
/usr/include/signal.h:79: syntax error, unexpected identifier, expecting ')' in 'void (* _Nullable bsd_signal(int, void (* _Nullable)(int)))(int);' at 'bsd_signal'
/usr/include/signal.h:79: syntax error, unexpected ')', expecting ',' or ';' in 'void (* _Nullable bsd_signal(int, void (* _Nullable)(int)))(int);' at ')'
以下略

@kouさんによると問題なさそうで、test/run-test.shが動けばよさそう。やってみたところ100% passedになった。

ということでようやくビルド成功という感じです。

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"
}

とすれば良い。