zlib の使い方

矢田 晋

Abstract: zlib は圧縮アルゴリズムの一種である Deflate のライブラリであり,C#, Haskell, Java, Perl, Python, Ruby など,主要なプログラミング言語では,軒並み使えるように整備されています.圧縮・伸長が高速なこともあり,ディスク領域の有効利用や通信量の削減を目的として,zlib は気軽に利用できます.本記事は,C 言語から zlib を利用する方法の解説になっています.

はじめに

最新版のマニュアル公式サイトで提供されています.また,zlib のヘッダである zlib.h には,マニュアルと同様の内容がコメントとして記述されています.利用している zlib が最新版と異なる場合,zlib.h を確認する方が良いかもしれません.おそらく /usr/include/zlib.h としてインストールされています.

英語のマニュアルを読んでも細かすぎて伝わらないと思う方には,zlib の使い方を説明している日本語のウェブページが参考になると思います.最新版ではありませんが,マニュアルの和訳も存在しています.

以下,zlib のインストール方法を紹介した後,メモリ上での圧縮・伸長gzip 形式のファイルを操作する関数群について説明します.また,zlib を用いて圧縮・伸長をおこなうプログラムのサンプルコードを用意しました.Deflate とは異なる圧縮アルゴリズムをライブラリ化した libbzip2 や liblzma も zlib と同様のインタフェースを持っているので,zlib の使い方が分かっていれば,簡単に使えるようになると思います.

インストール

Debian 系の環境における開発用パッケージのインストール

$ sudo aptitude install zlib1g-dev
$ sudo apt-get install zlib1g-dev

Debian 系の環境であれば,aptitude を使って簡単にインストールできます.インストールには root 権限が必要なので,必要に応じて sudosu を使ってください.古い環境であれば,apt-get になるかもしれません.

Red Hat 系の環境における開発用パッケージのインストール

$ sudo yum install zlib-devel

Red Hat や CentOS であれば,yum を使って簡単にインストールできます.こちらも必要に応じて sudosu を使ってください.

ソースコードからのインストール

$ wget http://zlib.net/zlib-1.2.6.tar.gz
$ tar zxf zlib-1.2.6.tar.gz
$ cd zlib-1.2.6
$ ./configure
$ make
$ make check
$ sudo make install

ソースコードの tarball は公式サイトからダウンロードできます.ブラウザや wget により tarball をダウンロードした後は,configure, make, make check, make install という一般的な手順でインストールします.zlib を軽く試してみるのであれば,make check まで終わった時点で,zlib.h, zconf.h, libz.a のみをコピーして使うことも可能です.

インストールの確認

// test.c
#include <zlib.h>
int main(void) {
  z_stream strm = { .zalloc = Z_NULL, .zfree = Z_NULL, .opaque = Z_NULL };
  deflateInit(&strm, Z_DEFAULT_COMPRESSION);
  deflateEnd(&strm);
  return 0;
}
$ gcc -Wall -std=c99 test.c -lz

簡単なソースコードをコンパイル・リンクしてみれば,zlib が問題なくインストールされているかどうか分かります.リンクにおいては,zlib を指定するオプション(-lz)が必要になります.

サンプルは C 言語のコードになっています.gcc では問題なくコンパイルできますが,g++ ではエラーになるのでご注意ください.strm の初期化コードが気になる方は,プログラミング言語 C の新機能をご覧ください.

圧縮・伸長

圧縮・伸長の概要

// 外部から利用する機会のあるメンバを抜粋
typedef struct z_stream_s {
  Bytef    *next_in;  // 入力位置
  uInt     avail_in;  // 入力データの残りバイト数
  uLong    total_in;  // 入力データの合計バイト数

  Bytef    *next_out; // 出力位置
  uInt     avail_out; // 出力バッファの残りバイト数
  uLong    total_out; // 出力データの合計バイト数

  char     *msg;      // エラーメッセージ

  alloc_func zalloc;  // メモリの確保に用いる関数
  free_func  zfree;   // メモリの解放に用いる関数
  voidpf     opaque;  // zalloc と zfree に渡すポインタ
} z_stream;
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;

zlib の基本的な使い方では,z_stream という構造体を利用することになります.z_stream のオブジェクトを作成して,zalloc, zfree, opaque を設定しましょう.これらのメンバにより,zlib の内部状態を保持するためのメモリを確保・解放する方法が決まります.特段の理由がない限り,デフォルトの設定となる Z_NULL を代入しておけば問題ありません.これらのメンバを設定しておかないと,以下で説明する関数の動作は未定義になります.

z_stream のオブジェクトを初期化した後は,圧縮する場合と伸長する場合とで呼び出すべき関数が異なります.圧縮する場合は,deflateInit() で初期設定,deflate() で圧縮,deflateEnd() で終了処理となります.伸長する場合は,inflateInit() で初期設定,inflate() で伸長,inflateEnd() で終了処理となります.zlib の公式サイトにある,サンプルコードを使って圧縮・伸長の流れを説明したドキュメントがとても参考になります.

初期設定に delateInit(), inflateInit() を用いると,zlib 形式の圧縮・伸長になります.gzip 形式の圧縮・伸長については,delateInit2(), inflateInit2() による初期化が必要です.伸長においては,圧縮形式を自動判別することもできます.

圧縮前後のデータをすべてメモリ上に展開できるときや,メモリマップト I/O によりファイル全体をメモリ上にマップできるときなど,データをひとまとめにして圧縮できる状況では,compress(), compress2() を使って一息に圧縮することが可能です.同様に,uncompress() を使って一息に伸長することも可能です.これらの関数を用いる場合,z_stream は不要になります.

圧縮

int deflateInit(z_stream *strm, int level);
int ret = deflateInit(&strm, Z_DEFAULT_COMPRESSION);
assert(ret == Z_OK);

圧縮の初期設定には deflateInit() を使います.第 1 引数には zalloc, zfree, opaque を設定した z_stream のオブジェクト,第 2 引数である level には圧縮レベルを指定するようになっています.デフォルトの圧縮レベルは Z_DEFAULT_COMPRESSION(=6)であり,他に,圧縮速度を最高にする Z_BEST_SPEED(=1),圧縮率を最高にする Z_BEST_COMPRESSION(=9),圧縮を無効にする Z_NO_COMPRESSION(=0) という定数が用意されています.基本的に,数値を小さくすると圧縮時間が短くなり,大きくすると圧縮後のサイズが小さくなります.deflateInit() は失敗すると Z_OK 以外の値を返します.

int deflate(z_stream *strm, int flush);
int flush = Z_NO_FLUSH;
int ret = Z_OK;
do {
  strm.next_in = (Bytef *)...;
  strm.avail_in = ...;
  if (/* 入力が尽きていれば */) {
    flush = Z_FINISH;
  }
  do {
    strm.next_out = (Bytef *)...;
    strm.avail_out = ...;
    ret = deflate(&strm, flush);
    assert(ret != Z_STREAM_ERROR);
    // 出力バッファの中身を処理
  } while (strm.avail_out == 0);
  assert(strm.avail_in == 0);
} while (flush != Z_FINISH);
assert(ret == Z_STREAM_END);

圧縮には deflate() を使います.第 1 引数には入力データと出力バッファを設定した z_stream のオブジェクト,第 2 引数である flush には Z_NO_FLUSH を指定します.ただし,すべての入力データを指定した後は,圧縮を完了させるために Z_FINISH を指定します.また,Z_SYNC_FLUSHZ_FULL_FLUSH を指定することにより,内部状態として保持されているデータの出力を強制したり,内部状態を初期化したりすることも可能ですが,これらの機能を使う機会は少ないと思います.

入力データの設定については,z_stream のメンバである next_in, avail_in に対しておこないます.出力バッファの設定については,z_stream のメンバである next_out, avail_out に対しておこないます.next_in, next_outBytef のポインタになっているので,unsigned char のポインタを設定する場合であっても,明示的に型変換しておく方が無難です.

deflate() の返り値は Z_OK, Z_STREAM_END, Z_STREAM_ERROR, Z_BUF_ERROR の 4 種類です.圧縮が問題なく継続していれば Z_OK になります.第 2 引数が Z_FINISH のとき,圧縮が無事に完了すれば Z_STREAM_END が返り値になります.残りの 2 つはエラーを意味しますが,Z_BUF_ERROR は入力データ・出力バッファの一時的な不足を示しているだけなので,入力データ・出力バッファを設定しなおしてから,改めて deflate() を呼び出すことにより,圧縮を継続することができます.問題になるのは Z_STREAM_ERROR であり,next_in, next_outNULL のときや,内部状態が破壊されたときなどに返り値となります.

int deflateEnd(z_stream *strm);

deflate()Z_STREAM_END が返した後は,deflateEnd() により,内部状態に割り当てたメモリを解放します.圧縮が完了していない状態で deflateEnd() を呼び出すと,返り値は Z_DATA_ERROR になります.

伸長

int inflateInit(z_stream *strm);
strm.next_in = Z_NULL;
strm.avail_in = 0;
int ret = inflateInit(&strm);
assert(ret == Z_OK);

伸長は圧縮と同様の手順でおこないますが,一部のオプションがなくなるので,圧縮と比べて少し簡単になります.まず,next_in, avail_in, zalloc, zfree, opaque を設定した z_stream のオブジェクトを引数として inflateInit() を呼び出し,伸長の初期設定をおこないます.

int inflate(z_stream *strm, int flush);
int ret = Z_OK;
do {
  strm.next_in = (Bytef *)...;
  strm.avail_in = ...;
  assert(strm.avail_in != 0);
  do {
    strm.next_out = (Bytef *)...;
    strm.avail_out = ...;
    ret = inflate(&strm, Z_NO_FLUSH);
    assert((ret != Z_NEED_DICT) &&
           (ret != Z_STREAM_ERROR) &&
           (ret != Z_DATA_ERROR) &&
           (ret != Z_MEM_ERROR));
    // 出力バッファの中身を処理
  } while (strm.avail_out == 0);
} while (ret != Z_STREAM_END);

初期設定の後は,入力データと出力バッファを設定した z_stream のオブジェクトを第 1 引数として inflate() を呼び出し,データの伸長をおこないます.第 2 引数である flush には Z_NO_FLUSH を指定します.Z_NO_FLUSH 以外にも Z_SYNC_FLUSH, Z_FINISH, Z_BLOCK, Z_TREES を指定できますが,これらの機能を使う機会は少ないと思います.入出力の設定方法は,圧縮されたデータが入力となり,伸長されたデータが出力となることを除けば,圧縮の場合と同じです.next_in, avail_in に入力データ,next_out, avail_out に出力バッファを設定します.

inflate() の返り値は,deflate() の返り値 4 種類(Z_OK, Z_STREAM_END, Z_STREAM_ERROR, Z_BUF_ERROR)に Z_NEED_DICT, Z_DATA_ERROR, Z_MEM_ERROR の 3 種類を加えた 7 種類になります.伸長が問題なく継続していれば Z_OK,無事に完了すれば Z_STREAM_END になります.Z_NEED_DICT は圧縮に際して deflateSetDictionary() で指定した辞書が必要なことを意味します.伸長を継続するには,inflateSetDictionary() により辞書を設定する必要があります.辞書の内容が分からないときは,復帰できないので,エラーとして扱うことになります.

残りの 4 種類はエラーを意味しますが,圧縮の場合と同様に,Z_BUF_ERROR は入力データ・出力バッファの一時的な不足を示しているだけなので,入力データ・出力バッファを設定しなおしてから,改めて inflate() を呼び出すことにより,伸長を継続することができます.後は復帰の難しいエラーです.Z_STREAM_ERRORnext_in, next_outNULL のときや,内部状態が破壊されたときなどの返り値です.zlib によって伸長できないデータが入力されたときは Z_DATA_ERROR になり,メモリの確保に失敗したときは Z_MEM_ERROR になります.

int inflateEnd(z_stream *strm);

伸長の成否によらず,内部状態に割り当てたメモリを解放するため,inflateEnd() を呼び出す必要があります.

gzip 形式の圧縮・伸長

圧縮したデータをファイルに出力する場合,標準的なコマンド(gzip)で簡単に操作できる gzip 形式にしておいた方が便利なことがあります.gzip 形式の圧縮・伸長については,delateInit2(), inflateInit2() を利用することになります.

int deflateInit2(z_stream *strm,
                 int level,
                 int method,
                 int windowBits,
                 int memLevel,
                 int strategy);
int ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION,
    Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY);
assert(ret == Z_OK);

delateInit2()deflateInit() より多くの引数を持っています.第 3 引数である method には圧縮方法を指定するようになっていますが,zlib-1.2.6 で指定できるのは Z_DEFLATED のみであり,選択肢はありません.第 4 引数である windowBits にはウィンドウ・サイズを指定するようになっています.deflateInit() と同様の動作になるのは 15 を指定した場合ですが,この設定では zlib 形式になります.gzip 形式に切り替えるには,16 を加えた 31 を指定する必要があります.第 5 引数である memLevel にはメモリの消費量を指定するようになっています.1 以上 9 以下の値を指定することが可能であり,デフォルトの設定は 8 です.大きくしても特に嬉しいことはありません.第 6 引数である strategy には圧縮の戦略を指定するようになっており,Z_DEFAULT_STRATEGY によりデフォルトの戦略を指定できます.

int inflateInit2(z_stream *strm, int windowBits);
int ret = inflateInit2(&strm, 47);
assert(ret == Z_OK);

inflateInit2() は,第 2 引数としてウィンドウ・サイズを受け取るようになっています.deflateInit2() に対して指定した以上の値にしなければなりません.deflateInit()15 を用いるため,15 より小さい値にするのは危険です.また,deflateInit2() の場合と同様に,gzip 形式のデータを伸長するには,16 を加えた 31 を指定する必要があります.さらに 16 を加えた 47 を指定すると,ヘッダから圧縮形式を自動判別するようになります.

一括圧縮・一括伸長

int compress(Bytef *dest, uLongf *destLen,
             const Bytef *source, uLong sourceLen);
uLong compressBound(uLong sourceLen);
uLong destLen = compressBound(sourceLen);
Bytef *dest = (Bytef *)malloc(destLen);
assert(dest != NULL);
int ret = compress(dest, &destLen, source, sourceLen);
assert(ret == Z_OK);

入力データや出力バッファを分割する必要がない状況では,compress() を用いることにより,z_stream を介することなく圧縮が可能です.compressBound() により得られるサイズの領域を出力バッファとして確保した後で compress() を呼び出すことになります.compress() の第 2 引数である destLen は,出力バッファのサイズを入力する目的だけでなく,圧縮後のサイズを出力する目的でも利用されます.

int compress2(Bytef *dest, uLongf *destLen,
              const Bytef *source, uLong sourceLen, int level);

compress2() については,Z_BEST_COMPRESSION などの圧縮レベルを第 5 引数として指定することができます.ただし,ウィンドウ・サイズを指定することができないので,gzip 形式の圧縮には利用できません.

int uncompress(Bytef *dest, uLongf *destLen,
               const Bytef *source, uLong sourceLen);

伸長には uncompress() を利用します.出力バッファのサイズを求める手段は用意されていないので,圧縮の段階で元のサイズを保存しておく必要があります.

gzip 形式のファイル

ファイル操作の概要

zlib には gzip 形式のファイルを操作するための型(gzFile)と関数群(gz*())が用意されています.gzFileFILE * の代替になり,gz*()fopen()fclose() などの代替になります.ただし,一部の関数については,インタフェースが異なっていたり,機能に制限があったりします.

ファイルを開く・閉じる

zlib の関数対応する関数変更点
gzopen()fopen() モードの仕様
gzdopen()fdopen() モードの仕様
gzclose()fclose()
gzFile gzopen(const char *path, const char *mode);
gzFile gzdopen(int fd, const char *mode);
int gzclose(gzFile file);
gzFile file = gzopen("output.gz", "wb9");
assert(file != NULL);

gzopen() によりファイルを開くことができます.fopen() と同様に,第 1 引数にファイル名,第 2 引数にモードを指定するようになっています.ただし,読み書き両用を示す + を指定することはできません.また,書き込み用にファイルを開く場合,圧縮レベルを 0 以上 9 以下の数字によって指定できます.返り値の型は gzFile であり,ファイルを開くことができれば,NULL 以外の値になります.このハンドルは以降の操作に必要であり,操作の後で gzclose() を使って閉じる必要があります.

gzFile file = gzdopen(STDIN_FILENO, "rb");
assert(file != NULL);

既に開いているファイル記述子があれば,gzdopen() により gzFile と関連付けることができます.fdopen() と同様に,第 1 引数にファイル記述子,第 2 引数にモードを指定するようになっています.また,gzopen() と同様に,+ の指定ができない代わりに,圧縮レベル(09)の指定ができるようになっています.gzdopen() により得られた gzFilegzclose() を使って閉じる必要があります.gzclose() は関連付けられたファイル記述子も閉じるので,ファイル記述子を閉じずに残しておきたいときは,dup() などを使って複製しておくようにしてください.

gzopen()gzdopen() により得られた gzFile は,gzclose() を使って閉じる必要があります.書き込み用のファイルについては,内部状態として保持されているデータをファイルに書き込んでから閉じるようになっています.gzclose() が失敗したときは,返り値が Z_OK 以外になります.Z_ERRNO のときは,エラーの原因を示す値が errno に格納されています.

int gzbuffer(gzFile file, unsigned size);

ファイルを開いた後,読み込みや書き込みをおこなう前であれば,gzbuffer() を使って内部バッファのサイズを設定することができます.デフォルトのサイズは 8,192 であり,65,536131,072 を指定することによって,読み込み速度が向上します.また,内部バッファのサイズは,gzprintf() を使って一度に書き込みできるバイト数にも影響します.

ファイルから読み込む

zlib の関数対応する関数変更点
gzread()fread() 引数の順序,整数の型,サイズの統合,エラーの判別
gzgets()fgets() 引数の順序
gzgetc()fgetc()
gzungetc()ungetc()
int gzread(gzFile file, voidp buf, unsigned len);
char *gzgets(gzFile file, char *buf, int len);
int gzgetc(gzFile file);
int gzungetc(int c, gzFile file);

zlib が提供する読み込み用の関数は gzread(), gzgets(), gzgetc(), gzungetc() の 4 種類であり,fread(), fgets(), fgetc(), ungetc() と対応しています.ただし,gzread()gzgets() については,引数の順序や整数の型などが異なっています.64-bit 環境では int, unsignedsize_t のサイズが異なるため,特に注意が必要です.また,fread() はファイルの終端とエラーを区別せずに 0 を返しますが,gzread() はエラーを区別して -1 を返すようになっています.

ファイルに書き込む

zlib の関数対応する関数変更点
gzwrite()fwrite() 引数の順序,整数の型,サイズの統合,エラーの判別
gzprintf()fprintf() 長さの制限
gzputs()fputs() 引数の順序
gzputc()fputc() 引数の順序
int gzwrite(gzFile file, voidpc buf, unsigned len);
int gzprintf(gzFile file, const char *format, ...);
int gzputs(gzFile file, const char *s);
int gzputc(gzFile file, int c);

zlib が提供する書き込み用の関数は gzwrite(), gzprintf(), gzputs(), gzputc() の 4 種類であり,fwrite(), fprintf(), fputs(), fputc() と対応しています.ただし,引数の順序や整数の型などが異なっています.64-bit 環境では int, unsignedsize_t のサイズが異なるため,特に注意が必要です.なお,gzwrite() は書き込むバイト数が 0 の場合とエラーが起きた場合を区別せずに 0 を返すようになっています.gzprintf() については,gzbuffer() により指定されたバイト数以上の長さを一度に書き込むことができません.デフォルトでは 8,191 バイト以下に制限されることになります.

ファイルの状態を確認・変更する

zlib の関数対応する関数変更点
gzflush()fflush() 引数の追加
gzseek()fseek() 整数の型,機能の制限
gztell()ftell()
gzrewind()rewind()
gzeof()feof()
gzerror()ferror() 引数の追加
gzclearerr()clearerr()
int gzflush(gzFile file, int flush);
int gzrewind(gzFile file);
z_off_t gztell(gzFile file);
z_off_t gzseek(gzFile file, z_off_t offset, int whence);
int gzeof(gzFile file);
const char *gzerror(gzFile file, int *errnum);
void gzclearerr(gzFile file);

単純なファイルの読み書き以外にも,内部状態として保持されているデータをファイルに書き込んだり,読み書きの位置を変更したり,状態を確認・変更したりする関数が用意されています.ほとんどの関数には対応する標準関数があるものの,一部の機能に変更や制限があるので,利用に際しては注意が必要です.

gzflush() による内部状態のフラッシュについては,fflush() と異なり,第 2 引数として Z_SYNC_FLUSHZ_FINISH を指定するようになっています.

gzip 形式はランダムアクセスを想定していないので,読み書きの位置を気軽に変更することはできません.gzseek() による読み込み位置の変更は擬似的に実現されているだけなので,効率が悪く,広範囲の移動は避けておく方が無難です.また,書き込み位置の変更については,前方への移動が不可能であり,後方への移動は移動範囲を 0 で埋めるという実装になっています.

gzerror() は,エラー番号を第 2 引数に代入し,エラーメッセージを返すようになっています.ただし,エラーがファイルの操作に起因する場合,エラー番号は Z_ERRNO になり,エラーの詳細は errno で確認する必要があります.

サンプルコード

$ gcc -Wall -O2 -std=c99 zlib-test.c -lz -o zlib-test

zlib 形式および gzip 形式の圧縮・伸長をおこなうプログラムのサンプルコードとして zlib-test.c を用意しました.C99 の機能を使っているので,gcc には -std=c99 を渡すようにしてください.

$ ./zlib-test --help
Usage: ./zlib-test [OPTION]... [FILE]...
Version: zlib-1.2.6
Options:
  -d, --deflate  圧縮します (default)
  -i, --inflate  伸長します
  -g, --gzip     gzip 形式で圧縮します
  -z, --zlib     zlib 形式で圧縮します (default)
  -l, --level=[0-9]    圧縮レベルを指定します (default: 6)
  -o, --output=[FILE]  出力ファイルを指定します (default: stdout)
  -h, --help     このヘルプを表示します

コマンドライン引数による圧縮形式や圧縮レベルの切り替えが可能になっています.-h もしくは --help をオプションとして渡すことにより,zlib のバージョンとコマンドライン引数の一覧を確認することができます.

おわりに

本記事では zlib の基本的な使い方を説明しました.大抵のアプリケーションについては対処できるようになっています.より高度な使い方については,公式サイトのドキュメントを参照してください.