HDF5をC++で使う：(その1）簡単な入出力

# 前書き
[HDF(Hierarchical Data Structure)](https://www.hdfgroup.org/)が何かという説明は省略します（ここにいらした方は既にご存じと思いますので）。ちゃんと知りたいという方は、上記本家ウェブページか[Wikipedia](https://ja.wikipedia.org/wiki/Hierarchical_Data_Format)をご参照下さい。

本記事ではHDF5を扱います。既に普及しているデータフォーマットなので関連記事はいろいろ見つかりますが、自分自身の整理のために書くことにしました。開発環境はUbuntu 22.04LTS、コーディング言語はC++、コンパイラはg++ 11.3.0です。

# ライブラリ＆ツール群のインストール

HDF5を扱うプログラム開発に必要な諸々は
```sh
% apt install libhdf5-dev hdf5-tools
```
でインストールできます。本記事を書いている時点で、libhdf5のバージョンは1.10.7です。

デフォルト設定で、ヘッダファイル群は /usr/include/hdf5/serial/下、ライブラリ群は /usr/lib/x86_64-linux-gnu/hdf5/serial/lib下にそれぞれ置かれます。C++で開発する場合、includeするヘッダファイルはH5Cpp.h、リンクするライブラリはlibhdf5とlibhdf5_cppの二つです。hdf5-toolsに含まれるシェルスクリプト h5c++ をコンパイラ替わりに使えばこの辺の設定は不要ですが、他のライブラリと組み合わせる際に不便なので知っておく方が良いと思います。本記事を書く際には、私は次のようなmakefileを使ってます。

```makefile
CXX=g++
INCLUDEDIR=-I/usr/include/hdf5/serial
LIBDIR=-L/usr/lib/x86_64-linux-gnu/hdf5/serial/lib
CXXFLAGS=-Wall -O2 $(INCLUDEDIR) $(LIBDIR)
LINK=-lhdf5_cpp -lhdf5

%: %.cpp
	$(CXX) $(CXXFLAGS) $< -o $@ $(LINK)
clean:
	rm -f *.o *~
```

# 簡単なデータ入出力プログラム

## データスペースとデータセット

HDF5ライブラリでは、データは**データスペース**と**データセット**の組で管理されます。ライブラリではそれぞれ`H5::DataSpace`、`H5::DataSet`というクラスで定義されています。

データスペースはデータの型と配列サイズの情報を持ち、データセットはデータ配列そのものを持ちます。したがって、
 - データ出力（書き出し）操作ではデータスペースを定義してからそれと整合するデータセットを作成し、ファイルにデータを転送する
 - データ入力（読み込み）操作ではファイルに保存されたデータセットをまず読み込み、それに埋め込まれたデータスペースを取得して、必要なメモリを用意しデータを転送する

という手順にそれぞれなります。

データスペースもデータセットもライブラリ操作のためのインタフェースであって、ファイルフォーマットと直接対応しているわけではないのでご注意下さい。

## データ書き出し

まずは簡単なデータwriterを作ります。
```c++
#include <iostream>
#include <H5Cpp.h>

#define FILENAME "test.h5"
#define DATASET_NAME "/dataset"

int main(int argc, char *argv[])
{
  H5::H5File file( FILENAME, H5F_ACC_TRUNC );

  hsize_t dims[] = { 4, 6 };
  H5::DataSpace dataspace( sizeof(dims) / sizeof(hsize_t), dims );

  H5::DataSet dataset = file.createDataSet( DATASET_NAME, H5::PredType::STD_I32LE, dataspace );
  int *dset_data = new int [dims[0]*dims[1]];
  if( !dset_data ) return false;
  for(int i=0; i<static_cast<int>(dims[0]); i++ ){
    for(int j=0; j<static_cast<int>(dims[1]); j++ )
      dset_data[i*dims[1]+j] = i * dims[1] + j;
  }
  dataset.write( dset_data, dataset.getDataType() );
  delete dset_data;
  dataset.close();

  file.close();
  return 0;
}
```

流れを一つ一つ追っていきます。まずは
```C++
  H5::H5File file( FILENAME, H5F_ACC_TRUNC );
```
で`H5::H5File`インスタンスを作り`FILENAME`を読み込んでいます。`H5F_ACC_TRUNC`は、もし`FILENAME`が存在していたときには上書きする、という指定になります。`FILENAME`が存在していたときにはエラーとする場合には、`H5F_ACC_EXCL`を第2引数に与えます。

なお、エラーは全て例外として発行されますので、適切に処理するにはtry ... catch を用いる必要があります。これは[(その2)](https://qiita.com/zhidao/items/6594b2cf5c8eff2c5dc4)で扱うことにします。

次に、データスペースを定義します。
```C++
  hsize_t dims[] = { 4, 6 };
  H5::DataSpace dataspace( sizeof(dims) / sizeof(hsize_t), dims );
```
`dims[]`により、4×6の2次元配列としてデータを保存する、ということを指定しています。コンストラクタの第1引数を`sizeof(dims) / sizeof(hsize_t)`とすることで、2次元配列であることを自動的に読み取っています。

そしてファイルに書き出すデータセットを
```C++
  H5::DataSet dataset = file.createDataSet( datasetname, H5::PredType::STD_I32LE, dataspace );
```
と作成します。
`datasetname`はデータセットの名前（文字列）です。HDFのデータセットは全て文字列でidentifyされます。頭の'/'は省略可能ですが、[(その2）](https://qiita.com/zhidao/items/6594b2cf5c8eff2c5dc4)で紹介するグルーピングを採用する際にバグの元になるので、慣習として付けることをお勧めします。
第2引数は配列の要素となるデータの型を指定するもので、`H5::DataType`のインスタンスを与えます。この例ではその子クラスである`H5::PredType`（PredはPredefinedの略）のstaticメンバ`STD_I32LE`を与えています（LEはLittle Endianの意）。
他にどのようなものがあるかは/usr/include/hdf5/serial/H5PredType.hを参照して下さい。例えば同じint型でも、CPUアーキテクチャによってビット幅とendianが違うので、作業が同一マシンで閉じているならば`H5::PredType::NATIVE_INT`を使うのが無難です。

続く2重forループで適当に作成したデータ配列`dset_data`を、write()メソッドで転送します。
```C++
  dataset.write( dset_data, dataset.getDataType() );
```
`H5::DataSet`のgetDataType()メソッドは、保存されているデータの型を返すものです。write()メソッドの第2引数になぜもう一度データ型を与えなければならないのかは不明です。

書き出しが終わったらデータセット、ファイルとも閉じましょう。
```C++
  dataset.close();
  file.close();
```
上記をコンパイルして、コマンドライン引数にファイル名（ここでは`test.h5`とします）を与えると、HDF5ファイルが作成されます。hdf5-toolsに含まれるh5dumpを使えば、次のように中身をコンソールに出力することができます。
```
% h5dump test.h5                  
HDF5 "test.h5" {
GROUP "/" {
   DATASET "dataset" {
      DATATYPE  H5T_STD_I32LE
      DATASPACE  SIMPLE { ( 4, 6 ) / ( 4, 6 ) }
      DATA {
      (0,0): 0, 1, 2, 3, 4, 5,
      (1,0): 6, 7, 8, 9, 10, 11,
      (2,0): 12, 13, 14, 15, 16, 17,
      (3,0): 18, 19, 20, 21, 22, 23
      }
   }
}
}
```

# データ読み込み

同様にデータreaderも作ってみます。
```C++
#include <iostream>
#include <H5Cpp.h>

#define DATASET_NAME "dataset"

int main(int argc, char *argv[])
{
  if( argc < 2 ){
    std::cerr << "input file name." << std::endl;
    return 1;
  }
  H5::H5File file( argv[1], H5F_ACC_RDONLY );
  H5::DataSet dataset = file.openDataSet( argc > 2 ? argv[2] : DATASET_NAME );
  file.close();

  H5::DataSpace dataspace = dataset.getSpace();
  hsize_t dims[dataspace.getSimpleExtentNdims()];
  dataspace.getSimpleExtentDims( dims );

  int *dset_data = new int [dims[0]*dims[1]];
  dataset.read( dset_data, dataset.getDataType() );
  for(int i=0; i<static_cast<int>(dims[0]); i++ ){
     for(int j=0; j<static_cast<int>(dims[1]); j++ )
        std::cout << ' ' << dset_data[i*dims[1]+j];
     std::cout << std::endl;
  }
  delete dset_data;
  dataset.close();
  return 0;
}
```
これも流れを一つ一つ追っていきましょう。まずは、
```C++
  if( argc < 2 ){
    std::cerr << "input file name." << std::endl;
    return 1;
  }
  H5::H5File file( argv[1], H5F_ACC_RDONLY );
```
として、コマンドライン第1引数で指定されたファイルを`H5F_ACC_RDONLY`（読み込みモード）で開きます。
読み書き可能にしたい場合は`H5F_ACC_RDWR`を第2引数に与えます。これらのファイルアクセスモードを表すマクロは/usr/include/hdf5/serial/H5Fpublic.hで定義されています。

次に
```C++
  H5::DataSet dataset = file.openDataSet( argc > 2 ? argv[2] : DATASET_NAME );
  file.close();
```
としてファイルに保存されたデータセットを取得しています。openDataSet()メソッドの引数にはデータセット名（文字列）を与えるのですが、ここではコマンドライン第2引数で与えるようにしています（与えられなかった場合、事前定義された"dataset"を使います）。
データセットを取得したらファイルはclose()してしまって構いません。

続いて
```C++
  H5::DataSpace dataspace = dataset.getSpace();
  hsize_t dims[dataspace.getSimpleExtentNdims()];
  dataspace.getSimpleExtentDims( dims );
```
とし、書き出し時とは逆にデータセットからgetSpace()メソッドを使ってデータスペースを取得しています。データスペースのgetSimpleExtentNdims()メソッドは、データ配列の次元数を返すメソッドです。これを使って`hsize_t`の配列を作成し、データスペースのgetSimpleExtendDims()メソッドによってその配列にデータ配列のサイズを抽出します。

得られたデータ配列サイズ情報に基づいてメモリを確保し、データセットのread()メソッドを使って
```C++
  int *dset_data = new int [dims[0]*dims[1]];
  dataset.read( dset_data, dataset.getDataType() );
```
のようにデータ取得しています。write()メソッドと同様に、read()メソッドも第2引数にデータ型を指定する必要がありますので、getDataType()メソッドを使ってこれを与えます。

その後2重forループで中身を出力した後で、
```C++
  dataset.close();
```
としてデータセットを閉じ、終了です。

先程作った`test.h5`を第1引数に与えて実行すれば、次の出力が得られます。
```
 0 1 2 3 4 5
 6 7 8 9 10 11
 12 13 14 15 16 17
 18 19 20 21 22 23
```