【初心者向け】zlibライブラリの紹介と簡単な使い方

【初心者向け】zlibライブラリの紹介と簡単な使い方

はじめに:なぜ圧縮・解凍が必要なのか?

皆さんは、インターネットでファイルをダウンロードしたり、誰かにメールでファイルを送ったり、スマートフォンで写真を撮ったり、パソコンにたくさんのデータを保存したりする際に、「圧縮」という言葉を聞いたことがあるかもしれません。ファイルサイズを小さくするために、ZIPファイルにまとめたりしますよね。

なぜ、私たちはデータを圧縮する必要があるのでしょうか?主な理由はいくつかあります。

  1. ストレージ容量の節約: パソコンのハードディスク、スマートフォンのフラッシュメモリ、クラウドストレージなど、データを保存する場所には限りがあります。データを圧縮することで、より多くの情報を同じ容量の中に詰め込むことができます。特に、テキストファイル、画像ファイル、動画ファイルなど、サイズが大きいデータは圧縮の効果が大きいです。
  2. データ転送時間の短縮: インターネットやローカルネットワークを通じてデータを送受信する際、データ量が少なければ少ないほど、転送にかかる時間が短くなります。これは、ダウンロード速度の向上や、ウェブページの表示速度の高速化に直結します。通信帯域が限られている環境では、圧縮は非常に有効な手段です。
  3. 通信コストの削減: データ転送量に応じて料金が決まるような通信サービスでは、圧縮によって転送量を減らすことが直接的なコスト削減につながります。
  4. 効率的なデータ処理: 圧縮されたデータはディスクI/O(読み書き)の量を減らすことができるため、処理速度が向上する場合もあります。

このように、圧縮・解凍は私たちのデジタルライフにおいて非常に重要な技術基盤となっています。そして、数多くのソフトウェアやシステムで、この圧縮・解凍の機能を実現するために利用されているのが、zlibライブラリです。

zlibライブラリとは何か?

zlibは、可逆圧縮アルゴリズムであるDeflateを実装した、フリーで汎用性の高いデータ圧縮ライブラリです。Deflateは、LZ77アルゴリズムとハフマン符号化を組み合わせた強力な圧縮方式で、非常に多くのファイルフォーマット(ZIP, Gzip, PNG, TIFFなど)で採用されています。

zlibは、Jean-loup Gailly氏とMark Adler氏によって開発されました。その名前は、有名なデータ圧縮ユーティリティであるgzipに由来しています。zlibは、gzipと同様の圧縮アルゴリズムを使用していますが、ファイル形式の詳細(ヘッダーやフッター)に依存せず、純粋なDeflateデータストリームを扱うことができるため、より汎用的に様々なアプリケーションに組み込むことが可能です。

zlibの主な特徴は以下の通りです。

  • 高い汎用性: 特定のファイル形式に限定されず、様々なデータストリームやバッファに対して圧縮・解凍を適用できます。
  • 可逆圧縮: 圧縮されたデータを完全に元の状態に戻すことができます。データが失われることはありません(非可逆圧縮とは異なります)。
  • 高い圧縮率と処理速度: Deflateアルゴリズムは、比較的高い圧縮率と良好な処理速度のバランスが取れています。
  • クロスプラットフォーム: Windows, macOS, Linux, BSD, スマートフォンのOSなど、非常に多くのオペレーティングシステムやハードウェアプラットフォームで動作します。
  • フリーソフトウェア: Zlib Licenseという非常に寛容なライセンスで提供されており、商用・非商用を問わず、ほとんど制約なく自由に利用・再配布・改変が可能です。
  • C言語で書かれている: 主要な言語はC言語ですが、多くのプログラミング言語(Python, Java, C++, Rubyなど)から利用するためのラッパーライブラリが存在します。

なぜzlibを学ぶのか?

あなたがプログラマーであれば、ソフトウェア開発においてzlibに出会う機会は非常に多いでしょう。例えば:

  • ファイルの圧縮・解凍機能を実装する。
  • ネットワーク経由でデータを送受信する際に、帯域幅を節約するためにデータを圧縮する。
  • 画像ファイル(PNGなど)やアーカイブファイル(ZIP, Gzipなど)の読み書きを行うライブラリ内部で利用されている。
  • ゲーム開発でアセットデータのサイズを削減する。
  • データベースシステムでデータを圧縮して保存する。

zlibは、これらの様々な場面で「デファクトスタンダード(事実上の標準)」として広く利用されています。zlibの使い方を理解することは、多くのソフトウェア開発において役立つスキルの習得につながります。

この記事では、zlibの基本的な使い方、特にC言語を用いたプログラミング方法について、初心者向けに詳しく解説していきます。後半では、より実践的な利用方法や、知っておくと便利な知識についても触れていきます。

圧縮・解凍の基本原理:Deflateアルゴリズムの概要

zlibが採用しているDeflateアルゴリズムは、どのようにしてデータを小さくするのでしょうか? Deflateは、主に二つの技術を組み合わせています。

  1. LZ77アルゴリズムに基づくデータ圧縮:
    LZ77アルゴリズムは、イスラエル人のJacob Ziv氏とAbraham Lempel氏によって1977年に発表されました。このアルゴリズムの基本的なアイデアは、「データストリームの中に繰り返し現れるパターン(文字列)を見つけ、それを以前に出現した同じパターンの位置と長さで置き換える」というものです。

    例えば、「ABABABC」という文字列があったとします。
    – 最初の「A」はそのまま出力します。
    – 次の「B」もそのまま出力します。
    – 「AB」は既に出現しています。「A」から1文字戻った位置から2文字が「AB」です。これを「(1, 2)」のように符号化します(位置, 長さ)。
    – 「ABC」は既に出現した「AB」の後に「C」が続いたものです。「A」から1文字戻った位置から2文字+「C」として「(1, 2)C」のように符号化します。あるいは、「A」から3文字戻った位置から3文字として「(3, 3)」のように符号化するかもしれません。

    このように、長い文字列の繰り返しを、短い「(位置, 長さ)」のペアで置き換えることで、データを圧縮します。位置は、現在の場所からどれだけ前に戻るかを示し、長さは一致した文字列の長さを示します。Deflateでは、最大32768バイト(32KB)前までを探索し、最大258バイトの長さの一致を検出できます。

    テキストデータ、プログラムコード、一部の画像データなど、繰り返しパターンが多いデータに対して、LZ77は高い圧縮効果を発揮します。

  2. ハフマン符号化に基づくエントロピー符号化:
    LZ77によって繰り返しパターンが置き換えられた後、データは「そのままのバイト列」と「(位置, 長さ)のペア」の組み合わせになります。これらのデータ要素をさらに効率的に表現するために、ハフマン符号化が使われます。

    ハフマン符号化は、データの出現頻度に基づいて、短い符号(ビット列)を割り当てる技術です。出現頻度の高いデータ要素には短いビット列を、出現頻度の低いデータ要素には長いビット列を割り当てます。これにより、データ全体のビット数を減らします。

    例えば、アルファベットA, B, C, Dがあり、その出現頻度が以下のようだったとします。
    – A: 50%
    – B: 20%
    – C: 20%
    – D: 10%

    もし固定長の2ビットで符号化すると、A=00, B=01, C=10, D=11 となり、1文字あたり平均2ビット必要です。
    ハフマン符号化では、出現頻度に応じて以下のように可変長の符号を割り当てることができます。
    – A: 0 (1ビット)
    – B: 10 (2ビット)
    – C: 110 (3ビット)
    – D: 111 (3ビット)

    この場合、1文字あたりの平均ビット数は (0.5 * 1) + (0.2 * 2) + (0.2 * 3) + (0.1 * 3) = 0.5 + 0.4 + 0.6 + 0.3 = 1.8ビットとなり、固定長よりも効率的になります。

    ハフマン符号化は、符号化されたビット列を元のデータに一意に復元できる(プレフィックス符号という性質を持つ)ように設計されます。

Deflateの仕組みまとめ:

Deflateアルゴリズムは、入力データをまずLZ77によって「リテラルバイト(そのままの文字)」と「繰り返しパターンの参照(位置, 長さ)」のシーケンスに変換します。次に、このシーケンスを構成する各要素(リテラルバイトと(位置, 長さ)ペアを表すシンボル)に対して、ハフマン符号化を適用して最終的な圧縮ビット列を生成します。

zlibは、このDeflateアルゴリズムを高速かつ効率的に実装しています。データの特性に応じて最適な圧縮レベル(圧縮率と速度のバランス)を選択できるようになっています。

zlibライブラリの導入

zlibは非常にポータブルなライブラリであり、多くのシステムにプリインストールされているか、簡単にインストールできるようになっています。ここでは、主にC/C++で利用する場合の導入方法について説明します。

ソースコードからのビルド

zlibの公式ウェブサイト (https://zlib.net/) から最新のソースコードをダウンロードできます。ソースコードからのビルドは、様々な環境で利用できる最も確実な方法です。

標準的なUnix/Linux/macOS環境であれば、ダウンロードしたtar.gzファイルを展開後、ターミナルで以下のコマンドを実行します。

bash
tar xvf zlib-x.y.z.tar.gz
cd zlib-x.y.z
./configure
make
sudo make install

  • ./configure: システム環境に合わせてMakefileを作成します。
  • make: ライブラリ本体(libz.a または libz.so/libz.dylib)をビルドします。
  • sudo make install: システムの標準的な場所にライブラリファイルとヘッダーファイル(zlib.h, zconf.h)をインストールします。システムによってはsudoが必要ない場合もあります。

Windows環境でMSVC (Visual Studio) を使用する場合、ソースコードに含まれるwin32/Makefile.mscファイルなどを参照してビルドを行います。例えば、Developer Command Promptで以下のように実行します。

cmd
cd zlib-x.y.z
nmake -f win32/Makefile.msc

MinGWやCygwinなどのUnix風環境であれば、上記Unix/Linux/macOSと同様のコマンドでビルドできることが多いです。

ビルドが成功すると、ライブラリファイルとヘッダーファイルが生成されます。これらを自分のプロジェクトから参照できるように設定します。

パッケージマネージャーを利用した導入

多くのオペレーティングシステムや開発環境では、パッケージマネージャーを使って簡単にzlibをインストールできます。

  • Debian/Ubuntu:
    bash
    sudo apt update
    sudo apt install zlib1g-dev

    開発用のヘッダーファイルや静的ライブラリなどが含まれる-devパッケージをインストールします。

  • Fedora/CentOS/RHEL:
    bash
    sudo dnf install zlib-devel
    # または yum install zlib-devel

  • macOS (Homebrew):
    bash
    brew install zlib

    Homebrewでインストールした場合、通常はシステム標準のzlibが優先されます。明示的にHomebrew版を使うには、コンパイル時に適切なオプションを指定する必要がありますが、多くの場合システム標準版で十分です。

  • Windows (vcpkg, msys2など):
    vcpkgやMSYS2のようなパッケージマネージャーを利用すると、Windows環境でも他のプラットフォームと同様に簡単に導入できます。

パッケージマネージャーを使うのが最も手軽で推奨される方法です。

他の言語からの利用

Python, Java, C++, Rubyなど、多くのプログラミング言語には、zlibを呼び出すための標準ライブラリや外部ライブラリ(ラッパーライブラリ)が存在します。

  • Python: 標準ライブラリにzlibモジュールがあります。
  • Java: 標準ライブラリにjava.util.zip.Deflaterjava.util.zip.Inflaterクラスがあります。これらは内部でzlibを呼び出しています。
  • C++: zlib自体がC言語ライブラリなので、C++からそのまま利用できます。Boostライブラリのboost::iostreamsなど、zlibをラップしてC++のストリームとして扱えるようにするライブラリもあります。
  • Ruby: 標準ライブラリにzlibモジュールがあります。

これらの言語を利用する場合は、各言語のドキュメントを参照して、それぞれの方法でzlibの機能を利用してください。本記事では、zlibのコア機能に焦点を当てるため、主にC言語での使い方を解説します。

zlibを使った基本的な圧縮・解凍処理(C言語)

ここでは、zlibライブラリを使って、メモリ上のデータを圧縮・解凍する最も基本的な方法をC言語のコード例で示します。

必要なヘッダーファイル

zlibの関数を利用するためには、zlib.hヘッダーファイルをインクルードする必要があります。

“`c

include

include // 入出力関数用

include // memcpy用

include // exit用

“`

zlibの基本的な関数

zlibでの圧縮・解凍処理は、ストリーム(連続的なデータフロー)として扱われます。処理の状態は z_stream という構造体で管理されます。

主な関数と構造体メンバー:

  • z_stream strm;: 処理状態を保持する構造体。
  • strm.zalloc = Z_NULL;: メモリ割り当て関数ポインタ。通常はZ_NULL(標準のアロケータを使用)を設定。
  • strm.zfree = Z_NULL;: メモリ解放関数ポインタ。通常はZ_NULLを設定。
  • strm.opaque = Z_NULL;: アロケータ関数に渡される任意のデータ。通常はZ_NULLを設定。
  • strm.avail_in;: 入力バッファに残っているバイト数。
  • strm.next_in;: 次に読み込む入力データの先頭へのポインタ。
  • strm.avail_out;: 出力バッファに書き込める空き容量(バイト数)。
  • strm.next_out;: 次に書き込む出力バッファの先頭へのポインタ。

圧縮処理

圧縮処理の基本的な流れは以下のようになります。

  1. z_stream 構造体の初期化。
  2. deflateInit または deflateInit2 関数で圧縮ストリームを初期化。
  3. 入力データを strm.next_instrm.avail_in に設定し、出力バッファを strm.next_outstrm.avail_out に設定。
  4. deflate 関数を繰り返し呼び出して圧縮処理を実行。
    • deflate関数は、入力データを読み込み、圧縮結果を出力バッファに書き込みます。
    • 入力データがすべて処理されるか、出力バッファがいっぱいになるまで処理を行います。
    • Z_NO_FLUSHZ_SYNC_FLUSHZ_FULL_FLUSHZ_FINISHなどのフラグを指定できます。通常、処理中はZ_NO_FLUSHを使い、最後の呼び出しでZ_FINISHを指定します。
  5. deflate 関数の戻り値を確認し、処理の継続または終了を判断。Z_OKは処理継続、Z_STREAM_ENDは全データ処理完了を示します。
  6. 必要に応じて、出力バッファの内容を別の場所へ移動させ、出力バッファを再設定して deflate を再度呼び出すことを繰り返します。
  7. 入力データがすべて処理され (Z_FINISH フラグを指定)、deflate 関数が Z_STREAM_END を返したら処理完了。
  8. deflateEnd 関数で圧縮ストリームを終了し、確保したリソースを解放。

解凍処理

解凍処理の基本的な流れは以下のようになります。

  1. z_stream 構造体の初期化。
  2. inflateInit または inflateInit2 関数で解凍ストリームを初期化。
  3. 入力データ (strm.next_in, strm.avail_in) と出力バッファ (strm.next_out, strm.avail_out) を設定。
  4. inflate 関数を繰り返し呼び出して解凍処理を実行。
    • inflate関数は、入力データを読み込み、解凍結果を出力バッファに書き込みます。
    • 入力データがすべて処理されるか、出力バッファがいっぱいになるまで処理を行います。
    • Z_NO_FLUSHZ_SYNC_FLUSHなどのフラグを指定できますが、解凍では通常Z_NO_FLUSHを使います。
  5. inflate 関数の戻り値を確認し、処理の継続または終了を判断。Z_OKは処理継続、Z_STREAM_ENDは全データ処理完了を示します。Z_BUF_ERRORは出力バッファがいっぱいで処理が中断されたことを示しますが、エラーではなく、出力バッファを空けて再度呼び出せば継続できます。
  6. 必要に応じて、出力バッファの内容を別の場所へ移動させ、出力バッファを再設定して inflate を再度呼び出すことを繰り返します。
  7. 入力データがすべて処理され、inflate 関数が Z_STREAM_END を返したら処理完了。
  8. inflateEnd 関数で解凍ストリームを終了し、確保したリソースを解放。

簡単なコード例:メモリ to メモリの圧縮・解凍

この例では、短い文字列をメモリ上で圧縮し、それを解凍して元の文字列に戻します。実際のアプリケーションでは、ファイルやネットワークストリームに対して行うことが多いですが、基本概念は同じです。

“`c

include

include

include

include

// 入出力バッファのサイズ

define CHUNK 16384 // 16KB

// エラーコードを文字列に変換するヘルパー関数 (簡易版)
char* zerr(int ret) {
switch (ret) {
case Z_OK: return “Z_OK”;
case Z_STREAM_END: return “Z_STREAM_END”;
case Z_NEED_DICT: return “Z_NEED_DICT”;
case Z_ERRNO: return “Z_ERRNO”; // 実際にはerrnoを見る
case Z_STREAM_ERROR: return “Z_STREAM_ERROR”;
case Z_DATA_ERROR: return “Z_DATA_ERROR”;
case Z_MEM_ERROR: return “Z_MEM_ERROR”;
case Z_BUF_ERROR: return “Z_BUF_ERROR”;
case Z_VERSION_ERROR: return “Z_VERSION_ERROR”;
default: return “UNKNOWN_ERROR”;
}
}

// メモリ上のデータを圧縮する関数
// source: 入力データ
// sourceLen: 入力データの長さ
// dest: 出力バッファ (圧縮後のデータが格納される)
// destLen: 出力バッファの最大サイズ
// 成功した場合はZ_OK、それ以外はエラーコードを返す
int compress_memory(unsigned char source, unsigned long sourceLen,
unsigned char
dest, unsigned long *destLen) {

int ret;
z_stream strm;

// z_stream構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;

// 圧縮ストリームの初期化
// Z_DEFAULT_COMPRESSIONはデフォルトの圧縮レベル
ret = deflateInit(&strm, Z_DEFAULT_COMPRESSION);
if (ret != Z_OK) {
    fprintf(stderr, "deflateInit failed: %s\n", zerr(ret));
    return ret;
}

// 入力バッファと出力バッファの設定
strm.avail_in = sourceLen; // 入力データの全サイズ
strm.next_in = source;     // 入力データの先頭ポインタ

strm.avail_out = *destLen; // 出力バッファのサイズ
strm.next_out = dest;      // 出力バッファの先頭ポインタ

// 圧縮処理の実行
// Z_FINISHフラグは、これですべての入力が与えられ、出力バッファがすべてフラッシュされるまで待つことを示す
ret = deflate(&strm, Z_FINISH);

// deflate関数の戻り値のチェック
// Z_STREAM_ENDは、圧縮処理が正常に完了し、すべての出力が生成されたことを示す
if (ret != Z_STREAM_END) {
    fprintf(stderr, "deflate failed: %s (after Z_FINISH)\n", zerr(ret));
    deflateEnd(&strm); // 終了処理を忘れずに
    return ret;
}

// 圧縮後のデータサイズを取得
// strm.avail_out は、出力バッファの残りサイズを示している
// 元の出力バッファサイズから残りを引けば、書き込まれたデータサイズになる
*destLen = *destLen - strm.avail_out;

// 圧縮ストリームの終了処理
deflateEnd(&strm);

return Z_OK;

}

// メモリ上のデータを解凍する関数
// source: 入力データ (圧縮済み)
// sourceLen: 入力データの長さ
// dest: 出力バッファ (解凍後のデータが格納される)
// destLen: 出力バッファの最大サイズ
// actualDestLen: 実際に解凍されたデータの長さを格納するポインタ
// 成功した場合はZ_OK、それ以外はエラーコードを返す
int decompress_memory(unsigned char source, unsigned long sourceLen,
unsigned char
dest, unsigned long destLen,
unsigned long *actualDestLen) {

int ret;
z_stream strm;

// z_stream構造体の初期化
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;

// 解凍ストリームの初期化
// inflateInitは、Deflateデータストリームを解凍するために使用
ret = inflateInit(&strm);
if (ret != Z_OK) {
    fprintf(stderr, "inflateInit failed: %s\n", zerr(ret));
    return ret;
}

// 入力バッファと出力バッファの設定
strm.avail_in = sourceLen; // 入力データ (圧縮済み) の全サイズ
strm.next_in = source;     // 入力データ (圧縮済み) の先頭ポインタ

strm.avail_out = destLen;  // 出力バッファ (解凍用) のサイズ
strm.next_out = dest;      // 出力バッファ (解凍用) の先頭ポインタ

// 解凍処理の実行
// Z_NO_FLUSHは、可能なだけ解凍し、バッファがいっぱいにならなければ待つことを示す
// この例ではメモリ to メモリでバッファサイズが固定なのでZ_NO_FLUSHで一度に処理できる
// ストリーム処理の場合はループが必要
ret = inflate(&strm, Z_NO_FLUSH);

// inflate関数の戻り値のチェック
// Z_STREAM_ENDは、解凍処理が正常に完了し、すべての出力が生成されたことを示す
if (ret != Z_STREAM_END) {
    fprintf(stderr, "inflate failed: %s\n", zerr(ret));
    inflateEnd(&strm); // 終了処理を忘れずに
    return ret;
}

// 実際に解凍されたデータサイズを取得
// strm.avail_out は、出力バッファの残りサイズを示している
// 元の出力バッファサイズから残りを引けば、書き込まれたデータサイズになる
*actualDestLen = destLen - strm.avail_out;

// 解凍ストリームの終了処理
inflateEnd(&strm);

return Z_OK;

}

int main() {
const char *original_data = “This is a simple test string for zlib compression and decompression.”
“It contains some repeated patterns to show the compression effect.”
“Let’s make it a bit longer to see how it works with more data.”
“ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789”
“Repeat Repeat Repeat Repeat Repeat Repeat Repeat Repeat Repeat”;

unsigned long original_len = strlen(original_data) + 1; // 終端NULLも含める

// 圧縮後のデータ格納用バッファ
// zlibのドキュメントでは、圧縮後の最大サイズは元サイズの0.1% + 12バイト + 元サイズ
// 程度になるとされているが、安全側に十分に大きなバッファを確保する
// ここでは簡単な例のため、元サイズより少し大きく確保
unsigned long compressed_buf_len = original_len * 2; // 十分なサイズを仮定
unsigned char *compressed_data = (unsigned char *)malloc(compressed_buf_len);
if (!compressed_data) {
    fprintf(stderr, "Failed to allocate memory for compressed data\n");
    return 1;
}
unsigned long actual_compressed_len = compressed_buf_len; // compress_memory関数に渡すために初期化

// 圧縮処理
printf("Original data length: %lu bytes\n", original_len);
int ret = compress_memory((unsigned char *)original_data, original_len,
                          compressed_data, &actual_compressed_len);

if (ret != Z_OK) {
    fprintf(stderr, "Compression failed!\n");
    free(compressed_data);
    return 1;
}

printf("Compressed data length: %lu bytes\n", actual_compressed_len);
printf("Compression ratio: %.2f%%\n", (double)actual_compressed_len / original_len * 100.0);

// 解凍後のデータ格納用バッファ
// 解凍後のサイズは元データサイズと等しいはず
unsigned long decompressed_buf_len = original_len;
unsigned char *decompressed_data = (unsigned char *)malloc(decompressed_buf_len);
if (!decompressed_data) {
    fprintf(stderr, "Failed to allocate memory for decompressed data\n");
    free(compressed_data);
    return 1;
}
unsigned long actual_decompressed_len = 0; // 解凍後の実際のサイズ

// 解凍処理
ret = decompress_memory(compressed_data, actual_compressed_len,
                        decompressed_data, decompressed_buf_len,
                        &actual_decompressed_len);

if (ret != Z_OK) {
    fprintf(stderr, "Decompression failed!\n");
    free(compressed_data);
    free(decompressed_data);
    return 1;
}

printf("Decompressed data length: %lu bytes\n", actual_decompressed_len);

// 元のデータと解凍されたデータが一致するか確認
if (actual_decompressed_len != original_len ||
    memcmp(original_data, decompressed_data, original_len) != 0) {
    fprintf(stderr, "Data mismatch after decompression!\n");
    // 一致しない場合はデータを表示してみる
    // printf("Original:\n%s\n", original_data); // NULL文字が含まれる可能性があるので注意
    // printf("Decompressed:\n%s\n", decompressed_data);
    ret = 1; // エラーとして終了
} else {
    printf("Data decompressed successfully and matches original data.\n");
    ret = 0; // 成功
}

// メモリ解放
free(compressed_data);
free(decompressed_data);

return ret;

}
“`

コードの解説

  1. #include <zlib.h>: zlibの機能を使うために必須です。
  2. z_stream strm;: zlibの全ての操作はこの構造体を中心に進められます。入力データ、出力バッファ、処理の状態などがこの構造体に含まれます。
  3. strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL;: メモリ管理をzlibのデフォルト実装に任せるための設定です。通常はこのように設定します。
  4. deflateInit(&strm, Z_DEFAULT_COMPRESSION);: 圧縮処理を開始するための初期化関数です。
    • 第一引数には z_stream 構造体へのポインタを渡します。
    • 第二引数は圧縮レベルです(0から9)。Z_DEFAULT_COMPRESSIONは通常6に相当し、速度と圧縮率のバランスが良い設定です。
    • この関数は、内部で圧縮に必要な状態やバッファを確保します。成功すると Z_OK を返します。
  5. strm.avail_in = sourceLen; strm.next_in = source;: 圧縮したい入力データ(source)とそのサイズ(sourceLen)を z_stream 構造体に設定します。next_inは次に読み込むデータの先頭を指し、avail_inはそこから読み込めるバイト数です。
  6. strm.avail_out = *destLen; strm.next_out = dest;: 圧縮結果を書き込む出力バッファ(dest)とその空き容量(*destLen)を設定します。next_outは次に書き込む場所を指し、avail_outはそこから書き込めるバイト数です。
  7. ret = deflate(&strm, Z_FINISH);: 圧縮処理を実行します。
    • 第一引数には z_stream 構造体へのポインタを渡します。
    • 第二引数には処理を制御するフラグを指定します。Z_FINISHは、これが入力の最後のチャンクであり、全ての圧縮結果をflushしてストリームを終了することを示します。通常、ストリーム処理の最後の呼び出し以外では Z_NO_FLUSH を使います。
    • deflate関数は、avail_inからデータを読み込み、avail_outへ圧縮結果を書き込みます。avail_inavail_outは処理の進行に応じて自動的に更新されます。
    • Z_STREAM_ENDが返されたら圧縮完了です。
  8. *destLen = *destLen - strm.avail_out;: deflate処理が終わった後、strm.avail_outには出力バッファに書き込めずに残った容量が入っています。元の出力バッファサイズからこれを引くことで、実際に書き込まれた(圧縮された)データのサイズが得られます。
  9. deflateEnd(&strm);: 圧縮ストリームを終了し、deflateInitなどで確保された内部リソースを解放します。処理が完了したら必ず呼び出す必要があります。
  10. inflateInit(&strm);: 解凍処理を開始するための初期化関数です。
    • deflateInitと同様に、z_stream構造体へのポインタを渡します。
    • 解凍には圧縮レベルなどのパラメータは不要です。
    • 成功すると Z_OK を返します。
  11. strm.avail_in = sourceLen; strm.next_in = source;: 解凍したい入力データ(圧縮済み)とそのサイズを設定します。
  12. strm.avail_out = destLen; strm.next_out = dest;: 解凍結果を書き込む出力バッファとその空き容量を設定します。
  13. ret = inflate(&strm, Z_NO_FLUSH);: 解凍処理を実行します。
    • Z_NO_FLUSHは、利用可能な全ての入力データを使って解凍を進め、可能な限り多くの出力を生成しますが、内部状態は維持して次の入力に備えることを示します。メモリtoメモリの例では、入力データが全て揃っており、出力バッファも十分に確保されていると仮定しているため、一度の呼び出しで完了できる可能性があります。
    • Z_STREAM_ENDが返されたら解凍完了です。
  14. *actualDestLen = destLen - strm.avail_out;: 圧縮時と同様に、出力バッファの残容量から書き込まれた解凍後のデータサイズを計算します。
  15. inflateEnd(&strm);: 解凍ストリームを終了し、リソースを解放します。

このコード例は、入力データがすべてメモリ上にあり、出力バッファサイズも事前に分かっている(または十分に大きく確保できる)という単純なケースです。しかし、実際のファイルI/Oやネットワーク通信では、データを小さなチャンク(塊)に分けて繰り返し処理する必要があります。

より実践的な使い方:ファイル圧縮・解凍とストリーム処理

前述のメモリtoメモリの例は単純化されていましたが、実際のアプリケーションでは通常、ファイルやネットワーク接続からデータを読み込み、圧縮/解凍しながら別のファイルやネットワーク接続に書き込みます。この場合、データは「ストリーム」として扱われ、一度にすべてのデータをメモリに読み込むのではなく、チャンクごとに処理を行います。

ファイル圧縮の基本的な流れ(ストリーム処理)

  1. 入力ファイルと出力ファイルを開く。
  2. z_stream 構造体を初期化。
  3. deflateInit で圧縮ストリームを初期化。
  4. 入力バッファと出力バッファを用意する(例: サイズ CHUNK の配列)。
  5. 以下のループ処理を行う:
    • 入力ファイルからデータを読み込み、入力バッファに格納。読み込んだサイズを strm.avail_in に、バッファの先頭を strm.next_in に設定。
    • 出力バッファの空き容量と先頭を strm.avail_out, strm.next_out に設定。
    • deflate(&strm, Z_NO_FLUSH) を呼び出し、利用可能な入力データを圧縮して出力バッファに書き込む。
    • deflateの戻り値を確認。Z_OK または Z_STREAM_END なら続行可能。エラーの場合はループを中断。
    • strm.avail_out が 0 になったら(出力バッファがいっぱいになったら)、出力バッファの内容をファイルに書き出す。出力バッファをリセットし、strm.next_outstrm.avail_out を再設定。
    • 入力ファイルからの読み込みが EOF (End Of File) に達したら、deflate(&strm, Z_FINISH) を呼び出し、最終的な圧縮結果を取得・書き出し。deflateZ_STREAM_END を返すまでこのステップを繰り返す。
  6. ループ終了後、deflateEnd でストリームを終了。
  7. ファイルを閉じる。

ファイル解凍の基本的な流れ(ストリーム処理)

  1. 入力ファイル(圧縮済み)と出力ファイルを開く。
  2. z_stream 構造体を初期化。
  3. inflateInit で解凍ストリームを初期化。
  4. 入力バッファと出力バッファを用意する(例: サイズ CHUNK の配列)。
  5. 以下のループ処理を行う:
    • 入力バッファにデータがない場合、入力ファイルからデータを読み込み、バッファに格納。読み込んだサイズを strm.avail_in に、バッファの先頭を strm.next_in に設定。読み込みが EOF なら入力は終わり。
    • 出力バッファの空き容量と先頭を strm.avail_out, strm.next_out に設定。
    • inflate(&strm, Z_NO_FLUSH) を呼び出し、利用可能な入力データから解凍し、出力バッファに書き込む。
    • inflate の戻り値を確認。Z_OK なら続行可能。Z_STREAM_END なら解凍完了。Z_BUF_ERROR は出力バッファがいっぱいだが、入力はまだ残っていることを示す(エラーではない)。その他のエラーの場合はループを中断。
    • strm.avail_out が処理前より減っていたら(出力バッファにデータが書き込まれたら)、書き込まれた部分(元の avail_out から現在の strm.avail_out を引いたバイト数)をファイルに書き出す。出力バッファをリセットし、strm.next_outstrm.avail_out を再設定。
    • inflateZ_STREAM_END を返したらループを終了。
  6. ループ終了後、inflateEnd でストリームを終了。
  7. ファイルを閉じる。

圧縮レベルの設定

deflateInit 関数や deflateInit2 関数では、第二引数に圧縮レベルを指定できます。

  • Z_NO_COMPRESSION (0): 圧縮を行わず、ストア(そのまま格納)します。ヘッダーとフッターは付加されますが、データサイズはほとんど変わりません。最も高速です。
  • Z_BEST_SPEED (1): 最も高速な圧縮設定です。圧縮率は低くなります。
  • 1 から 9: 圧縮率が徐々に高くなります。数字が大きいほど圧縮率は高くなりますが、処理速度は遅くなります。
  • Z_BEST_COMPRESSION (9): 最も圧縮率の高い設定です。処理速度は最も遅くなります。
  • Z_DEFAULT_COMPRESSION (-1): zlibのデフォルト設定(通常6)です。速度と圧縮率のバランスが良い設定です。

アプリケーションの要件(速度重視か、圧縮率重視か)に応じて適切なレベルを選択します。

ウィンドウサイズとメモリ管理

deflateInit2inflateInit2 関数を使うと、圧縮・解凍のためのより詳細なパラメータ(ウィンドウサイズ、メモリレベル、圧縮方式など)を設定できます。

  • ウィンドウサイズ: LZ77アルゴリズムが繰り返しパターンを探索する際の「履歴バッファ」のサイズです。ウィンドウサイズが大きいほど、より遠い位置のパターンを見つけられる可能性が高まり、圧縮率が向上する可能性がありますが、使用するメモリが増加します。deflateInit2inflateInit2windowBitsパラメータで設定します(通常9から15の値を指定し、2の累乗バイトになります。例: 15なら32KB)。デフォルトは15です。
  • メモリレベル: 圧縮アルゴリズムが内部で使用するメモリ量を制御します。deflateInit2memLevelパラメータで設定します(1から9)。数字が大きいほど多くのメモリを使用しますが、圧縮率や速度が向上する可能性があります。デフォルトは8です。

これらの高度なパラメータは、通常はデフォルト設定で十分です。特に初心者の方は、まずは deflateInitinflateInit、そして圧縮レベルの指定に慣れるのが良いでしょう。

メモリ管理 (zalloc, zfree, opaque) も deflateInitinflateInit のデフォルト設定 (Z_NULL) で、標準の mallocfree が使われます。特殊なメモリ管理が必要な組み込み環境などでなければ、通常はデフォルトのままで問題ありません。

Gzip形式との連携

zlibは純粋なDeflateデータストリームを扱いますが、Deflateは単独のフォーマットではなく、他のフォーマット(ZIP, Gzip, PNGなど)の中で使われる圧縮方式です。特にGzipは、Deflateデータにヘッダー(ファイル名、タイムスタンプなど)とフッター(データサイズ、チェックサムなど)を付加したシンプルなファイルフォーマットです。

zlibライブラリは、Deflateデータストリームだけでなく、Gzip形式のデータも扱うための機能を提供しています。これは主に deflateInit2 および inflateInit2 関数で行います。

deflateInit2windowBits パラメータに、通常のウィンドウサイズ (9〜15) に16を加えた値を指定することで、Gzipヘッダーとフッター付きのデータを生成するようになります。

例: deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY);
ここで 15 + 16 = 31 を指定すると、32KBウィンドウでGzip形式の出力を生成します。

inflateInit2windowBits パラメータに、通常のウィンドウサイズ (9〜15) に16を加えた値、または特殊な値 (-15から-9) を指定することで、Gzip形式の入力を解凍できるようになります。

  • windowBits = 9 から 15: 純粋なDeflateストリームを解凍(zlibヘッダー/フッターを使用)。inflateInitと同じ。
  • windowBits = -9 から -15: ヘッダーやフッターがない、生のDeflateストリームを解凍。
  • windowBits = 9+16 から 15+16 (つまり 25 から 31): DeflateストリームまたはGzipストリームを自動判別して解凍。最もよく使われる設定です。Gzipヘッダー/フッターは無視されます。

例: inflateInit2(&strm, Z_DEFLATED, 15 + 16);
ここで 15 + 16 = 31 を指定すると、Gzip形式の入力も自動的に解凍できます。

これらの関数を使うことで、gzipコマンドで作成されたファイルをzlibで解凍したり、zlibでGzip互換のファイルを作成したりすることが可能になります。

例えば、ファイル圧縮の例で、deflateInitdeflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY) に置き換え、生成されるファイルの拡張子を .gz にすれば、gzip互換の圧縮ファイルを作成できます。解凍も同様に inflateInitinflateInit2(&strm, Z_DEFLATED, 15 + 16) に置き換えれば、.gz ファイルを解凍できます。

高度なトピック:チェックサムとエラー検出

zlibは、圧縮データが転送中や保存中に破損していないかを確認するためのチェックサム機能を提供しています。zlibがサポートしているチェックサムアルゴリズムは二つあります。

  1. Adler-32:
    Mark Adler氏によって開発された、比較的高速なチェックサムアルゴリズムです。zlibのDeflateストリームのフッターにはAdler-32チェックサムが含まれています。

    • 関数: adler32(adler, buf, len)
      • adler: 前回の adler32 計算結果、または初期値 adler32(0L, Z_NULL, 0)
      • buf: チェックサムを計算するデータのポインタ。
      • len: データの長さ。
    • 使い方: データをチャンクごとに処理する場合、各チャンクに対して adler32 を呼び出し、前回の結果を次の呼び出しに渡します。最後の呼び出しで得られた値が、データ全体のAdler-32チェックサムとなります。解凍時にも同じ計算を行い、フッターに含まれるチェックサムと比較することで、データが破損していないか確認できます。
  2. CRC-32:
    循環冗長検査 (Cyclic Redundancy Check) の一種で、Adler-32よりも堅牢な(破損検出能力の高い)チェックサムアルゴリズムです。Gzip形式のフッターにはCRC-32チェックサムが含まれています。

    • 関数: crc32(crc, buf, len)
      • crc: 前回の crc32 計算結果、または初期値 crc32(0L, Z_NULL, 0)
      • buf: チェックサムを計算するデータのポインタ。
      • len: データの長さ。
    • 使い方: Adler-32と同様に、チャンクごとに計算できます。Gzip形式で圧縮する場合、zlibが自動的にCRC-32を計算してフッターに含めてくれます。手動で計算することも可能です。

これらのチェックサムは、データ圧縮・解凍処理とは独立して、データの信頼性を検証するために利用できます。例えば、ダウンロードしたファイルが破損していないか確認する際に使われます。

zlibライブラリは、圧縮/解凍中にこれらのチェックサムを内部で計算し、ストリーム構造体 (z_stream) の adler メンバーに格納します。Gzip形式の圧縮/解凍を行う場合は、自動的にCRC-32チェックサムが計算されます。

ディクショナリ圧縮

Deflateアルゴリズムは、既に出現したデータパターンを参照することで圧縮を行います。この「既に出現したデータ」をあらかじめ用意しておくことで、圧縮率を向上させられる場合があります。これがディクショナリ圧縮です。

  • deflateSetDictionary(strm, dictionary, dictLength): 圧縮を開始する前に、ディクショナリとして使用するデータを設定します。ディクショナリの内容は、圧縮されるデータの先頭にあるものとして扱われます。
  • inflateSetDictionary(strm, dictionary, dictLength): 解凍を開始する前に、圧縮時に使用されたのと同じディクショナリを設定します。解凍時に、圧縮されたデータがディクショナリを参照した場合に必要となります。

ディクショナリ圧縮は、同じ種類のデータを繰り返し圧縮・解凍する場合に有効です。例えば、似たような構造を持つログファイルや、ネットワークプロトコルにおける繰り返しの多いヘッダー情報などを圧縮する際に、共通部分をディクショナリとして事前に与えることで、圧縮率を向上させることが期待できます。

ただし、ディクショナリを正しく利用するためには、圧縮時と解凍時で全く同じディクショナリを使用する必要があります。また、ディクショナリ自体のサイズや、ディクショナリを選択・管理するための仕組みが必要になるため、一般的な用途ではあまり使われません。

zlibを使う上での注意点

zlibは非常に堅牢で広く使われているライブラリですが、利用する上でいくつか注意すべき点があります。

  • エラーハンドリングの重要性: zlib関数は、成功時には Z_OKZ_STREAM_END などを返しますが、失敗時には様々なエラーコードを返します (Z_MEM_ERROR, Z_DATA_ERROR, Z_STREAM_ERROR など)。これらの戻り値を必ずチェックし、エラーが発生した場合は適切に処理を中断したり、エラーメッセージを出力したりするコードを書くことが非常に重要です。特に、ファイルやネットワークからのデータ読み込み・書き込みと組み合わせるストリーム処理では、エラーが発生する可能性が高くなります。
  • バッファオーバーフローの回避: strm.avail_in, strm.next_in, strm.avail_out, strm.next_out を適切に管理することが不可欠です。入力データが avail_in を超えていたり、圧縮/解凍結果を出力バッファ (next_out) に avail_out を超えて書き込もうとすると、バッファオーバーフローを引き起こし、セキュリティ上の脆弱性やプログラムのクラッシュにつながる可能性があります。特に、出力バッファサイズが入力サイズより大きくなる可能性がある解凍処理では、出力バッファを十分に大きく確保するか、チャンクごとに書き出しながら処理する必要があります。
  • メモリリークの防止: deflateInit, inflateInit などで内部的に確保されたメモリは、必ず deflateEnd, inflateEnd で解放する必要があります。これらの終了関数を呼び忘れると、メモリリークの原因となります。エラー発生時も含め、処理が終了する全てのパスで終了関数が呼ばれるように注意が必要です。
  • 圧縮率と速度のトレードオフ: 圧縮レベルの設定によって、圧縮率と処理速度、そして使用するメモリ量が変化します。高い圧縮率を求めると速度が犠牲になり、多くのメモリを使う傾向があります。アプリケーションの要件に合わせて最適なレベルを選択することが重要です。デフォルトの Z_DEFAULT_COMPRESSION は多くのケースでバランスが良い設定ですが、特定の用途では最適なチューニングが必要になる場合があります。
  • Gzipヘッダー/フッターと純粋Deflate: inflateInit (windowBits 9-15) はzlibヘッダー/フッターを持つDeflateストリームを扱います。inflateInit2windowBits に 25-31 を指定すると、zlibまたはGzipストリームを自動判別して扱えます。Gzipファイル (.gz) を解凍したい場合は、後者の設定を使う必要があります。逆に、独自のフォーマットにDeflateデータだけを埋め込みたい場合は、前者の設定を使用します。

他の圧縮ライブラリとの比較(簡単に)

zlib以外にも、多くのデータ圧縮ライブラリが存在します。代表的なものと、zlibとの簡単な比較を以下に示します。

  • gzip: ファイル圧縮ユーティリティです。内部でzlib(またはその前身であるlibz)のDeflateアルゴリズムを使用しています。ファイル形式(ヘッダー・フッター)がzlibの純粋なDeflateストリームとは異なりますが、zlibはGzip形式も扱えます。
  • bzip2 (libbz2): Burrows-Wheeler変換とMove-to-Front変換、ハフマン符号化などを組み合わせた圧縮アルゴリズムです。Deflateよりも一般的に圧縮率が高いですが、圧縮・解凍速度は遅く、より多くのメモリを使用します。
  • xz (liblzma): LZMA (Lempel-Ziv-Markov chain Algorithm) アルゴリズムを実装したライブラリです。bzip2よりもさらに圧縮率が高い傾向がありますが、さらに速度は遅く、多くのメモリを使用します。特に大きなファイルに対して高い圧縮率を発揮します。
  • LZO (LZO Library): LZ algorithm variants optimized for speed. 非常に高速な圧縮・解凍が特徴ですが、圧縮率はzlibより低い傾向があります。リアルタイム処理や組み込みシステムなどで利用されることがあります。
  • LZ4: Deflateやbzip2、xzと比較して圧倒的に高速な圧縮・解凍が特徴です。圧縮率はそれほど高くありませんが、速度が最優先される場面(例:ログデータの高速圧縮、インメモリデータベースなど)で広く利用されています。
  • Zstandard (zstd): Facebookが開発した新しい圧縮アルゴリズムおよびライブラリです。LZ77ベースですが、Deflateよりも高速かつ圧縮率が高いという、非常に優れた性能バランスを持っています。近年採用が広がっています。
  • Brotli: Googleが開発した、特にWebコンテンツ圧縮に特化した圧縮アルゴリズムです。Deflateよりも高い圧縮率を発揮しますが、解凍速度はzstdより遅い場合があります。

なぜzlibが依然として重要なのか?

新しい高性能な圧縮ライブラリが登場しているにも関わらず、zlibは今でも非常に広く使われ続けています。その理由はいくつかあります。

  • 普及率と互換性: zlibは非常に長い歴史を持ち、ZIP, PNG, Gzipなど、数多くの標準フォーマットやプロトコルで使われています。既存のシステムとの互換性を保つ上で不可欠な存在です。
  • クロスプラットフォーム性: ほぼ全ての環境で利用可能であり、組み込みシステムのようなメモリが限られた環境でも動作します。
  • シンプルさと安定性: ライブラリのAPIが比較的シンプルで使いやすく、長年の利用実績から非常に安定しています。
  • 十分な性能: 多くの一般的な用途において、zlibの速度と圧縮率は十分な性能を提供します。

新しいプロジェクトで最高の圧縮率や速度が必要な場合はzstdやLZ4などを検討する価値がありますが、既存フォーマットの対応や、最大公約数的な互換性、手軽さを求める場合は、依然としてzlibが第一の選択肢となります。

まとめ

この記事では、zlibライブラリの紹介から始まり、その基盤となるDeflateアルゴリズムの概要、ライブラリの導入方法、そして最も基本的なC言語での使い方を詳しく解説しました。さらに、ストリーム処理によるファイル圧縮・解凍の概念、Gzip形式との連携、チェックサムやディクショナリといった高度な機能、そして利用上の注意点や他のライブラリとの比較にも触れました。

zlibは、データ圧縮・解凍というコンピュータ科学の基本的な課題を解決するための、非常に強力で汎用性の高いツールです。そのシンプルかつ効率的な設計は、多くのソフトウェアやシステムで信頼性の高いデータ処理を実現するために貢献しています。

今回紹介したコード例はメモリtoメモリの最も単純なケースでしたが、実際のファイルI/Oやネットワーク通信におけるストリーム処理の概念を理解し、deflateinflate 関数をループの中で適切に扱うことが、zlibを使いこなす上で非常に重要になります。また、エラーハンドリングを丁寧に行い、バッファ管理に注意を払うことで、安全で堅牢な圧縮・解凍機能を実装することができます。

あなたがこれからプログラミングでデータ圧縮・解凍に取り組む際に、zlibは非常に役立つライブラリとなるはずです。この記事が、その第一歩を踏み出すための手助けとなれば幸いです。

学習の次のステップ

  • ファイル圧縮・解凍のサンプルコードを書いてみる。
  • deflateInit2inflateInit2 を使ってGzip形式のデータを扱ってみる。
  • zlibの公式ドキュメント (https://zlib.net/manual.html) を参照して、詳細な関数仕様やオプションについて学ぶ。
  • チェックサム機能を実際にコードに組み込んで、データの整合性を確認してみる。
  • より複雑なストリーム処理(例:パイプ、ネットワークソケット)での利用方法を調べる。

zlibは、データ圧縮の奥深い世界への入り口です。ぜひ実際に手を動かして、その機能を体験してみてください。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール