1. 目的
Linux および Windows 11 環境に HDF5 1.12.1 をインストールし、自分の C++ プログラムにリンクして使う。ビルドには CMake を使う。
HDF5 とは何か?という問いに対しては、Web 情報へのリンクを幾つか紹介して回答としておく:
2. なぜ C++ で HDF5 を使うのか?
理由は人それぞれだろうが、筆者の場合は無線機の組込DSPからベースバンド信号を PC に引き上げて解析したいからである。
38.4k symbol/sec で流れ込んでくる複素信号, 受信電力レベル, パイロット信号の同期状態など複数の時系列データを実機側の C++ プログラムで巨大な構造体として RAM に溜め、デバッガで PC にダンプする。これを Julia や Mathematica に移して解析したい。PC 側の C++ プログラムで同じ構造体を定義すれば、バイナリデータをロードしてその構造体型にキャストすることで実機のデータを分解できる。以前はデータの種類毎に個別のファイルに保存して Julia 等で個別に読み込んでいたが、メタデータのようなものが増えるに連れてファイル操作が煩雑になってくる。
そこで PC 側のプログラムで C++ の構造体から類似の HDF5 形式のデータに変換し、単一のファイルで完結させて扱いを楽にしようと思った次第である。
3. この記事を書く理由
Linux 環境で公式の説明に従いインストールまではできたが、利用時にリンクに失敗した。参照した説明は下記。
release_docs/USING_HDF5_CMake.txt
にある CMakeLists.txt
の例では find_package()
を用いてヘッダやライブラリを自動で認識してリンクする方法が紹介されているが、これがまともに機能しない。
他の利用者が残した情報も試したが、全て失敗した。参照した情報は下記。
結局、リンカのエラーメッセージや 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の場合
$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 の 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
7.1.1. 注意
ここでは HDF5 を Release
モードではなく RelWithDebInfo
でビルドする。なぜなら自分でプログラムを書いている最中はデバッグできなくては都合が悪いから。自分で書いた cpp ファイルをデバッグモードでコンパイルし、リリースモードの HDF5 ライブラリとリンクすることもできたが(Linux ではできたが、Windows では失敗した)、モードが異なるオブジェクトファイル同士をリンクする場合の懸念については例えば下記で述べられている。
リリースモードに比べて速度は劣るが、デバッグ不可よりはずっとマシである。どうしてもリリースモードにしたくなったら、リリースモードでビルドした HDF5 デバッグモードのそれとは別のディレクトリに配置し、リンク先を切り替えて実行ファイルを作成する手がある。
7.1.2. 手順
-
https://www.hdfgroup.org/downloads/hdf5/source-code/ の CMake Versions から
CMake-hdf5-1.12.1.tar.gz
をダウンロードし、展開する。 -
展開してできるフォルダ
CMake-hdf5-1.12.1
に移動する。 -
環境変数
CC
,CXX
を設定する。筆者はCC=clang
,CXX=clang++
とした。 -
build-unix.sh
を編集する:-
C
オプションをRelWithDebInfo
にする(理由は既に述べた)。 -
INSTALLDIR
を設定する。筆者はWebで見た慣例に従い/usr/local/hdf5-1.12.1
とした。このオプションはBUILD_GENERATOR
オプションの直後に,
で区切って追加すればよい(但しスペースを挿入してはならない!)。
-
-
build-unix.sh
を実行する。ビルド完了後に自動テストが実行される。筆者の環境では100%通過した。 -
CMake-hdf5-1.12.1/build
に移動し、sudo make install
を実行する。HDF5 本体がインストールされる。 -
zlib のインストール:
CMake-hdf5-1.12.1/build/ZLIB-prefix/src/ZLIB-build
(v1.12.2, v1.13.1 の場合はZLIB
をHDF5_ZLIB
と読み替え) に移動し、sudo make install
を実行する。 -
szlibのインストール:
CMake-hdf5-1.12.1/build/SZIP-prefix/src/SZIP-build
に移動し、sudo make install
を実行する。 -
(任意) HDF5用コマンドラインツールのパスを通す。 但し、既に Anaconda をインストールしている環境ではそちらで導入されている HDF5 に含まれる
h5dump
等のコマンドのパスが通っている場合がある。 そちらのバージョンがこの記事でインストールするバージョンとさほど離れていない場合、敢えて今インストールしたものでパスを置き換える必要はないと思う。よって下記の手順はスキップすればよい。
パスを通す場合は、シェルの初期化ファイル (Ubuntu なら~/.bashrc
や~/.profile
等) に下記を追加する。# 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. 手順
-
https://www.hdfgroup.org/downloads/hdf5/source-code/ の CMake Versions から
CMake-hdf5-1.12.1.zip
をダウンロードし、展開する。 -
展開してできるフォルダ
CMake-hdf5-1.12.1
に移動する。 -
build-VS2019-64.bat
を編集し、C
オプションをDebug
にする。 -
build-VS2019-64.bat
を実行する。
ビルド完了後に自動テストが実行される。筆者の環境では2件失敗した。The following tests FAILED: 384 - H5CLEAR_CMP-h5clr_mdc_image_m (Failed) 1726 - H5DUMP-tfloatsattrs (Failed)
- テスト 384 はライブラリ本体ではなくツール
h5clear-shared.exe
の問題である。 - テスト 1726 も同様にライブラリ本体ではなくツール
h5dump.exe
の問題である。下記に情報があるが、浮動小数点数の丸めの挙動がプラットフォーム毎に異なることに起因するらしい。
- テスト 384 はライブラリ本体ではなくツール
-
CMake-hdf5-1.12.1/build
ディレクトリにHDF5-1.12.1-win64.msi
が作成されているので、これを実行してインストールする。デフォルト設定でC:/Program Files/HDF_Group/HDF5/1.12.1
にインストールされる。
8. HDF5を使うプログラムのビルド
下記の非常に単純なプログラムを動かすことを目標にする。
#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 で公開した。
ブランチは 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 のビルドモードと一致させることを強く推奨する。その理由は「注意」で述べている。
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
。「既知の問題」でも述べたように、dllImport
は使えない。
8.4. ハマった箇所と回避策
「この記事を書く理由」でも述べたが、公式の資料 release_docs/USING_HDF5_CMake.txt
にある CMakeLists.txt
の例では find_package()
が用いられているが、これが機能しない。リンカの引数を表示させて判ったが、C++ 用のライブラリや zlib, szlib, aec のライブラリが列挙されない。C 用のライブラリは列挙されていた。そこで、CMake スクリプトの中で全列挙して対処している。
9. 動作確認
実用を意識した動作確認を行う。下記の関数に基づいて点群を生成して Julia で読み込んで可視化してみる。
$$
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 利用の良いサンプルコードが公式ページにある。
今回用いるソースコードは下記。前述の GitHub リポジトリ HDF5_cpp_build_example に含まれている。src/CMakeLists.txt
中の set(EXE_FILE_TITLE ...)
の ...
を hdf5_my_example3
に設定するとこのソースファイルがビルド対象になる。
#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 はそもそも考慮されていないように見受けられる。
そこで仕方なく、行列が属するグループ内に metadata グループを作り、その中に上述のスカラーを DataSet として個別に保存した。
本来の HDF5 の慣例通り Attribute を利用するバージョンのソースファイルは hdf5_my_example2.cpp
として残してあるが、後述の src/view.ipynb
では扱えない。
上記 hdf5_my_example3.cpp
のコードで生成した HDF5 ファイルの構造を h5dump
コマンドで表示すると次のような出力を得る。
$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
として用意した。実行すると次の図を得る。