opencvr: OpenCV Rubyバインディング

OpenCVの新しいRubyバインディングであるopencvr-0.1をリリースしました。とは言っても現状はまだ足りない機能が多く実用的とは言い難い状態なので、どちらかと言うと「OpenCVのRubyバインディングの開発を始めました」というアナウンスという位置づけです。

https://github.com/wagavulin/opencvr

gemはまだ作っていないのでコマンド一発でインストールとはいかないですが、apt/brewでインストールしたOpenCVを使ってビルドできるようにはなっています。Ubuntu-20.04かmacOSの環境なら試すのはそんなに難しくないはずなので、気が向いたら試してもらえればと思います。方法はREADME.mdのHow to installを参照してください。

サンプルとスクリーンショット

元画像

f:id:wagavulin:20150315101002j:plain

Drawing

画像に矩形や直線などの図形や文字を書き込みます。

f:id:wagavulin:20210707190930j:plain

#!/usr/bin/env ruby
require 'cv2'
img = CV2::imread("input.jpg")
CV2::putText(img, "Hello OpenCV", [50, 50], CV2::FONT_HERSHEY_DUPLEX, 1.0, [0, 0, 0], lineType: CV2::LINE_AA)
CV2::imwrite(__dir__ + "/out-drawing.jpg", img)

油絵風

f:id:wagavulin:20210707190936j:plain

#!/usr/bin/env ruby
require 'cv2'
img = CV2::imread("input.jpg")
out = CV2::Xphoto.oilPainting(img, 2, 5, CV2::COLOR_BGR2Lab)
CV2.imwrite(__dir__ + "/out-oil.jpg", out)

鉛筆画風

白黒

f:id:wagavulin:20210707190946j:plain

色付き

f:id:wagavulin:20210707190954j:plain

#!/usr/bin/env ruby
require 'cv2'
img = CV2::imread("input.jpg")
out1, out2 = CV2.pencilSketch(img, sigma_s: 60, sigma_r: 0.07, shade_factor: 0.05)
CV2.imwrite(__dir__ + "/out-pencil1.jpg", out1)
CV2.imwrite(__dir__ + "/out-pencil2.jpg", out2)

使い方

Python版とだいたい同じです。

// C++
cv::Mat img = cv::imread("input.jpg", cv::IMREAD_COLOR);
# Python
img = cv2.imead("input.jpg", cv2.IMREAD_COLOR)
# Ruby
img = CV2::imread("input.jpg", CV2::IMREAD_COLOR)
  • C++ APIでオプショナルになっている引数はRubyでも省略可能です。
  • オプショナルな引数はキーワード引数も使えます。キーワード名はC++ APIの仮引数名と同じです。
  • 必須引数はキーワード引数にはできません。
  • CV2以外の名前空間名は、Rubyでは最初の文字のみ大文字にしたものになっています。
    • C++のcv.xphoto.oilPainting()CV2::Xphoto::oilPainting()です。
  • cv::Size, cv::Point, cv::RectなどはArrayになります。
    • 例えばcv::Sizeは数値2つを持つArrayです。
  • 引数が出力に使われる場合(引数が非const参照など)、結果は戻り値として帰ります。

OpenCVのAPI ReferenceにはPython APIも載っているで詳しくはそちらを参考にしてください。例えば以下のcv::clipLine()は、C++では3つの引数 imgSize, pt1, pt2を受け取りboolを返しますが、見ての通りpt1, pt2は非const参照で出力にも使われます。従ってPython/Ruby APIでは戻り値が3つになります。

f:id:wagavulin:20210708192137p:plain

なぜ作ったか

OpenCVは非常に有名な画像処理・コンピュータビジョンのライブラリで、C++で書かれてます。Pythonバインディングは公式に提供されていますがRubyバインディングについてはgemがいくつかありますが、決定版と言えるものはない感じです。古いバージョンについてはruby-opencvがよく使われていましたが、最新のOpenCV-4系では使えません。OpenCV-2.XまではC++/C両方のAPIが用意されており、ruby-opencvはC APIを使ってバインドされていましたが、4.0からはC APIが廃止されてしまったためです。

red-opencv, opencv-glib

新しいOpenCVに対応したRubyバインディングとして、red-opencvとopencv-glibがあります(2つを組み合わせて使います)。まずopencv-glibはOpenCVのC++ APIのGObjectバインディングを提供します。GObjectの詳細はここでは省きますが、GObjectからはRubyだけでなくPerl, Javaなどといった様々なプログラミング言語のバインディングを自動生成することができます。これを使ってRubyバインディングを提供するのがred-opencvです。

このopencv-glibについては私も以前に開発に参加して私のコードも少し入っていますが、手が止まってしまいました。理由は大きく2つあります。

  • OpenCVのAPIにglibバインディングではうまくできないところがあった。
  • OpenCVには大量のクラス・関数があり、手作業でのバインディングでは終わりが見えなかった。

前者の問題についてはglibに対する私の知識不足もあるので、実はたいした問題ではないかもしれません。しかし後者は由々しき問題です。試しにPython APIでバインドされている関数・enumをざっと数えたところ4000近くありました。

  • クラスに属さないグローバルな関数: 892
  • クラスに属する関数: 2925
  • enum型: 195

手作業でやると毎日休まず1日10個作っても10年以上かかるわけで、この数字を見たときに心が折れました。

ちなみに別の方法としてPyCallを使うというのがあります。私は試してはいませんが、Pythonバインディングが既に存在する以上、PyCallを使えばRubyからも呼べるはずです。ただそれを言ってしまうと話が終わってしまうのでそこはスルーして進めます。とにかくRubyから直接呼びたいのです(あんまり深い理由はないですが)。

Pythonバインディングとopencvr

では公式でサポートされているPythonバインディングはどうしているかというと、バインディングコードを自動生成しています。OpenCVのC++ APIのヘッダファイルからインターフェース情報を読み取り、それを基にバインディングコードを生成します。具体的にはhdr_parser.pyというスクリプトでヘッダファイルの情報を読み取り、それを基にgen2.pyがPythonのバインディングコードを生成します(もっと細かく言うと、自動生成コード以外ににいくつかの.cpp, .hppを使っています)。

f:id:wagavulin:20210708022558p:plain

この仕組みを真似すればRubyバインディングも自動生成できるはず、という発想で作ったのがopencvrです。hdr_parser.pyはそのまま流用し、gen2.pyを独自のものに置き換えています。Python用のgen2.pyやその他.cppファイルなど合わせて4000行くらいあり、それのRuby版を作るわけですが、4000個の関数のバインディングコードを書くよりはかなり作業量は減るはずです。

実装状況

こうして始めたopencvrですが、実装状況はまだ未熟です。ざっくり言うとバインドできているのは以下の条件を満たすもののみです。

  • クラスに属さないグローバル関数である
  • 引数・戻り値ともint, floatなどの基本型もしくはcv::Size, cv::Rectなど、Ruby側でArrayにバインドされているものである。
    • std::vector<int>など、これらを要素に持つstd::vectorはサポート。
  • クラスは現状未サポートだが、cv::Matのみは使用可能(というかこれがないと始まらない)。ただし使えるメソッドはcols(), rows(), channels(), at()のみ。

グローバル関数のみ、と聞くとほとんど何もできないじゃないかと思われそうですが、OpenCVのAPIはグローバル関数になっているものが結構あるのでそれなりに使えます。詳しいバインド状況はWikiページにあります。

またOpenCVの中心的なクラスであるcv::Matのメソッドがほとんど対応していないのは理由があって、次に書きます。

cv::Matと数値計算ライブラリ

OpenCVでは画像データなど多くのデータを表すのにcv::Matクラスを使います(APIリファレンス上ではcv::InputArray, cv::OutputArray, cv::InputOutputArrayになっているところ)。Python APIではこのcv::Matクラスに対応するPythonのクラスを作るのではなく、Numpyのndarrayクラスを使っています。例えば画像ファイルを読み込むcv::imread()関数は、C++ APIではcv::Matインスタンスを返しますが、Python APIではndarrayを返します。Pythonの画像処理・機械学習・データサイエンス系のライブラリの多くはndarrayを使っているので、それらのライブラリと容易に連携させることができるわけです。

import cv2
img = cv2.imread("input.jpg")
print(img.__class__) # => <class 'numpy.ndarray'>

これをどうやって実現しているかというと、cv::MatのAllocatorという機能を使っています。これはcv::Matが内部で使うメモリを確保するときに使用する関数を指定するもので、これとNumpyのC APIを組み合わせて実現します。例えばcv::imread()を読んだとき、以下のことをやります。

  • cv::imread()の戻り値 (m) とは別に空のcv::Matインスタンス (temp) を作る。
  • tempのAllocatorに独自のAllocatorをセットする。
  • mtempにコピーする。
    • このとき独自のAllocatorが使われる。その中ではNumpyのPyArray_SimpleNew()を使ってメモリを確保する。
    • 確保した領域へのポインタがcv::Matインスタンス内にセットされる。

出来上がったtempとPython側に返すポインタは以下のようになっています。

f:id:wagavulin:20210707192520p:plain

現状のopencvrはこのようなことはやってはおらず、Ruby側にもMatクラスを定義するようになっています。Pythonと同様のことをするならNumo::NArrayが候補になると思いますが、そもそも技術的に可能かどうかの検討もしていないので今後どうするかは未定です。cv::Matクラスのバインディングが手抜きなのはこれが理由です。

なお検討していない理由は時間の都合もさることながら、Numo::NArrayのC APIの使い方がさっぱり分からないというのが原因なので、分かりやすいドキュメントなどあったら教えてほしいです。

まとめ

  • OpenCVのRubyバインディング作ってます
  • 現状は未対応の部分がたくさんあります
  • Python版の仕組みを真似しているので、Python版の同等のことが現実的な工数できるはずです
  • Numpyみたいなものを使うかは未定です

と、ここまで書いたところで改めてgemを探すとropencvというgemがC++ヘッダから自動的にバインドするRubyインターフェースを提供しているっぽいのを見つけました。opencvr-0.1.0を作るまでにそれなりに時間使ったのですが、無駄だったんですかねぇ。

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