C++でHDF5を使う手順

## 1. 目的

Linux および Windows 11 環境に HDF5 1.12.1 をインストールし、自分の C++ プログラムにリンクして使う。ビルドには CMake を使う。

HDF5 とは何か？という問いに対しては、Web 情報へのリンクを幾つか紹介して回答としておく:

* [Hierarchical Data Format](https://ja.wikipedia.org/wiki/Hierarchical_Data_Format)
* [THE HDF5® LIBRARY & FILE FORMAT](https://www.hdfgroup.org/solutions/hdf5/)
* [INTRODUCTION TO HDF5](https://portal.hdfgroup.org/display/HDF5/Introduction+to+HDF5)

## 2. なぜ C++ で HDF5 を使うのか？

理由は人それぞれだろうが、筆者の場合は無線機の組込DSPからベースバンド信号を PC に引き上げて解析したいからである。

38.4k symbol/sec で流れ込んでくる複素信号, 受信電力レベル, パイロット信号の同期状態など複数の時系列データを実機側の C++ プログラムで巨大な構造体として RAM に溜め、デバッガで PC にダンプする。これを Julia や Mathematica に移して解析したい。PC 側の C++ プログラムで同じ構造体を定義すれば、バイナリデータをロードしてその構造体型にキャストすることで実機のデータを分解できる。以前はデータの種類毎に個別のファイルに保存して Julia 等で個別に読み込んでいたが、メタデータのようなものが増えるに連れてファイル操作が煩雑になってくる。

そこで PC 側のプログラムで C++ の構造体から類似の HDF5 形式のデータに変換し、単一のファイルで完結させて扱いを楽にしようと思った次第である。

## 3. この記事を書く理由

Linux 環境で公式の説明に従いインストールまではできたが、利用時にリンクに失敗した。参照した説明は下記。

* [How to Change HDF5 CMake Build Options](https://portal.hdfgroup.org/display/support/How+to+Change+HDF5+CMake+Build+Options)

`release_docs/USING_HDF5_CMake.txt` にある `CMakeLists.txt` の例では `find_package()` を用いてヘッダやライブラリを自動で認識してリンクする方法が紹介されているが、これがまともに機能しない。

他の利用者が残した情報も試したが、全て失敗した。参照した情報は下記。

* [HDF5_setup (hysk)](http://www.hysk.sakura.ne.jp/Linux_tips/HDF5_setup)
* [HDF5ライブラリをUNIXにインストール](http://www.kotsuki-shunji.com/intro/memo_hdf5.html)

結局、リンカのエラーメッセージや Web 情報と格闘・試行錯誤して解決できた。これから C++ で HDF5 を使いたい人がハマらず済むように、上手く行った方法を公開する。

## 4. 対象とする HDF5 のバージョン

本記事が対象とする HDF5 のバージョンは下記の通り。

* 1.12.1: Linux, Windows 10 両方OK
* 1.12.2: Linux -> OK, Windows 10 -> NG(ビルド後のテストケースで fail 頻発)
* 1.13.1、1.13.2: 1.12.2 と同じ状況

以下 1.12.1 想定で記述するが、特別に記載のない限りバージョンを読み替えることで 1.12.2, 1.13.X にも対応する。バージョンによる差異がある項目は個別に記述する。

## 5. 既知の問題

Windows については静的リンクはできたが、動的リンクできなかった。実行ファイル生成までは完了するが、起動しても `main` 関数到達前に落ちる。

## 6. 筆者が試した環境

### 6.1. Linuxの場合

```bash
$arch
x86_64

$lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description:    Pengwin
Release:        11
Codename:       bullseye

$clang --version
Debian clang version 11.0.1-2
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

$cmake --version
cmake version 3.18.4
```

### 6.2. Windowsの場合

* 命令セット: x86-64
* OS: Windows 11 64bit
* [https://portal.hdfgroup.org/display/support/Building+HDF5+with+CMake](https://portal.hdfgroup.org/display/support/Building+HDF5+with+CMake) の Preconditions に書かれている通りだが、
  * CMake 3.15 以上
  * Visual Studio 2019。Visual Studio Installer で 「C/C++ によるデスクトップ開発」をインストール済み。
  * WiX。HDF5 のビルド時に msi 形式インストーラを作成するのに必要。筆者が使ったのは v3.11.2。

## 7. HDF5のインストール

### 7.1. Linuxの場合

参考URL: [https://portal.hdfgroup.org/display/support/How+to+Change+HDF5+CMake+Build+Options](https://portal.hdfgroup.org/display/support/How+to+Change+HDF5+CMake+Build+Options)

#### 7.1.1. 注意

ここでは HDF5 を `Release` モードではなく `RelWithDebInfo` でビルドする。なぜなら自分でプログラムを書いている最中はデバッグできなくては都合が悪いから。自分で書いた cpp ファイルをデバッグモードでコンパイルし、リリースモードの HDF5 ライブラリとリンクすることもできたが(Linux ではできたが、Windows では失敗した)、モードが異なるオブジェクトファイル同士をリンクする場合の懸念については例えば下記で述べられている。

* [Mixing debug and release library/binary - bad practice?](https://stackoverflow.com/questions/11658915/mixing-debug-and-release-library-binary-bad-practice?rq=1)

リリースモードに比べて速度は劣るが、デバッグ不可よりはずっとマシである。どうしてもリリースモードにしたくなったら、リリースモードでビルドした HDF5 デバッグモードのそれとは別のディレクトリに配置し、リンク先を切り替えて実行ファイルを作成する手がある。

#### 7.1.2. 手順

1. [https://www.hdfgroup.org/downloads/hdf5/source-code/](https://www.hdfgroup.org/downloads/hdf5/source-code/) の CMake Versions から `CMake-hdf5-1.12.1.tar.gz` をダウンロードし、展開する。
2. 展開してできるフォルダ `CMake-hdf5-1.12.1` に移動する。
3. 環境変数 `CC`, `CXX` を設定する。筆者は `CC=clang`, `CXX=clang++` とした。
4. `build-unix.sh` を編集する:
   1. `C` オプションを `RelWithDebInfo` にする(理由は既に述べた)。
   2. `INSTALLDIR` を設定する。筆者はWebで見た慣例に従い `/usr/local/hdf5-1.12.1` とした。このオプションは `BUILD_GENERATOR` オプションの直後に `,` で区切って追加すればよい(但しスペースを挿入してはならない!)。
5. `build-unix.sh` を実行する。ビルド完了後に自動テストが実行される。筆者の環境では100%通過した。
6. `CMake-hdf5-1.12.1/build` に移動し、`sudo make install` を実行する。HDF5 本体がインストールされる。
7. zlib のインストール: `CMake-hdf5-1.12.1/build/ZLIB-prefix/src/ZLIB-build` (v1.12.2, v1.13.1 の場合は `ZLIB` を `HDF5_ZLIB` と読み替え) に移動し、`sudo make install` を実行する。
8. szlibのインストール: `CMake-hdf5-1.12.1/build/SZIP-prefix/src/SZIP-build` に移動し、`sudo make install` を実行する。
9. (任意) HDF5用コマンドラインツールのパスを通す。 **但し、既に Anaconda をインストールしている環境ではそちらで導入されている HDF5 に含まれる `h5dump` 等のコマンドのパスが通っている場合がある。** そちらのバージョンがこの記事でインストールするバージョンとさほど離れていない場合、敢えて今インストールしたものでパスを置き換える必要はないと思う。よって下記の手順はスキップすればよい。
  パスを通す場合は、シェルの初期化ファイル (Ubuntu なら `~/.bashrc` や `~/.profile` 等) に下記を追加する。

    ```bash
    # HDF5 tools
    PATH="${PATH}:/usr/local/hdf5-1.12.1/bin"
    ```

    これで `h5dump` 等の便利なコマンドが使えるようになる。

### 7.2. Windowsの場合

ここでは HDF5 を `Release` モードではなく `Debug` モードでビルドする。動機は Linux 環境でのインストールの説明で述べていることと同じである。ではなぜ `RelWithDebInfo` でないのか？もちろんそれは試したが、失敗した。自分のC++プログラムをデバッグモードでコンパイルしつつ `RelWithDebInfo` の　HDF5 ライブラリとリンクすることはエラーが生じて不可能だった。Linux と Windows では制約が異なるようである。

#### 7.2.1. 手順

1. [https://www.hdfgroup.org/downloads/hdf5/source-code/](https://www.hdfgroup.org/downloads/hdf5/source-code/) の CMake Versions から `CMake-hdf5-1.12.1.zip` をダウンロードし、展開する。
2. 展開してできるフォルダ `CMake-hdf5-1.12.1` に移動する。
3. `build-VS2019-64.bat` を編集し、`C` オプションを `Debug` にする。
4. `build-VS2019-64.bat` を実行する。
   ビルド完了後に自動テストが実行される。筆者の環境では2件失敗した。

   ```txt
   The following tests FAILED:
   384 - H5CLEAR_CMP-h5clr_mdc_image_m (Failed)
   1726 - H5DUMP-tfloatsattrs (Failed)
   ```

   * テスト 384 はライブラリ本体ではなくツール `h5clear-shared.exe` の問題である。
   * テスト 1726 も同様にライブラリ本体ではなくツール `h5dump.exe` の問題である。下記に情報があるが、浮動小数点数の丸めの挙動がプラットフォーム毎に異なることに起因するらしい。
     * [h5dump test failed](https://forum.hdfgroup.org/t/h5dump-test-failed/8762)

5. `CMake-hdf5-1.12.1/build` ディレクトリに `HDF5-1.12.1-win64.msi` が作成されているので、これを実行してインストールする。デフォルト設定で `C:/Program Files/HDF_Group/HDF5/1.12.1` にインストールされる。

## 8. HDF5を使うプログラムのビルド

下記の非常に単純なプログラムを動かすことを目標にする。

```C++:hdf5_my_example.cpp
#include <cstdio>
#include <cstdlib>

//#define H5_BUILT_AS_DYNAMIC_LIB 1 // In Windows, turn on this when using hdf5 as a shared library.
#include <H5Cpp.h>

int main() {
    printf("Hello, World!\n");
    const std::string fileName("test.h5");
    H5::H5File file(fileName, H5F_ACC_TRUNC);
    printf("File creation is done.\n");
    return EXIT_SUCCESS;
}
```

このサンプルプログラム, ビルド用の Cmake 設定ファイル, スクリプトを GitHub で公開した。

[HDF5_cpp_build_example](https://github.com/motchy869/HDF5_cpp_build_example)

ブランチは `for_HDF-1.12.1` を選択する(v1.12.2, v1.13.1 は適宜別ブランチを選択する)。Linux の場合は `scripts/doDebugBuild.sh` を、Windows の場合は `scripts/doDebugBuild.ps1` を実行するとビルド開始する。

以下で Linux, Windows 共通および個別の主要な調整箇所を説明する。CMake に慣れている人なら説明を読まずとも、都合良く書き換えられると思う。

### 8.1. 共通の調整箇所

`src/CMakeLists.txt` にて下記を調整できる。

* C, C++ 標準 `CMAKE_C_STANDARD`, `CMAKE_CXX_STANDARD`
* プロジェクト名。例では `HDF5MyExample` としている。

### 8.2. Linuxの調整箇所

* `scripts/doDebugBuild.sh` にてビルドモードを `Debug` または `Release` から選択できる。HDF5 のビルドモードと一致させることを強く推奨する。その理由は「[注意](#611-注意)」で述べている。

`src/CMakeLists_Linux.cmake` にて下記を調整できる。

* HDF5 をインストールしたディレクトリ `HDF5_ROOT_DIR`
* ライブラリのリンク形式 `HDF5_LIB_LINK_TYPE`。`static` または `shared`。筆者の環境では静的リンクは実行ファイルサイズが 20MiB を超えた。不都合が無ければ動的リンクを推奨する。

### 8.3. Windowsの調整箇所

* `scripts/doDebugBuild.ps1` にてビルドモードを `Debug` または `Release` から選択できる。注意点は Linux の場合と同様。

`src/CMakeLists_Windows.cmake` にて下記を調整できる。

* HDF5 をインストールしたディレクトリ `HDF5_ROOT_DIR`
* ライブラリのリンク形式 `HDF5_LIB_LINK_TYPE`。`static` または `dllImport`。「[既知の問題](#4-既知の問題)」でも述べたように、`dllImport` は使えない。

### 8.4. ハマった箇所と回避策

「[この記事を書く理由](#3-この記事を書く理由)」でも述べたが、公式の資料 `release_docs/USING_HDF5_CMake.txt` にある `CMakeLists.txt` の例では `find_package()` が用いられているが、これが機能しない。リンカの引数を表示させて判ったが、C++ 用のライブラリや zlib, szlib, aec のライブラリが列挙されない。C 用のライブラリは列挙されていた。そこで、CMake スクリプトの中で全列挙して対処している。

## 9. 動作確認

実用を意識した動作確認を行う。下記の関数に基づいて点群を生成して [Julia](https://ja.wikipedia.org/wiki/Julia_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0%E8%A8%80%E8%AA%9E)) で読み込んで可視化してみる。

$$
    f(x_1,x_2) = \mathop{\mathrm{sinc}}\left(\sqrt{{x_1}^2 + {x_2}^2}\right) \quad (x_1,x_2\in\mathbb{R})
$$

$x_1,x_2$の範囲を-10～10、ステップ幅を0.5とすると41×41の行列が得られる。

C++ による HDF5 利用の良いサンプルコードが公式ページにある。

* [EXAMPLES FROM LEARNING THE BASICS](https://portal.hdfgroup.org/display/HDF5/Examples+from+Learning+the+Basics)

今回用いるソースコードは下記。前述の GitHub リポジトリ [HDF5_cpp_build_example](https://github.com/motchy869/HDF5_cpp_build_example) に含まれている。`src/CMakeLists.txt` 中の `set(EXE_FILE_TITLE ...)` の `...` を `hdf5_my_example3` に設定するとこのソースファイルがビルド対象になる。

```C++:hdf5_my_example3.cpp
#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <type_traits>

//#define H5_BUILT_AS_DYNAMIC_LIB 1 // In Windows, turn on this when using hdf5 as a shared library.
#include <H5Cpp.h>

template<typename T>
T sinc(const T x);

template<typename T>
T mathFunc_f(const T x1, const T x2);

int main() {
    constexpr float x1_start = -10.0, x2_start = -10.0;
    constexpr float x1_end = 10.0, x2_end = 10.0;
    constexpr float x1_step = 0.5, x2_step = 0.5;
    constexpr int N1 = static_cast<int>((x1_end - x1_start)/x1_step + 0.5f) + 1;
    constexpr int N2 = static_cast<int>((x2_end - x2_start)/x2_step + 0.5f) + 1;

    const std::string fileName("test.h5");

    H5::H5File file(fileName, H5F_ACC_TRUNC);

    H5::FloatType datatype(H5::PredType::NATIVE_FLOAT);
    datatype.setOrder(H5T_ORDER_LE);

    H5::Group group_gridData(file.createGroup("gridData"));
    H5::Group group_metaData(group_gridData.createGroup("metaData"));


    /* Set points. */
    {
        hsize_t dims[] = {N1, N2};
        H5::DataSpace dataSpace(2, dims);
        H5::DataSet dataSet(group_gridData.createDataSet("points", datatype, dataSpace));

        float dataSet_raw[N1][N2];

        for (int i=0; i<N1; ++i) {
            for (int j=0; j<N2; ++j) {
                dataSet_raw[i][j] = mathFunc_f(x1_start + i*x1_step, x2_start + j*x2_step);
            }
        }

        dataSet.write(dataSet_raw, datatype);
    }

    /* Set metadata. (actually, just a small data sets) */
    {
        hsize_t dim[] = {1};
        H5::DataSpace dataSpace(1, dim);

        const std::array<const char *, 4> metaDataNames({"x1_start", "x1_step", "x2_start", "x2_step"});
        const std::array<const float *, 4> metaDataPtrs({&x1_start, &x1_step, &x2_start, &x2_step});

        for (size_t i=0; i<metaDataNames.size(); ++i) {
            H5::DataSet dataSet(group_metaData.createDataSet(metaDataNames[i], datatype, dataSpace));
            dataSet.write(metaDataPtrs[i], datatype);
        }
    }

    printf("File creation is done.\n");
    return EXIT_SUCCESS;
}

/**
 * @brief sinc function.
 *
 * @tparam T the data type of input
 * @param[in] x input value
 * @return sinc(x): sin(x)/x for |x| >= 0.001, 1 for |x| < 0.001
 */
template<typename T>
T sinc(const T x) {
    static_assert(std::is_floating_point<T>::value, "T must be a floating-point type.");
    if (std::abs(x) < 0.001) {
        return 1.0f;
    }
    return std::sin(x) / x;
}

/**
 * @brief sinc(sqrt(x1^2+x2^2))
 *
 * @tparam T the data type of input
 * @param[in] x1 x1
 * @param[in] x2 x2
 * @return sinc(sqrt(x1^2+x2^2))
 */
template<typename T>
T mathFunc_f(const T x1, const T x2) {
    static_assert(std::is_floating_point<T>::value, "T must be a floating-point type.");
    return sinc(std::sqrt(x1*x1 + x2*x2));
}
```

`x1_start, x2_start, x1_step, x2_step` と行列があれば `x1_end, x2_end` は復元できるのでファイルに含める必要がない。普通ならこのような小さくて個数が多いものは Attribute として行列本体に付属させるのが慣例である。

しかし Julia の HDF5 ライブラリ `HDF5.jl` がどうやら Attribute に対応していないことが判った。Attribute 付きのデータを読み込ませると行列部分が一部欠損し全体の順番も狂った上、Attribute を取得すらできなかった。下記の公式ページを見ると、DataSet に付加された Attribute はそもそも考慮されていないように見受けられる。

* [HDF5.jl Creating groups and attributes](https://juliaio.github.io/HDF5.jl/stable/#Creating-groups-and-attributes)

そこで仕方なく、行列が属するグループ内に metadata グループを作り、その中に上述のスカラーを DataSet として個別に保存した。

本来の HDF5 の慣例通り Attribute を利用するバージョンのソースファイルは `hdf5_my_example2.cpp` として残してあるが、後述の `src/view.ipynb` では扱えない。

上記 `hdf5_my_example3.cpp` のコードで生成した HDF5 ファイルの構造を `h5dump` コマンドで表示すると次のような出力を得る。

```bash
$h5dump --header test.h5
HDF5 "test.h5" {
GROUP "/" {
   GROUP "gridData" {
      GROUP "metaData" {
         DATASET "x1_start" {
            DATATYPE  H5T_IEEE_F32LE
            DATASPACE  SIMPLE { ( 1 ) / ( 1 ) }
         }
         DATASET "x1_step" {
            DATATYPE  H5T_IEEE_F32LE
            DATASPACE  SIMPLE { ( 1 ) / ( 1 ) }
         }
         DATASET "x2_start" {
            DATATYPE  H5T_IEEE_F32LE
            DATASPACE  SIMPLE { ( 1 ) / ( 1 ) }
         }
         DATASET "x2_step" {
            DATATYPE  H5T_IEEE_F32LE
            DATASPACE  SIMPLE { ( 1 ) / ( 1 ) }
         }
      }
      DATASET "points" {
         DATATYPE  H5T_IEEE_F32LE
         DATASPACE  SIMPLE { ( 41, 41 ) / ( 41, 41 ) }
      }
   }
}
}
```

これを読み込んで可視化する Julia ノートブックを `src/view.ipynb` として用意した。実行すると次の図を得る。

![2022_4_17_2051_plot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/165331/0022a9ec-9b05-43ec-55d0-4747e297c14d.png)