OpenCV 4.0 + CUDA付きでビルドすると、unresolved external "__declspec(dllimport) bool cv::__termination" と表示される

# はじめに
 - これは、OpenCV Advent Calendar 2018 4日目の記事です。
 - 関連記事は[目次](http://qiita.com/advent-calendar/2018/opencv)にまとめられています。
 - [昨日に続き](https://qiita.com/tomoaki_teshima/items/a395703284d30fa6e660)手島が担当します。
 - なお、本記事は筆者個人の意見であり、筆者の所属組織とは無関係です。

# TL;DR
 - ~~書きかけで公開します。~~ 修正しました。
 - OpenCVには長らくプログラム終了時に```UMat```の二重解放問題が潜んでいて、OpenCV 4.0で正式に対応が入ったが、そのせいで特定条件下でCUDA付きビルドがコケることになった。
 - 具体的にはWindowsでCUDA付きでビルドしようとすると、```unresolved external "__declspec(dllimport) bool cv::__termination"```が発生する
 - 次リリースでは直されるが、それまでは cmake時に```-DOPENCV_SKIP_DLLMAIN_GENERATION=ON``` をつける
 - (2018年12月26日追記)OpenCV 4.0.1がリリースされましたので、そちらでは本件修正されております


# OpenCV 4.0 のリリース
OpenCV 4.0 のリリースで、個人的に最大の変更点はCUDA関連のモジュールがopencv_contribに移動になったことです。(なお、繰り返しになりますが、あくまで本記事は筆者個人の意見であり、筆者の所属組織とは無関係です)
OpenCV 4.0 をソースからCUDA付きでWindows上でビルドしようとすると、重大な罠が潜んでいますので、そちらの紹介を。

# そもそも何が起きるのか
プログラムの終了時に二重解放による実行時エラーが発生するのですが、終了間際であること、確実に発生するわけではないこと、などの理由により、普段は気づかないと思います。

## 始まりのものがたり
 - 時系列的には5年前に既に[終了時の問題が提起されていました](https://github.com/opencv/opencv/pull/1490)
 - [Proposals for OpenCV_Init()/OpenCV_Shutdown() by alalek · Pull Request #1490 · opencv/opencv](https://github.com/opencv/opencv/pull/1490)

## 手島が問題を踏み抜く
 - [imgproc: program crashes after test of imgproc &quot;OCL_ImgProc/CvtColor8u32f&quot; · Issue #12750 · opencv/opencv](https://github.com/opencv/opencv/issues/12750)

 - このissue、ちゃんと読めば分かるのですが、
   - opencv_test_imgprocを実行すると、何もFAILせずにプログラムがちゃんと終了する
   - しかし、CMake付属の```ctest```を使うと、```Segmentation Fault```と表示される
   - というちょっと見ただけは分かりづらい実行時エラーです。

```
    Start 9: opencv_test_imgproc
1/1 Test #9: opencv_test_imgproc ..............***Exception: SegFault137.81 sec
```

 - デバッガ付きで実行して初めて分かったのですが、終了時に確かに実行時エラーが起きるのです。
   - テストプログラム自体には何ら問題が無いため、一見エラーには気づきません
   - 通常の、デバッガなしで実行した場合は、実行時エラーがプログラムの終了がほぼ同時のため、見た目には実行時エラーに気づ くことはほとんどありません。
   - しかし、ctestから実行した場合、ctest側でsegmentation faultを検知できるので明示的に実行時エラーが表示される訳です。

## コメントをもらう
 - はじめは```static```を外せば発生が免れるので、「```static```を外す」PRを出しました
 - しかし、```static```に設定されている配列は、LUTを始めとする、定数の置き場でありました。
 - ```static```がついている場合は最初に計算されて保持されているわけですが、```static```でなくなると、関数呼び出し毎にLUTを構築することになり、LUTの意味を成しません。
 - また、OpenCV メイン開発者の一人である、alalek先生から以下のコメントをもらいました。

>Looks like you catched:
>
> - termination order fiasco: OpenCL context is destroyed / corrupted before allocated buffers.
> - or Win32 ExitProcess terrible behavior - it terminates all non-main threads unconditionally - this usually corrupt multi-threading processes / libraries (deadlock is one way).

 - Windows側の問題か、OpenCLプラットフォーム（ドライバ？）側の問題か分からないけれど、二重解放が発生しているよ、と教えてくれました。
 - OpenCL用の配列、```UMat```は、coreモジュールで管理されているのですが、```UMat```用のLUTは```imgproc```で管理されています。
 - ```imgproc```の終了処理と```core```モジュールの終了処理は順序が不定で、場合によっては両方のモジュールから解放が発生するが、2度目の解放では既に領域が存在しないため、エラーになる、と。
 - 余談ですがこのalalek先生、本当に何でも知っていて、PR出す度に私が成長することになるほど、圧倒的にいろんなことを知っている方です。[^1]

> @alalek, you really know everything.

[^1]: "Not everything, I know only what I know"って返してくれたらホチキスとか蟹とかに出会えそうでしたが、そういうリプライはありませんでした

## alalek先生が修正してくれる
 - 幾度かのやり取りの後、alalek先生自身が、[OpenCVの各モジュールにDllMainを追加するPR](https://github.com/opencv/opencv/pull/12791)を出しました。
 - [cmake: add DllMain() into each OpenCV DLL by alalek · Pull Request #12791 · opencv/opencv](https://github.com/opencv/opencv/pull/12791)
 - このPRのポイントは、
   - 終了処理を各モジュールごとに関数でまとめる
   - coreモジュールだけが参照していた```__termination```フラグをモジュール間で共有することで```UMat```の二重解放を回避する
 - 点にあります。
 - 見ると分かりますが、テンプレートファイル1つをCMakeで見事に操って、最低限の修正で各モジュールにDllMainを追加しています。
 - 余力があればどこかでOpenCVで使われている華麗なCMakeのテクニックを紹介したいと思います。

## が、結果としてWindows + CUDA ビルドがコケることに
 - [build failure (link) with cudev in 4.0.0-beta · Issue #12865 · opencv/opencv](https://github.com/opencv/opencv/issues/12865)
 - alalek先生の[PR#12791](https://github.com/opencv/opencv/pull/12791)により、私の[issue#12750](https://github.com/opencv/opencv/pull/12750) は無事解決されました。
 - が、ここに```cudev```モジュールが関係してきます。
 - [8日目の記事](https://qiita.com/tomoaki_teshima/items/2a85bc4de7a68e5b8268)に紹介しますが、```cudev```モジュールは```core```モジュールに依存していない、不思議なモジュールなのです。なので、```__termination```フラグを```cudev```モジュールから参照しようとしても、```core```モジュールに依存していないため、参照できません。
 - めちゃくちゃ不思議な関係なのですが、どうやらcudevモジュールとはそういうモジュールのようです。
 - で、```extern```付きで宣言したものの、実体とリンクできないため、表題のエラーが発生する訳です。

 ```cpp
 unresolved external "__declspec(dllimport) bool cv::__termination" in DllMain
 ```

## 4.0のリリースと修正
 - 早速、[修正がマージされる](https://github.com/opencv/opencv/pull/13209)(2018年11月26日)も、[それよりも前に4.0がリリース](https://github.com/opencv/opencv/releases/tag/4.0.0)(2018年11月18日)される事態に
 - [cmake: don&#39;t generate dllmain for cudev module by alalek · Pull Request #13209 · opencv/opencv](https://github.com/opencv/opencv/pull/13209)
 - (2018年12月26日追記)2018年12月22日(日本時間)にOpenCV 4.0.1がリリースされましたので、そちらを使えば本記事のエラーは発生しません。

# 迂回方法
 - (2018年12月26日追記)繰り返しになりますが、2018年12月22日(日本時間)にOpenCV 4.0.1がリリースされましたので、そちらを使えば以下迂回作業も不要です。
 - WindowsでCMakeする際に```OPENCV_SKIP_DLLMAIN_GENERATION=ON```を指定します。
 - Windowsではコマンドラインでcmakeを叩く人は少ないと思いますが、一応PATHを通したりフルパスを叩けば普通にcmakeが使えます。

```
cmake -DOPENCV_SKIP_DLLMAIN_GENERATION=ON ...
```
 - なお、この修正はLinuxでは必要ありません。
 - また、OpenCVをstaticライブラリとしてリンクした場合も本件は発生しません。
 - 各モジュール間で```__termination```を共有するのはWindowsでの実装なので、LinuxやMacではリンカエラーは起きません

# 参考
 - [既に3年前の記事](https://qiita.com/dandelion1124/items/ab6004058af8fd393674)で@dandelion1124 先生が```__termination```について解説されています。
 - [OpenCVの初期化処理，終了処理 - Qiita](https://qiita.com/dandelion1124/items/ab6004058af8fd393674) (OpenCV Advent Calendar 2015の @dandelion1124 先生の記事)

# 終わりに
 - とりあえず差し替えました。
 - 明日は @hon_no_mushi さんの投稿で[RISC-V](https://qiita.com/hon_no_mushi/items/b661d5624b0227a2b038)だとぉ！？楽しみですね！
 - 最後に、繰り返しになりますが、あくまで本記事は筆者個人の意見であり、筆者の所属組織とは無関係です