はじめに
- これは、OpenCV Advent Calendar 2018 ~~19日目の記事です。~~12日目の記事です。空いてるのが寂しかったので前倒しで公開することにしました。
- 関連記事は目次にまとめられています。
- なお、本記事は筆者個人の意見であり、筆者の所属組織とは無関係です。
- 先日、こんなことがありました。
文脈全く理解してないですけど、config.h みたいなのを作るのならば add_custom_command じゃなくて configure_file がおすすめですよ。 https://t.co/YWYGu9h5NT
— Tomoaki Teshima (@tomoaki_teshima) 2018年10月2日
TL;DR
- OpenCVはビルド時にCMakeが使われるわけですが、様々なプラットフォームでトラブルなくビルドするためにあんな技やこんな技が使われていますよ、というお話。
cmakeのドキュメントとしてのOpenCVの可能性を感じる。
— 熊樹もしくはkumagi (@kumagi) 2018年10月2日
- @kumagi先生のおっしゃるように、OpenCVを読み解くとCMakeの使い方がわかる、って気もします。
cvconfig.h
- 本題のkumagi先生が言っていた、config.hというのは、要は下記みたいなヘッダのことだと思っています。
#define HAVE_CAMERA_LIBRARY
#define HAVE_MATRIX_LIBRARY
#define HAVE_ETHER_GRAB_LIBRARY
//#define HAVE_EIGEN
- このような
config.h
があったとして、全てのソースコードからこいつをinclude
すれば、特定の機能をON/OFFすることが容易になります。 - 私も学生時代、個人でソースコードを管理してましたが、当時の所属研究室ではマシンは共用で、日によって席を変えてビルドすることもあり、そうすると
- 「このマシンにはこのライブラリが入ってない」
- 「このマシンにはこのグラバのドライバが入ってない」
- などの細かい差異でビルドができたりできなかったりしました。
- なので前述の
config.h
みたいなのを用意してビルドできない問題を回避したりしてました。 - このアプローチ自体はよくある話だと思いますし、この記事を読んでる方も「あるある」と頷いている人が多いのではないでしょうか。
- 問題は、この
config.h
をどうやってメンテナンスするのか、ということです。 - 前述の
config.h
は割と短いので、メンテナンスは大したこと無いですが、OpenCVみたいなライブラリを、どこでもオートマチックにビルドするためには、メンテナンスが大変です。
そこでCMakeですよ
-
CMakeには、このようなファイルを自動生成するための、
configure_file
コマンドがあります。 -
このコマンドを使うと、CMakeの変数をC/C++の形に編集してくれるのです。
-
実際の例を見てみましょう。OpenCVでは、以下のような
cvconfig.h
があります。当然中身は環境によって異なります。
#ifndef OPENCV_CVCONFIG_H_INCLUDED
#define OPENCV_CVCONFIG_H_INCLUDED
/* OpenCV compiled as static or dynamic libs */
#define BUILD_SHARED_LIBS
/* OpenCV intrinsics optimized code */
#define CV_ENABLE_INTRINSICS
/* OpenCV additional optimized code */
/* #undef CV_DISABLE_OPTIMIZATION */
/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN " 61"
/* OpenCL Support */
#define HAVE_OPENCL
/* #undef HAVE_OPENCL_STATIC */
/* #undef HAVE_OPENCL_SVM */
/* OpenEXR codec */
#define HAVE_OPENEXR
- 当たり前ですが、このファイルはバージョン管理されていません。ですので、リポジトリには
cvconfig.h
は存在しません。 - その代り、cmake実行完了時点で自動生成されます。
-
cvconfig.h
は存在しないと書きましたが、自動生成される前の「ファイルのもと」は存在していて、バージョン管理もされています。 cmake/templates/cvconfig.h.in (OpenCV 4.0.0版) がそうです。 - ちょっと覗いてみましょう。
#ifndef OPENCV_CVCONFIG_H_INCLUDED
#define OPENCV_CVCONFIG_H_INCLUDED
/* OpenCV compiled as static or dynamic libs */
#cmakedefine BUILD_SHARED_LIBS
/* OpenCV intrinsics optimized code */
#cmakedefine CV_ENABLE_INTRINSICS
/* OpenCV additional optimized code */
#cmakedefine CV_DISABLE_OPTIMIZATION
/* Compile for 'real' NVIDIA GPU architectures */
#define CUDA_ARCH_BIN "${OPENCV_CUDA_ARCH_BIN}"
/* OpenCL Support */
#cmakedefine HAVE_OPENCL
#cmakedefine HAVE_OPENCL_STATIC
#cmakedefine HAVE_OPENCL_SVM
/* OpenEXR codec */
#cmakedefine HAVE_OPENEXR
- 先程の
cvconfig.h
と見比べると、似ていますが、微妙に違います。#cmakedefine
という、一見define
に見えるコマンドがずらりと並んでいます。そして、その右側にあるのはCMakeでも使われている変数があります。 - このファイルを生成しているのはここです。
# platform-specific config file
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/cvconfig.h")
configure_file("${OpenCV_SOURCE_DIR}/cmake/templates/cvconfig.h.in" "${OPENCV_CONFIG_FILE_INCLUDE_DIR}/opencv2/cvconfig.h")
- このファイルでは、
configure_file
コマンドを呼び、その引数としてcvconfig.h.in
(先程見た「ファイルのもと」)と、config.h
の出力先の2つを指定しています。 - こうすることで、CMake内の変数をもとに、
define
が有効になったり無効になったファイルができあがる訳です。 - これにより、
cvconfig.h
を管理しないでも、自動生成できます。かなり便利な機能ですので是非皆様も使ってみて下さい。
config.h を自作するなら、CMakeのconfigure_fileコマンドを使おう
inline と __inline__ と __inline
https://github.com/opencv/opencv/blob/master/3rdparty/libtiff/CMakeLists.txt#L39
foreach(inline_keyword "inline" "__inline__" "__inline")
- C++の予約語の一つ
inline
関数はご存知でしょうか。関数呼び出しでなく、関数自体は呼び元に埋め込むことで、主に実行速度の高速化に寄与するためのオプションです。 - 実際にインライン展開(埋め込み)が行われるかどうかはコンパイラが判断するのですが、そのコンパイラのキーワードが実は決まって無い問題があるのです。
- 前述の通りC++では
inline
というキーワードが決まっていますが、古いCコンパイラの規約にはこのinlineは無いのです。1。 - で、コンパイラによってはこの
inline
キーワードを独自に対応していたりします。規約で決まっていないため、コンパイラによって、inline
だったり__inline__
だったり__inline
だったりします。 - こういうコンパイラ依存の機能を使う場合、よくある対処法はコンパイラごとに
ifdef
で区切るアプローチです
#if defined __GNUC__
#define FOO Bar
#elif defined _MSC_VER
#define FOO foo
#else
#define FOO
#endif
-
ifdef __GNUC___
みたいなプリプロセッサで切り分けることが行われます。が、OpenCVでは2一味違ったアプローチが行われます。 - OpenCV内の
3rdparty
以下にあるlibtiff
内にはCのソースコードが含まれており、そこではinline
関数が宣言されています。 - そして、その時の識別子は
inline
です。
inline static int32
find0span(unsigned char* bp, int32 bs, int32 be)
- これだとGCCでは識別子が
inline
なので良いのですが、Visual Studio 2013とそれ以前では識別子が__inline
のため、このままだとコンパイルエラーになる(はず)です。 - ところが実際にはなりません。
- 実際にはこのファイルからインクルードされる
tif_config.h
に以下のような記述があります。なお、このtif_config.h
はCMake時に自動生成されるファイルです。
/* Define to `__inline__' or `__inline' if that's what the C compiler
calls it, or to nothing if 'inline' is not supported under any name. */
#ifndef __cplusplus
#define inline inline
#endif
- では、Visual Studio 2013ではどうなるのか、実際に見てみましょう。
/* Define to `__inline__' or `__inline' if that's what the C compiler
calls it, or to nothing if 'inline' is not supported under any name. */
#ifndef __cplusplus
#define inline __inline
#endif
- ここで、
define
を使うことでinline
識別子が__inline
識別子に置き換えられます。 - どこでこの
define
が発生するのかと言うと、libtiff
内のCMakeLists.txt
にこのからくりの本質があります。
foreach(inline_keyword "inline" "__inline__" "__inline")
if(NOT DEFINED C_INLINE)
set(CMAKE_REQUIRED_DEFINITIONS_SAVE ${CMAKE_REQUIRED_DEFINITIONS})
set(CMAKE_REQUIRED_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS}
"-Dinline=${inline_keyword}")
check_c_source_compiles("
typedef int foo_t;
static inline foo_t static_foo() {return 0;}
foo_t foo(){return 0;}
int main(int argc, char *argv[]) {return 0;}"
C_HAS_${inline_keyword})
- キーポイントは、1行目に列挙されてる
inline
,__inline__
,__inline
識別子と、check_c_source_compiles
コマンドです。 - このコマンドは、ソースコードとオプションを渡し、実際にコンパイラでコンパイルした成否の結果を返す関数です。
- これにより、CMake時点でコンパイラがどの識別子を使うのか、はたまた
inline
関数非対応なのかを知ることができます。 - 前述の通り、コード自体はスッキリしたまま、ゴテゴテした
ifdef
を隠せるのは便利だと思います。
check_c_source_compiles を活用してifdefを減らそう!
ビルド時ダウンロード
- 状況は様々だと思いますが、「このバイナリはダウンロードして配置するだけ」なバイナリってよくあると思います。
- プログラムに埋め込むアイコンだけだったら良いのですが、往々にしてこういうバイナリをリポジトリに追加すると雪だるま式にみんな追加し始めて手に負えなくなります。
Gitにバイナリ、ダメ絶対。
- とは言え、手間を考えると、バイナリをリポジトリに追加したくなるのも人情です。3
- 手間を考えるとリポジトリにバイナリを追加したいし、リポジトリのサイズを考えるとこのバイナリはリポジトリに入れたくない。
- こんなジレンマにハマったときは、個人的にOpenCVでどう対処してるかを参考にしています。
- で、OpenCVでも実は
IPP
とかWindows用ffmpeg
とか、バイナリをビルド時に利用しています。 - では、このバイナリはどこから来てるのか?
- 結論から言えば、OpenCVではバイナリ格納専用リポジトリを用意して、そこにはどんどんバイナリファイルがpushされています。そのリポジトリがopencv_3rdpartyです。4
- しかし、皆さん
opencv_3rdparty
の存在をご存知なかったように、ここから手動でビルド済みバイナリをダウンロードされた方はまずいないと思います。 - でも現にOpenCVではビルド時に
IPP
やffmpeg
などのビルド済みバイナリをダウンロードしてきます。では、どうやってダウンロードを実現しているのか?
はい、CMakeでやっています。
- 具体的には
cmake/OpenCVDownload.cmake
に記述があります。
# Download and optionally unpack a file
#
# ocv_download(FILENAME p HASH h URL u1 [u2 ...] DESTINATION_DIR d [ID id] [STATUS s] [UNPACK] [RELATIVE_URL])
- この
ocv_download
関数内ではfile
コマンドを呼んでいます
file(DOWNLOAD "${DL_URL}" "${CACHE_CANDIDATE}"
INACTIVITY_TIMEOUT 60
TIMEOUT 600
STATUS status
LOG __log)
- 具体的には、
file
コマンドのDOWNLOAD
オプションを利用していて、これにはURL、ダウンロード用ディレクトリ、MD5ハッシュ値なんかを渡すと、
- 指定URLから指定したディレクトリにダウンロード
- MD5ハッシュ値を計算してバイナリを検証
- ファイルを展開
- ぐらいまで自動でやってくれます。
- なおOpenCVでは後方互換5のために、このあたりの機能は利用しておらず、自前で関数に組み込んでいます。しかし3.0以降のCMakeでは上記のオプションは全てよしなに取り扱ってくれます
- なお、似た機能ではgit submoduleがあります。
- これはあるリポジトリに、別のリポジトリの特定のコミットを結びつける方法で、複数のオープンソースプロジェクトを連携させるときに使ったりします。が、正直地獄を見ると思います。
- 個人的に
git submodule
は百害あって一利なしで、以下のsakanazensenさんのツイートに私も強く同意します。
なぜ人はgit submoduleに魅力を感じてしまうのか。あんなにつらさしかないのに。モテるダメ男とそんなのばっか捕まえる女のようだ。
— 光の魚 (@sakanazensen) 2018年2月21日
CMakeのFILEコマンドを使って便利に開発しましょう。
dispatch
- OpenCV 3.3から、dispatch機能が追加されました。特定の命令セットを使わないように、OpenCVのロード時(≠ビルド時)に指定できる機能で、古いプロセッサ用にAVX命令をdisableしてOpenCVをビルドし直し、みたいな悲劇がなくなりました。
- このdispatchはかなりCMakeの芸術的な出来になっているので、このdispatch機能がどうやって成り立っているか、
明日別記事で解説しようと思います。(日付変更に伴い文章変更)
おわりに
- OpenCVで使われていて、一般的にも使えそうなCMakeの便利機能を紹介しました。
configure_file
コマンドcheck_c_source_compiles
コマンドfile
コマンドのDOWNLOAD
オプション明日も私の予定で、体力が残ってればdispatchがどう実現されているか解説します。
-
C99から
inline
が規格に盛り込まれた模様 ↩ -
というか厳密には
libtiff
ライブラリ ↩ -
余談ですが、私は数十KBのパワーポイントファイルとPDFをリポジトリに追加するのに猛反発して、結果昨年のOpenCVアドベントカレンダーに寄稿した「 ビルドしただけでパワーポイントを生成するCMake」が誕生しました ↩
-
厳密に言えばopencv_extraリポジトリもバイナリがどんどんpushされてるので、バイナリ用のリポジトリが2つもあります。 ↩
-
OpenCV 4.0からCMakeの最低バージョンが3.5.1に引き上げられましたが、3.0系や2.4系では2.8.12.2が最低バージョンであり、そのとき使えなかったコマンドが独自コマンドで提供されています。 ↩