まえがき
こんにちは。これは Hamee Advent Calendar 2018 6日目の記事です。
私は昔からデータ圧縮が好きで、学生時代は意味もなくファイルを圧縮しまくって悦に入ったりデータ圧縮を研究テーマにして先生を困らせたりしていました。今回はマイコン用ファームウェアで ZIP ファイル(のバイナリイメージ)からデータを展開(解凍)するという話を書きたいと思います。
ZIP を扱うなんて簡単。そう、組み込み以外ならね!
ZIP ファイルを扱うなんて今どき簡単です。UNIX 系なら zip / unzip コマンドとか使えば良いですし最近の言語やフレームワークでは何らかの形で ZIP の圧縮/解凍をサポートしていますし、いざとなったら zlib を直接使うこともできます。Windows だって .NET Framework 4.5 でサポートしています。一般的なアプリケーション開発ならそれらを使えば良いということになると思います。
でもメモリが少なかったりファイルシステムが整備されていなかったりの組み込み系ではそう簡単には行きません。しかし一方で、IoT 機器では低速な回線を使わなければならなかったり通信量の制限が厳しかったりで、送受信するデータをなるべくちっちゃくしなければならない事情があります。IoT 機器のファームウェア開発にはデータ圧縮への潜在的なニーズがあります。そう、何を言いたいかというと、
組み込み系だって ZIP を使いたいのだぜ!
そんな組み込み開発者の福音となるライブラリが miniz です。(←大げさ1)
miniz とは?
miniz は主に組み込み系マイコンで使うことを想定して作られた(と私が勝手に思っている) RFC 1950 (ZLIB Compressed Data Format) / RFC 1951 (DEFLATE Compressed Data Format) のコンパクトな実装で、ZIP ファイル形式にも対応しています。C 言語のソースファイルとして MIT ライセンスで配布されています。配布サイト(github)はこちら。
というわけであなたのプロジェクトにこのライブラリを突っ込んで、リファレンスを読みながら使ってみましょう!
…とはいかないのが組み込みの世界の厳しいところ。プロジェクト管理ツール(maven みたいな)なんかないので自分で適切に配置しないといけません。それに分かりやすいリファレンスなんかもありません。分からないならソースを見ろ!の世界です。以下ではプロジェクトへの導入方法や簡単な使い方を説明します。
なお、以降に記載する内容の動作確認には、最近話題に上ることが多い ESP32 (ESP32-DevKitC)と ESP-IDF を利用しています。試す方は最低 20 キロバイト程度のスタックメモリを確保することをお勧めします。
導入方法
まず、配布元 github の releases ページから最新版のパッケージ(miniz-?.?.?.zip)をダウンロードして解凍します(なお執筆時の最新版は 2.0.8 です)。色々ファイルが展開されますが、使うのは miniz.c と miniz.h だけですので、この2つのファイルがビルド対象になるよう配置しましょう。
なお、普段から git を使いまくっている皆さんの中には、なぜ git clone しないんだ?と疑問に思った方もいるかと思いますが、それはダメぽ。いや、別に clone しても良いのですが、その場合は別途 amalgamate という作業をしなければいけなくなります。これについては本題から外れますので省略しますが興味のある方は調べてみてください。
事前準備
ダウンロード&解凍して手に入れた miniz.c と miniz.h は、開発対象のアーキテクチャによってはそのままではビルドできないかもしれません(ビルドできたけど正常に動作しないなんてこともあるかも)。そのためアーキテクチャに合わせた設定が必要になります。
設定は miniz.h 内(上の方)にある #define のコメントアウトを外すことで行います。設定項目は色々ありますが、ここでは以下の点を設定してみます。
時刻情報を使わない
開発対象が時刻を扱わない場合はMINIZ_NO_TIME
のコメントアウトを外します。
#define MINIZ_NO_TIME
圧縮しない
展開(解凍)だけしたい場合は圧縮関連のコードは不要ですのでMINIZ_NO_ARCHIVE_WRITING_APIS
のコメントアウトを外します。
#define MINIZ_NO_ARCHIVE_WRITING_APIS
簡単な使い方
たとえばメモリ上に ZIP ファイルイメージが置かれている場合、それに格納されている特定のファイルをヒープメモリに展開するには以下の通り。
#include "miniz.h"
void *extract_zip_to_heap(
const char *zip_image,
size_t zip_image_size,
const char *file_name,
size_t *extracted_size
){
mz_zip_archive zip_archive;
void *p;
memset(&zip_archive, 0, sizeof(zip_archive));
mz_zip_reader_init_mem(&zip_archive, zip_image, zip_image_size, 0);
p = mz_zip_reader_extract_file_to_heap(&zip_archive, file_name, extracted_size, 0);
mz_zip_reader_end(&zip_archive);
return p;
}
この関数はzip_image
とzip_image_size
で示される ZIP ファイルイメージからファイル名file_name
のファイルをヒープメモリに展開してそのアドレスを返却します。また展開したデータのバイトサイズをextracted_size
が指すsize_t
変数にセットします。なお展開したデータを使い終わったらfree
関数で開放することを忘れないようにしましょう。
それから、上記のコードは処理の流れを見てもらうためエラーチェックを省略しています。本番で使うときはしっかりエラーチェックを入れましょう。以降のコードも同様です。
確保済みメモリ領域への展開
上の例ではヒープ領域にメモリを確保して、解凍したデータをそこに展開しました。しかし組み込み系では、なるべくヒープを使いたくない、あるいはすでに確保してあるバッファを使い回したいという場合がよくあります。そのような場合は以下のようにします。
#include "miniz.h"
void extract_zip_to_memory(
const char *zip_image,
size_t zip_image_size,
const char *file_name,
size_t *extracted_size,
void *extract_buffer,
size_t extract_buffer_size
){
mz_zip_archive zip_archive;
mz_zip_archive_file_stat file_stat;
mz_uint32 file_index;
memset(&zip_archive, 0, sizeof(zip_archive));
mz_zip_reader_init_mem(&zip_archive, zip_image, zip_image_size, 0);
mz_zip_reader_locate_file_v2(&zip_archive, file_name, NULL, 0, &file_index);
mz_zip_reader_file_stat(&zip_archive, file_index, &file_stat); /* file_stat.m_uncomp_sizeが展開後のサイズ */
*extracted_size = file_stat.m_uncomp_size;
mz_zip_reader_extract_to_mem(&zip_archive, file_index, extract_buffer, extract_buffer_size, 0);
mz_zip_reader_end(&zip_archive);
}
なんだかコード量が増えましたが、これは解凍したデータを特定アドレスに展開するmz_zip_reader_extract_to_mem
関数が、なぜか解凍後のサイズを返すようになっていないからです(mz_zip_reader_extract_file_to_heap
関数は返してくれるのに…)。そのため、mz_zip_reader_locate_file_v2
関数で ZIP ファイル内の解凍するファイルのインデックスを取得して、それを使ってmz_zip_reader_file_stat
関数でファイル情報を取得し、そこからファイルサイズを取得しています。もっとスマートな方法があっても良いと思うのですが、残念ながら見つかりませんでした。まぁ一応動きますし良しとしましょう。
メモリ外に置かれた ZIP イメージを読み、メモリ外に展開する方法
ここからが本題です。
上記2つの例ではメモリ上に置かれている ZIP イメージを読んで、それに格納されているファイルをメモリ上に展開しました。しかし組み込み系ではメモリ量が厳しく制限されることが多くて、これらの方法を利用できないことがあります。そのような場合は、たとえば何らかのインタフェースで接続された外部記憶( Flash メモリ等)を利用することが考えられます。ここでは Flash メモリに記録されている ZIP ファイルイメージを読み込んで一つのファイルを解凍し、解凍したファイルのイメージを Flash メモリに展開する方法を示します。
【重要!】
以降の内容は本家のドキュメントやサンプルには記載のないものです。筆者が miniz のソースコードを解析して「たぶんこうだろう」と考えた内容に過ぎません。もちろん一通り動くことは確認していますが過信は禁物です。もし間違いがありましたらご一報いただけると嬉しいです。m(_ _)m
ZIP イメージを読み出すコールバック関数の作成
まず、ZIP イメージを読み出すためのコールバック関数を用意します。この関数は以下のような形になります。
size_t zip_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n){
/* TODO: ZIPイメージのオフセットfile_ofsからnバイトをpBufに読み込む */
return (読み込んだバイト数);
}
この関数の戻り値は、原則的に引数n
と同じになります。
解凍データを書き出すコールバック関数の作成
次に、解凍したデータを書き出すためのコールバック関数を用意します。こちらは以下のような形になります。
size_t zip_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n){
/* TODO: pBufからnバイトのデータをオフセットfile_ofsに書き出す */
return (書き出したバイト数);
}
この関数の戻り値も原則的に引数n
と同じになります。
この関数は書き出し先のオフセットアドレスがfile_ofs
引数で指定されます。まるでランダムアクセスを想定したようなプロトタイプ宣言にも見えますね。このことは Flash メモリに書き出すことを考えると都合が悪いように思えます。ですが実際の展開(解凍)処理では、このオフセットアドレスはシーケンシャルに指定されます。つまり最初にこの関数が呼ばれるときは 0 が指定され、呼ばれるたびに単純にn
だけ増えていきます。そのため Flash メモリへの書き出しにもそれほど問題にならないと思います。
解凍の実行
以上で準備が整いましたので、mz_zip_reader_init
関数で ZIP イメージ読み込みの初期化をし、mz_zip_reader_extract_to_callback
関数で解凍を行います。
void extract_zip(
size_t zip_image_size,
const char *file_name,
size_t *extracted_size
){
mz_zip_archive zip_archive;
mz_zip_archive_file_stat file_stat;
mz_uint32 file_index;
memset(&zip_archive, 0, sizeof(zip_archive));
zip_archive.m_pRead = zip_read_func;
mz_zip_reader_init(&zip_archive, zip_image_size, 0);
mz_zip_reader_locate_file_v2(&zip_archive, file_name, NULL, 0, &file_index);
mz_zip_reader_file_stat(&zip_archive, file_index, &file_stat); /* file_stat.m_uncomp_sizeが展開後のサイズ */
*extracted_size = file_stat.m_uncomp_size;
mz_zip_reader_extract_to_callback(&zip_archive, file_index, zip_write_func, NULL, 0);
mz_zip_reader_end(&zip_archive);
}
mz_zip_reader_init
関数は ZIP イメージ読み込み処理のコンテキストを保持する mz_zip_archive
構造体を初期化します。この構造体には ZIP イメージを逐次読み込むためのコールバック関数 m_pRead
を指定できるので、先に用意した読み込み用のコールバック関数を設定しています。これで ZIP イメージを読み込めるようになります。
mz_zip_reader_extract_to_callback
関数は ZIP イメージから特定のファイルを展開(解凍)しますが、ファイル全体を一気に展開するのではなく、ファイルの先頭部分から数十キロバイトずつ展開し、その都度コールバック関数に展開結果を引き渡します。そこで先ほど用意した書き出し用のコールバック関数を指定し、少しずつ書き出すようにしています。
このようにすることで、ファイルシステムを使えない場合やワーキングメモリが少ない環境でも大きな ZIP イメージからデータを展開できるようになります。
メモリ使用量の確認
組み込み系ではワーキングメモリ(スタックメモリやヒープメモリ)の量が強く制限されます。そのためライブラリを導入する際は、そのライブラリがどの程度メモリを食うのかが大きな関心事になります。メモリ使用量は使うアーキテクチャにも依存しますが、ここでは例として ESP32 (ESP-IDF) を使って ZIP 展開する場合のヒープ使用量を見てみようと思います。
テスト用 ZIP イメージの作成
まずはテストするための適当な ZIP イメージを作成します。ここでは青空文庫さんから夏目漱石著『趣味の遺伝』のテキスト版をダウンロードさせていただいて、それを UTF-8 に変換して ZIP 化した上で、xxd コマンドで C 言語のソース(char 配列)に変換しました。ちなみに圧縮前のサイズは約 135KB で、圧縮後は約 50KB です。
ヒープ残量を出力するためのコールバックの作成
ZIP 展開中のヒープ使用量を確認するために、各コールバック内でヒープ残量をコンソール出力するようにします。また、ZIP 展開開始前と終了後のヒープ残量も出力するようにします。
#define DISP_HEAP_MEMORY
size_t zip_read_func(void *pOpaque, mz_uint64 file_ofs, void *pBuf, size_t n){
#ifdef DISP_HEAP_MEMORY
printf("zip_read_func : Free Heap Size = %u\n", esp_get_free_heap_size());
printf("zip_read_func : Minimum Free Heap Size = %u\n", esp_get_minimum_free_heap_size());
#endif
memcpy(pBuf, test_zip + file_ofs, n);
return n;
}
size_t zip_write_func(void *pOpaque, mz_uint64 file_ofs, const void *pBuf, size_t n){
#ifdef DISP_HEAP_MEMORY
printf("zip_write_func : Free Heap Size = %u\n", esp_get_free_heap_size());
printf("zip_write_func : Minimum Free Heap Size = %u\n", esp_get_minimum_free_heap_size());
#else
int i;
for(i = 0; i < n; i++) putchar(((char *)pBuf)[i]);
#endif
return n;
}
void app_main(){
size_t extracted_size;
#ifdef DISP_HEAP_MEMORY
printf("Before : Free Heap Size = %u\n", esp_get_free_heap_size());
printf("Before : Minimum Free Heap Size = %u\n", esp_get_minimum_free_heap_size());
#endif
extract_zip(sizeof(test_zip), "test.txt", &extracted_size);
printf("\nExtracted size : %u\n", extracted_size);
#ifdef DISP_HEAP_MEMORY
printf("After : Free Heap Size = %u\n", esp_get_free_heap_size());
printf("After : Minimum Free Heap Size = %u\n", esp_get_minimum_free_heap_size());
#endif
while(1){
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
ちなみにヒープの空きサイズを取得する関数として ESP-IDF にはesp_get_free_heap_size
とesp_get_minimum_free_heap_size
がありますが、前者は空きブロックのトータルサイズを返すのに対して後者は連続している空きブロックのうち最小ブロックのサイズを返します。だと思います(あまり自信ない)。
結果
Before : Free Heap Size = 281828
Before : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 281568
zip_read_func : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 281568
zip_read_func : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 281568
zip_read_func : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 281492
zip_read_func : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 281492
zip_read_func : Minimum Free Heap Size = 280712
zip_read_func : Free Heap Size = 198060
zip_read_func : Minimum Free Heap Size = 198060
zip_write_func : Free Heap Size = 198060
zip_write_func : Minimum Free Heap Size = 198060
zip_write_func : Free Heap Size = 198060
zip_write_func : Minimum Free Heap Size = 198060
zip_write_func : Free Heap Size = 198060
zip_write_func : Minimum Free Heap Size = 198060
zip_write_func : Free Heap Size = 198060
zip_write_func : Minimum Free Heap Size = 198060
zip_write_func : Free Heap Size = 198060
zip_write_func : Minimum Free Heap Size = 198060
Extracted size : 137395
After : Free Heap Size = 281660
After : Minimum Free Heap Size = 198060
一時的におよそ 82 キロバイトほど使うようです(意外に多い…)。展開処理を行う際は、余裕を見て 96 キロバイトほどのヒープの空きを確保しておいた方が良さそうです。また、今回の例では読み出し用のコールバック関数が複数回呼ばれたあとに書き出し用のコールバック関数が複数回呼ばれて終わっていますが、より大きなファイルを展開する場合はさらに交互に呼ばれる可能性があります。
なお、微妙にメモリリークが起こっているように見えますが、ESP32(というより FreeRTOS ?) でヒープを扱う際は仕方のないことらしいです。普通にmalloc
してfree
しただけでこうなります。
サンプルコード
ここまでで示したコードを github に上げておきますのでよかったら参考にしてください。
まとめ
以上のように miniz を導入することで組み込み系ソフトウェアでも ZIP を扱えるようになります。もちろん 8 ビットマイコンやよりメモリの制約が厳しい環境では利用できないと思いますが、IoT の分野で使われるシステムならハードウェアによる制約はあまり受けないでしょうし、ぜひ活用していきたいです。
謝辞
素晴らしいライブラリを公開してくださった Rich Geldreich and Tenacious Software LLC さんと RAD Game Tools and Valve Software さんに心よりの感謝を!
-
実際のところデータ圧縮/展開関係のライブラリは他にも色々あるんですが、もっともメジャー(ですよね?ですよね?)な ZIP 形式を扱えると色んな意味で便利だと思います。 ↩