高速データ圧縮ライブラリ zlib 入門:詳細な解説と実践
はじめに:データ圧縮の重要性と zlib
現代のデジタル社会において、データは私たちの生活やビジネスの基盤となっています。しかし、そのデータ量は爆発的に増加しており、ストレージ容量やネットワーク帯域幅は常に貴重なリソースです。ここで重要となるのが「データ圧縮」技術です。データを小さくすることで、ストレージコストを削減し、ネットワーク転送時間を短縮し、限られた帯域幅を効率的に利用することができます。
データ圧縮技術には様々なアルゴリズムが存在しますが、その中でも非常に広く普及し、多くのソフトウェアやフォーマット(例:PNG画像、ZIPアーカイブ、gzipファイル、HTTP圧縮)で利用されているのが、DEFLATEアルゴリズムです。そして、このDEFLATEアルゴリズムを高速かつ効率的に実装したライブラリが zlib です。
zlib は、Mark Adler と Jean-loup Gailly によって開発され、非常に緩やかな zlib ライセンス(または zlib/libpng ライセンス)の下で提供されています。このライセンスは、ソフトウェアへの組み込みや利用に関して非常に制限が少なく、商用・非商用を問わず広く利用できるため、多くのプロジェクトで採用されています。
zlib は C 言語で記述されており、高い移植性を持っています。Windows、Linux、macOS など、様々なプラットフォームで利用可能です。シンプルながらも強力な API を提供し、メモリ上のデータだけでなく、ストリームとして渡される大量のデータも効率的に処理できます。
この記事では、高速データ圧縮ライブラリ zlib の基本的な概念から、実際の C 言語での使い方、さらには高度な機能やパフォーマンスに関する考慮事項まで、詳細かつ網羅的に解説します。この記事を通して、あなたが zlib を理解し、自身のプロジェクトで効果的に活用できるようになることを目指します。
データ圧縮の基礎:DEFLATEアルゴリズムの仕組み
zlib が核として利用しているのは DEFLATE と呼ばれる可逆圧縮アルゴリズムです。可逆圧縮とは、圧縮されたデータを完全に元の状態に戻すことができる圧縮方法です。損失がないため、テキストデータやプログラムコードなど、少しのビットの違いも許されないデータの圧縮に適しています。
DEFLATE は、以下の二つの技術を組み合わせて高い圧縮率と処理速度を実現しています。
- LZ77 アルゴリズムに基づく繰り返し文字列の検出
- ハフマン符号化による符号の割り当て
1. LZ77 アルゴリズム(Lempel-Ziv 1977)
LZ77 は、入力データ内の繰り返し出現する文字列を検出するアルゴリズムです。検出された文字列は、過去に出現した同じ文字列への「参照」に置き換えられます。この参照は通常、「距離」(現在の位置から過去の出現位置までの距離)と「長さ」(一致した文字列の長さ)のペアで表現されます。
例えば、「ABCA BC ABC」という文字列があったとします。
* 最初の「ABCA」はまだ履歴にないのでそのまま出力されます(または特殊な符号化)。
* 次に「BC」が出てきます。これは直前の「ABCA」の「BC」と一致します。この「BC」は、現在の位置から3文字前の位置から始まり、長さは2文字です。LZ77では、この「BC」を「(距離=3, 長さ=2)」のような参照に置き換えます。
* 次に「ABC」が出てきます。これは最初に出現した「ABC」と一致します。現在の位置から7文字前の位置から始まり、長さは3文字です。これを「(距離=7, 長さ=3)」のような参照に置き換えます。
このように、頻繁に出現する長い文字列を短い参照に置き換えることで、データのサイズを削減します。LZ77は、データ内の「冗長性」(繰り返しのパターン)を効果的に削減するのに優れています。
2. ハフマン符号化
LZ77によって繰り返し文字列が参照に置き換えられた後、残ったそのままの文字や、LZ77の参照(距離と長さのペア)は、ハフマン符号化によってさらに効率的に符号化されます。
ハフマン符号化は、出現頻度の高いデータ(文字や参照)には短いビット列を割り当て、出現頻度の低いデータには長いビット列を割り当てることで、全体のデータ量を削減する手法です。例えば、「A」が頻繁に出現し、「Z」があまり出現しない場合、「A」には「0」、「Z」には「1110」のようなビット列を割り当てることで、全体のビット長を短くします。
DEFLATEでは、LZ77の出力(そのままの文字と参照)に対して、動的ハフマン符号化または固定ハフマン符号化を適用します。動的ハフマン符号化は、入力データに合わせて最適なハフマン木を構築するため圧縮率は高くなりますが、ハフマン木自体もデータに含める必要があります。固定ハフマン符号化は、事前に定義された木を使用するため高速ですが、圧縮率は劣る場合があります。zlib は通常、動的ハフマン符号化を利用します。
DEFLATEの組み合わせ
DEFLATEアルゴリズムは、LZ77による繰り返しパターンの検出と、ハフマン符号化による符号長の最適化という、異なる性質を持つ二つの手法を組み合わせることで、高い圧縮率と比較的高い処理速度を実現しています。zlib は、この DEFLATE アルゴリズムを効率的に実装しており、様々なオプションで圧縮率と速度のバランスを調整できるようになっています。
zlib ライブラリの概要
zlib は、DEFLATEアルゴリズムの実装を提供する C 言語ライブラリです。その主な特徴は以下の通りです。
- 高速性: DEFLATEアルゴリズムは比較的高速に実行でき、zlib はその効率的な実装を提供します。
- 高い圧縮率: DEFLATEは多くの種類のデータに対して良好な圧縮率を発揮します。
- 移植性: C言語で記述されており、様々なOSやアーキテクチャでコンパイル・実行可能です。
- フリーライセンス: zlib ライセンスの下で提供され、利用の制限が非常に少ないです。
- ストリーム処理: 大容量のデータをメモリに全て読み込むことなく、チャンク(断片)ごとに処理できます。これは、メモリが限られている環境や、ファイルやネットワークから逐次データを受け取る場合に非常に有用です。
- エラー検出: データの破損を検出するための Adler-32 または CRC-32 チェックサムをサポートしています。
zlib の核となるのは、圧縮・解凍の状態を保持する z_stream
構造体です。この構造体は、入力バッファ、出力バッファ、現在の圧縮/解凍の状態、進行状況を示すカウンタなどを保持します。zlib の API は、主にこの z_stream
構造体を引数として受け取り、データの圧縮または解凍を行います。
主要な関数カテゴリは以下の通りです。
- 初期化/終了処理:
deflateInit
,deflateInit2
,inflateInit
,inflateInit2
,deflateEnd
,inflateEnd
- 圧縮/解凍処理:
deflate
,inflate
- ユーティリティ:
adler32
,crc32
- 簡易インターフェース:
compress
,uncompress
(メモリ上の単一バッファ処理用)
次のセクションでは、これらの関数群を実際にどのように使用するのか、C 言語での具体的なコード例と共に解説します。
zlib の基本的な使い方 (C言語)
zlib を C 言語プログラムで使用するには、以下の手順が必要です。
- zlib ライブラリのインストール: 多くのOSにはパッケージマネージャー経由で zlib が提供されています (
libz-dev
,zlib-devel
など)。ソースコードからビルドすることも可能です。 - ヘッダーファイルのインクルード: ソースコードで
<zlib.h>
をインクルードします。 - リンク: コンパイル時に zlib ライブラリをリンクします(例:
-lz
)。
基本的な圧縮処理の流れ
メモリ上のデータを圧縮する最も基本的な流れは以下のようになります。
z_stream
構造体を宣言し、ゼロクリアするなどして初期化します。deflateInit
またはdeflateInit2
関数を呼び出し、圧縮処理を初期化します。deflateInit2
を使うと、圧縮レベル、アルゴリズムの詳細(ウィンドウサイズなど)、ヘッダー形式(zlib, gzip, raw DEFLATE)などをより細かく指定できます。- 入力データと出力バッファを準備します。
z_stream
のnext_in
,avail_in
に入力データのポインタとサイズを設定し、next_out
,avail_out
に出力バッファのポインタとサイズを設定します。 deflate
関数を呼び出し、圧縮処理を実行します。deflate
は、入力バッファからデータを読み込み、圧縮して出力バッファに書き込みます。出力バッファがいっぱいになるか、入力バッファが空になるまで処理を続けます。- 必要に応じて、出力バッファの内容をどこか(ファイルなど)に書き出し、出力バッファを再利用可能にします。入力データがまだ残っている場合は、入力バッファのポインタとサイズを更新し、手順3に戻ります。
- 入力データの全てを
deflate
に供給し終えたら、最後のデータの処理と残りの圧縮データのフラッシュのために、フラッシュモードとしてZ_FINISH
を指定してdeflate
を再度呼び出します。deflate
はZ_STREAM_END
を返すまで繰り返し呼び出す必要がある場合があります。 deflate
がZ_STREAM_END
を返したら、全てのデータが圧縮され、出力バッファに書き出されたことを意味します。deflateEnd
関数を呼び出し、圧縮処理で使用した内部リソースを解放します。- エラーが発生した場合は、
z_stream
のmsg
フィールドや関数の戻り値 (Z_OK
,Z_STREAM_END
,Z_BUF_ERROR
,Z_MEM_ERROR
,Z_DATA_ERROR
など) を確認して対応します。
基本的な解凍処理の流れ
圧縮されたデータを解凍する基本的な流れは以下のようになります。
z_stream
構造体を宣言し、ゼロクリアするなどして初期化します。inflateInit
またはinflateInit2
関数を呼び出し、解凍処理を初期化します。inflateInit2
を使うと、圧縮時に使用されたウィンドウサイズやヘッダー形式(zlib, gzip, raw DEFLATE)などを指定できます。特にヘッダー形式の指定は重要です。inflateInit
は zlib ヘッダーを仮定します。- 入力データ(圧縮データ)と出力バッファを準備します。
z_stream
のnext_in
,avail_in
に入力データ(圧縮データ)のポインタとサイズを設定し、next_out
,avail_out
に出力バッファのポインタとサイズを設定します。 inflate
関数を呼び出し、解凍処理を実行します。inflate
は、入力バッファから圧縮データを読み込み、解凍して出力バッファに書き込みます。出力バッファがいっぱいになるか、入力バッファのデータが足りなくなるまで処理を続けます。- 必要に応じて、出力バッファの内容をどこか(ファイルなど)に書き出し、出力バッファを再利用可能にします。入力データ(圧縮データ)がまだ残っている場合は、入力バッファのポインタとサイズを更新し、手順3に戻ります。
inflate
関数は、圧縮データの末尾に到達し、全てのデータが解凍されるとZ_STREAM_END
を返します。この戻り値を受け取るまでinflate
を繰り返し呼び出す必要があります。inflate
がZ_STREAM_END
を返したら、全てのデータが解凍されたことを意味します。inflateEnd
関数を呼び出し、解凍処理で使用した内部リソースを解放します。- エラーが発生した場合は、
z_stream
のmsg
フィールドや関数の戻り値 (Z_OK
,Z_STREAM_END
,Z_BUF_ERROR
,Z_DATA_ERROR
,Z_NEED_DICT
など) を確認して対応します。特にZ_DATA_ERROR
は入力データが壊れている可能性を示します。
簡単なサンプルコード (メモリ上のデータ圧縮・解凍)
ここでは、メモリ上の単一バッファに対して圧縮・解凍を行う簡単な例を示します。ストリーム処理ではないため、入力データと出力データのサイズに事前に見当をつけるか、必要に応じてバッファを再割り当てする必要があります。以下の例では、出力バッファを十分に大きく確保しています。
“`c
include
include
include
include
// メモリ上のデータを圧縮する関数
// input: 圧縮したいデータ
// input_len: input の長さ
// compressed_output: 圧縮データ格納先バッファ (呼び出し元で確保)
// compressed_output_len: compressed_output バッファのサイズ
// actual_compressed_len: 実際に出力された圧縮データの長さ (出力パラメータ)
// 戻り値: Z_OK 成功, それ以外 失敗
int compress_buffer(const unsigned char input, size_t input_len,
unsigned char compressed_output, size_t compressed_output_len,
size_t *actual_compressed_len) {
z_stream strm;
int ret;
// z_stream 構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// 圧縮処理の初期化 (デフォルト圧縮レベル)
// Z_DEFAULT_COMPRESSION は通常 6 です
ret = deflateInit(&strm, Z_DEFAULT_COMPRESSION);
if (ret != Z_OK) {
fprintf(stderr, "deflateInit failed: %d\n", ret);
return ret;
}
// 入力・出力バッファの設定
strm.next_in = (Bytef *)input;
strm.avail_in = input_len;
strm.next_out = (Bytef *)compressed_output;
strm.avail_out = compressed_output_len;
// 圧縮処理の実行
// Z_FINISH フラグは、入力データの最後まで処理し、
// 全ての圧縮データを出力バッファにフラッシュすることを示します
ret = deflate(&strm, Z_FINISH);
// deflate が Z_STREAM_END を返すまで繰り返すのが一般的ですが、
// バッファが十分に大きい場合は Z_FINISH で一度で終わることが多いです
if (ret != Z_STREAM_END) {
fprintf(stderr, "deflate failed: %d (%s)\n", ret, strm.msg);
// エラーまたはバッファ不足 Z_BUF_ERROR
deflateEnd(&strm);
return ret;
}
// 実際に出力されたデータの長さを取得
*actual_compressed_len = compressed_output_len - strm.avail_out;
// 終了処理
deflateEnd(&strm);
return Z_OK;
}
// メモリ上の圧縮データを解凍する関数
// compressed_input: 圧縮データ
// compressed_input_len: compressed_input の長さ
// decompressed_output: 解凍データ格納先バッファ (呼び出し元で確保)
// decompressed_output_len: decompressed_output バッファのサイズ
// actual_decompressed_len: 実際に出力された解凍データの長さ (出力パラメータ)
// 戻り値: Z_OK 成功, それ以外 失敗
int decompress_buffer(const unsigned char compressed_input, size_t compressed_input_len,
unsigned char decompressed_output, size_t decompressed_output_len,
size_t *actual_decompressed_len) {
z_stream strm;
int ret;
// z_stream 構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// 解凍処理の初期化 (zlib ヘッダーを想定)
ret = inflateInit(&strm);
if (ret != Z_OK) {
fprintf(stderr, "inflateInit failed: %d\n", ret);
return ret;
}
// 入力・出力バッファの設定
strm.next_in = (Bytef *)compressed_input;
strm.avail_in = compressed_input_len;
strm.next_out = (Bytef *)decompressed_output;
strm.avail_out = decompressed_output_len;
// 解凍処理の実行
// Z_FINISH フラグは、入力データの最後まで処理し、
// 全ての解凍データを出力バッファにフラッシュすることを示します
ret = inflate(&strm, Z_FINISH);
// inflate が Z_STREAM_END を返すのが正常終了
if (ret != Z_STREAM_END) {
fprintf(stderr, "inflate failed: %d (%s)\n", ret, strm.msg);
// エラーまたはバッファ不足 Z_BUF_ERROR
inflateEnd(&strm);
return ret;
}
// 実際に出力されたデータの長さを取得
*actual_decompressed_len = decompressed_output_len - strm.avail_out;
// 終了処理
inflateEnd(&strm);
return Z_OK;
}
int main() {
const char *original_data = “This is a sample string to be compressed and then decompressed. ”
“Data compression is useful for reducing storage space and transmission time. ”
“zlib is a widely used library for DEFLATE compression.”;
size_t original_len = strlen(original_data) + 1; // +1 for null terminator
// 圧縮後の最大サイズを見積もる
// 圧縮できない場合 (ランダムデータなど) はサイズが増加する可能性があります
// gzipマニュアルなどによると、入力サイズ + 12バイトのヘッダー + 4バイトのチェックサム +
// 1バイトのフッター + ブロックごとのオーバーヘッド (5バイト?) + 未圧縮データのオーバーヘッド
// 厳密な最大値は計算が難しいですが、ここでは適当に大きめに確保します
// 実際には zlib の compressBound 関数などを使って見積もるのが良いです
size_t max_compressed_len = original_len + (original_len / 1000) + 12 + 4 + 1 + 5; // 適当な見積もり
if (original_len > 0) max_compressed_len = original_len + original_len / 100 + 12 + 4 + 1; // 少し改善
if (original_len > 0 && max_compressed_len < original_len + 50) max_compressed_len = original_len + 50; // 最小限のオーバーヘッド
// zlib には compressBound 関数があります: deflateBound(strm, sourceLen)
unsigned char *compressed_buffer = (unsigned char *)malloc(max_compressed_len);
size_t actual_compressed_len = 0;
if (compressed_buffer == NULL) {
perror("Failed to allocate memory for compressed buffer");
return 1;
}
// 圧縮実行
printf("Original data size: %zu bytes\n", original_len);
int ret_compress = compress_buffer((const unsigned char *)original_data, original_len,
compressed_buffer, max_compressed_len,
&actual_compressed_len);
if (ret_compress == Z_OK) {
printf("Compression successful.\n");
printf("Compressed data size: %zu bytes\n", actual_compressed_len);
printf("Compression ratio: %.2f%%\n", (double)actual_compressed_len / original_len * 100.0);
// 解凍処理
size_t max_decompressed_len = original_len; // 可逆圧縮なので元のサイズに戻るはず
unsigned char *decompressed_buffer = (unsigned char *)malloc(max_decompressed_len);
size_t actual_decompressed_len = 0;
if (decompressed_buffer == NULL) {
perror("Failed to allocate memory for decompressed buffer");
free(compressed_buffer);
return 1;
}
// 解凍実行
int ret_decompress = decompress_buffer(compressed_buffer, actual_compressed_len,
decompressed_buffer, max_decompressed_len,
&actual_decompressed_len);
if (ret_decompress == Z_OK) {
printf("Decompression successful.\n");
printf("Decompressed data size: %zu bytes\n", actual_decompressed_len);
// 元のデータと一致するか確認
if (actual_decompressed_len == original_len &&
memcmp(original_data, decompressed_buffer, original_len) == 0) {
printf("Original and decompressed data match.\n");
} else {
fprintf(stderr, "Error: Original and decompressed data do NOT match.\n");
}
} else {
fprintf(stderr, "Decompression failed with error code %d\n", ret_decompress);
}
free(decompressed_buffer);
} else {
fprintf(stderr, "Compression failed with error code %d\n", ret_compress);
}
free(compressed_buffer);
return 0;
}
“`
コンパイル方法:
bash
gcc -o zlib_example zlib_example.c -lz
実行:
bash
./zlib_example
このサンプルコードは、メモリ上で小さなデータを圧縮・解凍する基本的な流れを示しています。実際のアプリケーションでは、より大きなデータを扱うために「ストリーム処理」の技法が不可欠です。
ワンショット関数 compress
および uncompress
zlib には、メモリ上の単一バッファの圧縮・解凍を簡単に行うための compress
および uncompress
関数も用意されています。これらは内部で deflateInit
, deflate
, deflateEnd
または inflateInit
, inflate
, inflateEnd
をラップしたもので、非常に手軽に使えます。ただし、中間バッファ処理や詳細な制御はできません。
“`c
include
include
include
include
int main() {
const char *original_data = “This is a test string for compress/uncompress.”;
size_t original_len = strlen(original_data) + 1;
// 圧縮後のバッファサイズ見積もり (compressBound を使うのが確実)
uLong compressed_buffer_size = compressBound(original_len);
unsigned char *compressed_buffer = (unsigned char *)malloc(compressed_buffer_size);
if (compressed_buffer == NULL) {
perror("malloc failed");
return 1;
}
uLong actual_compressed_len;
// 圧縮
printf("Original size: %zu\n", original_len);
int ret_c = compress(compressed_buffer, &actual_compressed_len,
(const unsigned char *)original_data, original_len);
if (ret_c == Z_OK) {
printf("Compression successful. Compressed size: %lu\n", actual_compressed_len);
// 解凍
uLong decompressed_buffer_size = original_len;
unsigned char *decompressed_buffer = (unsigned char *)malloc(decompressed_buffer_size);
if (decompressed_buffer == NULL) {
perror("malloc failed");
free(compressed_buffer);
return 1;
}
uLong actual_decompressed_len;
int ret_uc = uncompress(decompressed_buffer, &actual_decompressed_len,
(const unsigned char *)compressed_buffer, actual_compressed_len);
if (ret_uc == Z_OK) {
printf("Decompression successful. Decompressed size: %lu\n", actual_decompressed_len);
// 確認
if (actual_decompressed_len == original_len &&
memcmp(original_data, decompressed_buffer, original_len) == 0) {
printf("Data matches original.\n");
} else {
fprintf(stderr, "Data mismatch!\n");
}
} else {
fprintf(stderr, "uncompress failed: %d\n", ret_uc);
}
free(decompressed_buffer);
} else {
fprintf(stderr, "compress failed: %d\n", ret_c);
}
free(compressed_buffer);
return 0;
}
``
deflate
このワンショット関数は非常にシンプルですが、大規模なデータやファイルI/Oを伴う処理には向いていません。そのため、通常は/
inflate` を使用したストリーム処理が推奨されます。
ストリーム処理 (大きなファイルの扱い)
メモリに全て収まらないような大きなデータを圧縮・解凍する場合や、ファイル、ネットワークソケットなどから逐次データを読み込み・書き出しながら処理を行う場合には、「ストリーム処理」が必須となります。
ストリーム処理では、入出力データを小さなチャンクに分割し、deflate
や inflate
関数を繰り返し呼び出して処理を進めます。z_stream
構造体は、チャンクを跨いで圧縮/解凍の状態(辞書、ハフマン木など)を自動的に引き継いでくれます。
ファイル入出力との連携
ファイルI/Oと連携させる場合の典型的な流れは以下のようになります。
圧縮 (ファイル単位):
- 入力ファイルと出力ファイルを開きます。
z_stream
を初期化し、deflateInit
またはdeflateInit2
を呼び出します。- メインループ:
- 入力ファイルからデータを一定量(例: 4KB, 8KB)入力バッファに読み込みます。
strm.next_in
,strm.avail_in
を設定します。- 出力バッファを準備し、
strm.next_out
,strm.avail_out
を設定します。 deflate
関数を呼び出します。入力がまだある間はZ_NO_FLUSH
を指定します。deflate
呼び出し後、strm.avail_out
が減っていれば、その分(元の出力バッファサイズ –strm.avail_out
)のデータが圧縮されたデータとして出力バッファに書き込まれているので、出力ファイルに書き出します。出力バッファが満杯になった場合も同様に書き出します。- 入力バッファが空になったら、次のチャンクをファイルから読み込みます。出力バッファが満杯になったら、バッファをクリアして再利用します。
- このループを入力ファイルの末尾まで続けます。
- 入力ファイルの末尾に達したら、残りのデータを全てフラッシュするために、フラッシュモードを
Z_FINISH
に設定してdeflate
を呼び出します。deflate
がZ_STREAM_END
を返すまで、出力バッファの内容を書き出しながらdeflate(..., Z_FINISH)
を繰り返し呼び出します。 deflate
がZ_STREAM_END
を返したら、deflateEnd
を呼び出し、リソースを解放します。- ファイルクローズなどの後処理を行います。エラー発生時は適切にハンドリングします。
解凍 (ファイル単位):
- 入力ファイル(圧縮ファイル)と出力ファイルを開きます。
z_stream
を初期化し、inflateInit
またはinflateInit2
を呼び出します。inflateInit2
を使う場合は、圧縮時のヘッダー形式に注意してください。- メインループ:
- 入力ファイルから圧縮データを一定量入力バッファに読み込みます。
strm.next_in
,strm.avail_in
を設定します。- 出力バッファを準備し、
strm.next_out
,strm.avail_out
を設定します。 inflate
関数を呼び出します。通常、フラッシュモードはZ_NO_FLUSH
を指定します。inflate
呼び出し後、strm.avail_out
が減っていれば、その分(元の出力バッファサイズ –strm.avail_out
)のデータが解凍されたデータとして出力バッファに書き込まれているので、出力ファイルに書き出します。出力バッファが満杯になった場合も同様に書き出します。inflate
は、入力バッファが尽きるか、出力バッファが満杯になるか、圧縮ストリームの終端に達するまで処理を行います。inflate
がZ_OK
を返した場合、処理を継続できます。Z_BUF_ERROR
は出力バッファが小さすぎる場合に起こり得ますが、通常はループ内で出力バッファを処理すれば回避できます。- 入力バッファが空になったら、次のチャンクをファイルから読み込みます。出力バッファが満杯になったら、バッファをクリアして再利用します。
inflate
がZ_STREAM_END
を返すまでこのループを続けます。
inflate
がZ_STREAM_END
を返したら、全てのデータが解凍されました。必要に応じて、出力バッファに残った最終的なデータを書き出します。inflateEnd
を呼び出し、リソースを解放します。- ファイルクローズなどの後処理を行います。エラー発生時(特に
Z_DATA_ERROR
など)は適切にハンドリングします。
ストリーム処理のサンプルコード (ファイルコピーと圧縮/解凍)
ここでは、標準入力から読み込み、標準出力に書き出す処理を、圧縮または解凍を挟んで行う簡単なコマンドラインツールの例を示します。
“`c
include
include
include
include
// バッファサイズ
define CHUNK 16384 // 16KB
// エラーメッセージ表示関数
void zerr(int ret) {
fputs(“zpipe: “, stderr);
switch (ret) {
case Z_ERRNO:
if (errno) fprintf(stderr, “error %d\n”, errno);
break;
case Z_STREAM_ERROR:
fputs(“invalid compression level\n”, stderr);
break;
case Z_DATA_ERROR:
fputs(“invalid or incomplete deflate data\n”, stderr);
break;
case Z_MEM_ERROR:
fputs(“out of memory\n”, stderr);
break;
case Z_VERSION_ERROR:
fputs(“zlib version mismatch!\n”, stderr);
}
}
// 圧縮処理 (in: 入力ファイルポインタ, out: 出力ファイルポインタ)
int def(FILE in, FILE out, int level) {
z_stream strm;
unsigned char in_buffer[CHUNK];
unsigned char out_buffer[CHUNK];
int ret;
int flush;
// z_stream 構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// 圧縮処理の初期化
ret = deflateInit(&strm, level);
if (ret != Z_OK) return ret;
// メインループ
do {
// 入力バッファにデータを読み込む
strm.avail_in = fread(in_buffer, 1, CHUNK, in);
if (ferror(in)) {
deflateEnd(&strm);
return Z_ERRNO;
}
// 入力がファイルの終端に達したか確認
flush = feof(in) ? Z_FINISH : Z_NO_FLUSH;
strm.next_in = in_buffer;
// 出力バッファの準備と deflate の呼び出し
do {
strm.next_out = out_buffer;
strm.avail_out = CHUNK;
// 圧縮実行
ret = deflate(&strm, flush);
if (ret == Z_STREAM_ERROR) { // パラメータ不正など
deflateEnd(&strm);
return ret;
}
// 出力バッファに書き出されたデータの量を計算
size_t have = CHUNK - strm.avail_out;
// 圧縮されたデータを出力ファイルに書き出す
if (fwrite(out_buffer, 1, have, out) != have) {
deflateEnd(&strm);
return Z_ERRNO;
}
} while (strm.avail_out == 0); // 出力バッファがいっぱいなら繰り返す
// avail_in == 0 は、入力バッファが空になったことを意味する
} while (flush != Z_FINISH); // Z_FINISH が指定され、かつ deflate が Z_STREAM_END を返すまで繰り返す
// Z_STREAM_END が返されたか確認
if (ret != Z_STREAM_END) { // Z_BUF_ERROR など
deflateEnd(&strm);
return ret; // または Z_DATA_ERROR など適切なエラーコードを返す
}
// 終了処理
deflateEnd(&strm);
return Z_OK;
}
// 解凍処理 (in: 入力ファイルポインタ, out: 出力ファイルポインタ)
int inf(FILE in, FILE out) {
z_stream strm;
unsigned char in_buffer[CHUNK];
unsigned char out_buffer[CHUNK];
int ret;
// z_stream 構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
// 解凍処理の初期化
// inflateInit は zlib ヘッダーを想定します
// gzip ファイルを扱いたい場合は inflateInit2(&strm, 15 + 16) のようにします
ret = inflateInit(&strm);
if (ret != Z_OK) return ret;
// メインループ
do {
// 入力バッファにデータを読み込む
strm.avail_in = fread(in_buffer, 1, CHUNK, in);
if (ferror(in)) {
inflateEnd(&strm);
return Z_ERRNO;
}
// 入力がファイルの終端に達し、かつ読み込みが0バイトだった場合ループ終了
if (strm.avail_in == 0) break;
strm.next_in = in_buffer;
// 出力バッファの準備と inflate の呼び出し
do {
strm.next_out = out_buffer;
strm.avail_out = CHUNK;
// 解凍実行
ret = inflate(&strm, Z_NO_FLUSH); // 通常は Z_NO_FLUSH
switch (ret) {
case Z_NEED_DICT: // 辞書が必要
case Z_DATA_ERROR: // 入力データ破損
case Z_MEM_ERROR: // メモリ不足
case Z_STREAM_ERROR: // パラメータ不正など
inflateEnd(&strm);
return ret;
}
// 出力バッファに書き出されたデータの量を計算
size_t have = CHUNK - strm.avail_out;
// 解凍されたデータを出力ファイルに書き出す
if (fwrite(out_buffer, 1, have, out) != have) {
inflateEnd(&strm);
return Z_ERRNO;
}
} while (strm.avail_out == 0); // 出力バッファがいっぱいなら繰り返す
// ret == Z_STREAM_END の場合はループを抜ける (完全解凍)
} while (ret != Z_STREAM_END);
// 終了処理
inflateEnd(&strm);
return Z_OK;
}
int main(int argc, char **argv) {
int ret;
// 使用法表示
if (argc < 2) {
fprintf(stderr, "Usage: %s [-d] [ file ... ]\n", argv[0]);
fprintf(stderr, " Compress or decompress stdin to stdout.\n");
fprintf(stderr, " -d: decompress\n");
return 1;
}
// オプション解析
int decompress_mode = 0;
if (argc > 1 && strcmp(argv[1], "-d") == 0) {
decompress_mode = 1;
}
// 処理実行
if (decompress_mode) {
// 解凍モード
ret = inf(stdin, stdout);
if (ret != Z_OK) zerr(ret);
} else {
// 圧縮モード (デフォルト圧縮レベル)
ret = def(stdin, stdout, Z_DEFAULT_COMPRESSION);
if (ret != Z_OK) zerr(ret);
}
// ファイルエラーチェック (stdout)
if (ferror(stdout)) {
fprintf(stderr, "Error writing to stdout.\n");
return 1; // または適切なエラーコード
}
fflush(stdout); // stdout のバッファをフラッシュ
return ret == Z_OK ? 0 : 1;
}
“`
コンパイル:
bash
gcc -o zpipe zpipe.c -lz
実行例:
mydata.txt
というファイルを圧縮し、mydata.zlib
として保存:
bash
./zpipe < mydata.txt > mydata.zlibmydata.zlib
を解凍し、標準出力に表示:
bash
./zpipe -d < mydata.zlib
このサンプルは、zlib の公式配布物に含まれている zpipe.c
を参考にしています。ストリーム処理の基本的な構造(ループ内で fread
/fwrite
と deflate
/inflate
を呼び出す)がよく分かります。特に、deflate
の Z_NO_FLUSH
と Z_FINISH
の使い分け、および inflate
の戻り値の処理が重要です。
高度な機能とオプション
deflateInit2
および inflateInit2
関数を使用することで、zlib の圧縮・解凍動作をより詳細に制御できます。
圧縮レベル (Compression Level)
deflateInit
の第2引数、または deflateInit2
の第2引数 level
で圧縮レベルを指定できます。指定可能な値は以下の通りです。
Z_NO_COMPRESSION
(0): 圧縮を行わず、LZ77マッチングとハフマン符号化をスキップします。データはほぼそのまま出力されますが、zlib ヘッダー/フッターとチャンクごとのヘッダーが付加されます。速度は最も速いですが、サイズ削減効果はありません。Z_BEST_SPEED
(1): 最速の圧縮設定です。LZ77マッチングの探索範囲などを限定し、計算コストを抑えます。圧縮率は低めになります。Z_DEFAULT_COMPRESSION
(-1): zlib のデフォルト設定です(通常は 6 と同じ)。速度と圧縮率のバランスが良い設定です。Z_BEST_COMPRESSION
(9): 最も高い圧縮率を目指す設定です。LZ77マッチングの探索範囲を広げるなど、計算に時間をかけます。速度は最も遅くなります。
1から9までの間の整数を指定することも可能で、値が大きいほど圧縮率が高まりますが、処理時間は増加します。
ウィンドウサイズ (Window Size)
deflateInit2
および inflateInit2
の第3引数 windowBits
で、LZ77アルゴリズムで使用する「ウィンドウサイズ」を指定できます。これは、過去のデータのうちどの範囲を検索して繰り返し文字列を探すか、という大きさを決定します。
- 値は対数で指定します。例えば、15 は 2^15 = 32768 バイト (32KB) を意味します。これがデフォルト値であり、最大値です。大きいウィンドウサイズは、長い繰り返しパターンを検出できる可能性を高めるため、圧縮率向上に寄与しますが、メモリ使用量が増加し、処理速度が低下する場合があります。
windowBits
には符号があり、ヘッダー/フッターの有無を指定できます。9
~15
: zlib ヘッダー/フッター付きの DEFLATE ストリームを出力/入力します。これが標準的な zlib フォーマットです。9 + 16
~15 + 16
: gzip ヘッダー/フッター付きの DEFLATE ストリームを出力/入力します。これはgzip
コマンドが生成するフォーマットです。9 + 32
~15 + 32
: zlib または gzip のどちらかのヘッダーを自動検出して入力します(解凍時のみ)。-9
~-15
: ヘッダー/フッターなしの「raw DEFLATE」ストリームを出力/入力します。これは通常、より上位のプロトコルやフォーマット(例: PNG)が独自のヘッダーやチェックサムを持つ場合に使用されます。
解凍時には、圧縮時に使用された windowBits
と同じ値を指定する必要があります。ただし、ヘッダー付きの場合は 15 + 32
を指定すれば、zlib/gzip ヘッダーを自動判別してくれるため便利です。
メモリ使用量の制御 (Memory Level)
deflateInit2
の第4引数 memLevel
で、圧縮器が使用するメモリ量を指定できます。1 から 9 までの整数を指定し、値が大きいほどより多くのメモリを使用しますが、通常は圧縮率が向上し、場合によっては速度も向上します(探索範囲が広がるなど)。デフォルトは 8 です。メモリが非常に限られた環境では、この値を小さく設定する必要があるかもしれません。
ヘッダーとチェックサム
zlib フォーマットおよび gzip フォーマットは、圧縮データ自体に加えてヘッダーとフッターを含みます。
- zlib フォーマット:
- ヘッダー: 圧縮方式、ウィンドウサイズ、圧縮レベルなどを示す情報が含まれます。
- フッター: Adler-32 チェックサムが含まれます。
- gzip フォーマット:
- ヘッダー: ファイル名、タイムスタンプなどの情報、圧縮方式などが含まれます。
- フッター: CRC-32 チェックサムと元のデータのサイズが含まれます。
zlib ライブラリは、これらのヘッダーやフッターの生成・検証を windowBits
の設定に基づいて自動で行います。
また、zlib は adler32
および crc32
という関数を提供しており、データ自体のチェックサムを計算することも可能です。これは、圧縮とは別にデータの整合性を検証したい場合に役立ちます。z_stream
構造体の adler
または crc32
フィールドは、圧縮/解凍中に計算されたチェックサムの値を保持します。
ディクショナリ (Dictionary)
特定の種類のデータ(例:同じファイルの異なるバージョン、似たような構造を持つデータ)を圧縮する場合、以前に処理したデータや、データに特有の頻出パターンを含む「ディクショナリ」を事前に圧縮器に与えることで、圧縮率を向上させることができます。
deflateSetDictionary
関数を使用して、圧縮を開始する前にディクショナリを設定できます。解凍時には、圧縮時に使用したディクショナリと完全に同じものを inflateSetDictionary
関数で設定する必要があります。ディクショナリのサイズはウィンドウサイズによって制限されます。
マルチスレッドでの利用
zlib ライブラリ自体は、単一の z_stream
インスタンスに対する操作においてスレッドセーフではありません。複数のスレッドから同時に同じ z_stream
構造体を操作することはできません。
しかし、異なる z_stream
構造体は完全に独立しているため、各スレッドが独自の z_stream
インスタンスを保持していれば、複数のスレッドで同時に圧縮・解凍処理を実行することが可能です。これは、複数のファイルを並行して処理したり、データを複数のチャンクに分割してそれぞれを別スレッドで処理したりする場合に有効です。
パフォーマンスに関する考慮事項
zlib を利用する上で、パフォーマンスは重要な要素です。圧縮率と速度はトレードオフの関係にあり、またバッファサイズやメモリ使用量もパフォーマンスに影響を与えます。
- 圧縮レベル vs 速度: 既に述べたように、圧縮レベルが高いほど圧縮率は向上しますが、処理時間も増加します。アプリケーションの要件(ストレージ/帯域幅の節約を重視するか、処理速度を重視するか)に応じて適切なレベルを選択する必要があります。
Z_DEFAULT_COMPRESSION
(6) は多くの用途で良いバランスを提供します。リアルタイム性が求められる場合はZ_BEST_SPEED
(1) やZ_NO_COMPRESSION
を検討することもあります。 - バッファサイズの影響: ストリーム処理における入出力バッファのサイズはパフォーマンスに影響を与えます。大きすぎるバッファはメモリ使用量を増やしますが、システムコールの回数を減らし、LZ77アルゴリズムによる長い繰り返しパターンの検出に有利になる可能性があります。小さすぎるバッファはシステムコールのオーバーヘッドを増やします。一般的な値として 4KB から 64KB 程度がよく使用されます。適切なバッファサイズは、データの性質、システム環境(CPU、メモリ、I/O速度)によって異なりますので、ベンチマークを行って最適なサイズを見つけるのが理想的です。また、バッファが CPU のキャッシュラインやページサイズに合うように調整することも効果がある場合があります。
- メモリ使用量:
memLevel
やwindowBits
の設定はメモリ使用量に直接影響します。メモリ使用量を減らすと、圧縮率が低下したり、速度が遅くなったりする可能性があります。特に組み込みシステムなどメモリが限られた環境では、これらのパラメータを調整する必要があります。 - CPU キャッシュ効率: バッファを効率的に管理し、データのコピーを最小限に抑えることで、CPU のキャッシュ効率を高め、パフォーマンスを向上させることができます。
他のライブラリとの比較
データ圧縮ライブラリは zlib 以外にも多数存在します。代表的なものをいくつか紹介し、zlib との簡単な比較を行います。
- gzip: gzip は、zlib を利用したコマンドラインツールおよびファイルフォーマットです。gzip ファイルは zlib フォーマットのデータに gzip ヘッダーとフッターが付加されたものです。互換性が必要な場合は gzip フォーマットを使用することがあります。
- bzip2: bzip2 は、Burrows-Wheeler変換とMove-to-Front変換、ランレングス符号化、ハフマン符号化を組み合わせた圧縮アルゴリズムです。DEFLATEよりも一般的に圧縮率が高いですが、処理速度は遅い傾向があります。メモリ使用量も多いです。
- LZ4, Snappy, Zstd (Zstandard): これらは比較的新しい圧縮アルゴリズムおよびライブラリです。zlib (DEFLATE) よりも高速に圧縮・解凍できることを主な目標として開発されています。多くの場合、zlib よりも高速ですが、圧縮率は zlib より低いか同程度であることが多いです。レイテンシが重要な場合や、CPUリソースを節約したい場合に適しています。Facebook (Zstd), Google (Snappy), Yann Collet (LZ4/Zstd) などが開発を主導しています。
zlib が依然として広く利用されている理由は、その長い歴史、高い互換性、豊富な実績、そして非常に寛容なライセンスにあります。多くのシステムやフォーマットに組み込まれているため、既存のシステムとの連携や、広く配布されるデータの圧縮には zlib が適している場合が多いです。より高い圧縮率が必要なら bzip2、より高い速度が必要なら LZ4/Snappy/Zstd といったように、用途に応じて適切なライブラリを選択することになります。
まとめ
高速データ圧縮ライブラリ zlib は、DEFLATE アルゴリズムを効率的に実装した、非常に普及しているライブラリです。シンプルながらも強力な API を提供し、メモリ上のデータから大容量のストリームデータまで、幅広い用途で利用できます。
この記事では、zlib の核となる DEFLATE アルゴリズムの仕組み、zlib ライブラリの基本概念、C 言語での基本的な使い方(メモリ処理とストリーム処理)、圧縮レベルやウィンドウサイズといった高度な機能、そしてパフォーマンスに関する考慮事項について詳細に解説しました。
zlib を活用することで、ソフトウェアのストレージ使用量を削減したり、ネットワーク転送効率を向上させたりすることが可能になります。今回学んだ知識を元に、ぜひあなたのプロジェクトで zlib を活用してみてください。
今後の学習リソース:
- zlib 公式ウェブサイト: (http://www.zlib.net/) 最新情報、ソースコード、公式ドキュメントへのリンクがあります。
- zlib マニュアル (zlib Manual): zlib の関数リファレンスや詳細な説明が記載されています。
- zlib 付属のサンプルコード:
zpipe.c
やminigzip.c
など、実用的なサンプルが含まれています。
データ圧縮技術は奥深く、学ぶべきことは多々ありますが、zlib はその強力な第一歩となるでしょう。この記事が、あなたが zlib の世界に入るための一助となれば幸いです。
約5000語の目標に向けて記述しましたが、実際の単語数は開発環境や計測方法により若干変動する可能性があります。