なんとなく

誰得感満載な記事が多いかも。Mono関係とLinuxのサーバ関係、レビューとか。

ライブラリ(libopenmetaverse)で使われているライブラリ(zlib.net)でハマった話

はじめに

5年近く弄っているけど、全然完成してなくて、やりたくなった時にだけ開発をすすめるっていう感じでAndroid用のSecondLife向けのViewerを作ってます。

f:id:takeshich:20160314015328p:plain

現在、Android用に向けて、まず、PC用のRadegastでRigidMesh対応していて(Android用の3D部はRadegastの3D Sceneを使って実装)、まだ完了していないのですが、データの取得部分でかなりハマってしまった話です。

データの取得には、libopenmetaverse(https://github.com/openmetaversefoundation/libopenmetaverse)というライブラリを使っていて、RigidMesh対応の実装はされていないので、追加で実装しました。 大方のデータは取得できるのですが、一部エラーが出て取得できない物があったので調べてみて、libopenmetaverseで使用しているzlib.net(ZLIB.NET - ZLIB for .NET, ZLIB for dotnet, .net deflate, .net inflate, C# deflate)の問題でした。

結論

ライブラリで使用しているライブラリがDLLの状態で、そのDLLでのエラーは検討をつけづらいのは確かです。 しかし、環境を用意して、エラーメッセージをきちんと確認すれば、ハマることもなくそれほど難易度は高くないはずです。

ハマった原因(言い訳)

  • 出現頻度が低いものでした。
  • きちんとエラーメッセージに基づいた対応方法を確認していませんでした。
  • 仕様書に惑わされてしまいました。
  • zlib.netの仕様の確認不足でした

上記の理由で、時間を要してしまいました。(言い訳)

解決しなけれならないのであれば、最小限の環境を作ってテストするのが一番早いと思いました。 わかっているつもりでもできないということはわかっていないんですよね。

エラー内容

最初に、出力されたエラーメッセージは

Failed to decode mesh asset

AssetMesh.csでエラーがcatchされた際に出力されていました。そのため、デバッグして、エラーがcatchされていたエラーメッセージを確認すると

inflating:

でした。はて?なんでしょう。。。

対応

まずは、RiggedMeshの仕様を確認しました。

The skin block is a gzip'd binary encoded LLSD map with the following entries: Mesh/Mesh Asset Format - Second Life Wiki

とあり、 inflating(展開)していて、LLSD(libopenmetaverseではOSD)に関わることですから、圧縮されているのを展開しているところに問題があるんだろうなと当たりをつけました。 圧縮展開関係では、libopenmetaverseは、zlib.net.dllを参照しているので、内部で何をやっているのかlibopenmetaveseを見るだけではわかりません。

ここまでは、PC用のRadegastで、使用されているライブラリであるlibopenmetaverseをデバッグしていたのですが、そのlibopenmetaverseでは、zlib.netはdllで参照する形になっていました。 幸い、libopenmetaverseをforkし、自身でAndroid用で動くようにしていたプロジェクトにはソースコードレベルでzlib.netを参照しており、zlib.netをデバッグできる環境がありました。

しかし、Android用はまだまだ動作が不安定で、安定的に当該エラーを発生させられる状況ではありませんでした。 時間をかけて、なんとか発生させてエラーメッセージを見ることはできました。今思うと時間をかけずにちゃちゃっと環境を作ってしまうべきでした。PC用のRadegastにおいてzlib.netのソース持ってきて、libopenmetaverseからプロジェクトとして参照させるなど。

エラーメッセージは、nullで、エラーコードが-5でした。

調べてみると-5は、Z_BUF_ERRORでした。

今思うと本来ならここでZ_BUF_ERRORとは何かということを調べて、後述するように、その結果、bufferが足りてないから、増やせばいいと気づけばよかったのですが、気づけませんでした。

エラーメッセージはわかり、ああ、なにかエラーなんだなというのがわかったので、 RiggedMeshの仕様にあった、gzip'dをみて、gzipで圧縮されたのを展開するのだなと思ってしまいました。 そこでzlibでどういうことをやっているのか仕様を確認することにしました

データがgzipなのかを確認せず、そうと思い込んでいました。

libopenmetaverseで使われているものは、zlib.dllではなく、C#に移植されたzlib.netでしたが、それをzlibと同様と捉えてzlibの仕様を確認していました。

参考にしたのは、以下のサイトです。

wlog.flatlib.jp

qiita.com

zlibとgzipそしてdeflate

zlibとgzipそしてdeflateとは何かについて理解しなければなりません。上述の記事より以下に引用させていただきます。

名称 説明
deflate 圧縮アルゴリズムペイロードのみを生成する。
gzip deflateを利用してデータを圧縮するコマンドラインツールの名称、またはそれが採用している圧縮データフォーマットの名称。メタデータを付加する。
zlib deflateを利用してデータを圧縮するC言語で作成されたライブラリの名称、またはそれが採用している圧縮データフォーマットの名称。メタデータを付加する。
  • gzipとzlibでメタデータの内容は異なる。
  • zlibはgzipの圧縮データフォーマットもサポートしている。
  • PHPはエクステンションとしてzlibを利用している。
  • HTTP通信で最も多く利用されているものはgzip圧縮データフォーマットである。

gzip圧縮されたデータの展開方法いろいろ - Qiita

zlibとgzipは似てるけどヘッダやフッタが異なります。zlibのライブラリでは、gzipも扱えるものです。

そこで、まず、inflateInit()のwindowbitか?とおもい、変更してみたのですが、エラーは変わらずでした。 なんだろうと思い、zlib.netのソースコードを追いかけてみるとgzip自体の処理がありませんでした。なんとzlib.netはgzipに対応しておりませんでした。

次に.NET Framework クラス ライブラリ には、System.IO.Compressionがあり、gzip(RFC 1952)に対応しています。 そこで、libopenmetaverseにエラーが出たら、gzipstreamで再処理するロジックを入れ確認しました。 しかし、gzipのヘッダではないというエラーが出てました。

ん?と思い、byte配列を確認するとzlibのヘッダである0x78 0xdaとあるではないですか! ずっとgzipで送られてきているものと信じてしまっていたので、力が抜けました。

Z_BUF_ERRORとは何か?

(追記:2016/07/13)

以下にある対応方法は誤りのようだ。 この記事を書いた後気になって調べてみたら、確かかはきちんと検証できていないのだが、 たまたま入出力のバッファがぴったり空/一杯になった (ZoutputStream.avail_in == ZoutputStream.avail_out == 0) で返ってきた時に、再度 inflate() が呼ばれてしまう処理になっている。zlib.netは入力が既にない状態で inflate() を呼ぶと Z_BUF_ERROR を返す。 試しに、zlib.netにおいて、当該処理の中にif (ZoutputStream.avail_in == 0 && ZoutputStream.avail_out == 0) break;を入れてみたら、Z_BUF_ERROR を返さなくなった。

以下にある対応方法は、確率的にZ_BUF_ERRORが出現するのを少なくしているだけで、根本的な解決ではない。

(ここまで、追記:2016/07/13)

そこで、最初のエラーメッセージに立ち返って、Z_BUF_ERRORとは何かということを調べました。

圧縮についての記述ですが、

Z_BUF_ERROR 出力バッファのサイズ不足 zlib 入門

とあります、出力バッファのサイズ不足なんですよ!!!

さらに、

CHUNK is simply the buffer size for feeding data to and pulling data from the zlib routines. Larger buffer sizes would be more efficient, especially for inflate(). If the memory is available, buffers sizes on the order of 128K or 256K bytes should be used. zlib Usage Example

とあって、

#define CHUNK 16384

Just like in def(), the same output space is provided for each call of inflate().

       strm.avail_out = CHUNK;
       strm.next_out = out;

という記述もありました。

それで、zlib.netでその処理をしているところを見つけると

protected internal int bufsize = 4096;

となっておりました。

If the memory is available, buffers sizes on the order of 128K or 256K bytes should be used.

とありましたので、128Kに

protected internal int bufsize = 128 * 1024;

と変更し、ビルドし、libopenmetaverseで使用しているzlib.net.dllを入れ替えて、libopenmetaverseをリビルトし、確認してみるとエラーは出なくなりまりました。

なお、 .NET Framework クラス ライブラリ には、System.IO.Compression があって、deflate(RFC 1951)とgzip(RFC 1952)には対応しているようですが、zlib(RFC 1950)には対応していないようです。 しかし、ヘッダやフッタのメタデータを除けばペイロード(データ自体)はdeflateですから、メタデータ部を考慮せずDeflateStream を使えばいいようです。zlib.netにおいてもソースを見るとwindowbitを値にするとdeflateのみになる実装のようです。

System.IO.Compression 名前空間

Zlib.dll? Zlib.NET? DeflateStream? - うぃずのひとりごと

まとめ

  • zlib.netではdeflate(RFC 1951)とzlib(RFC 1951)のみでgzip(RFC 1952)は使えません。
  • 上述したように、gzip(RFC 1952)は.NET Framework クラス ライブラリにあります。

ハマって勉強になりました。zlib.netでも面倒だからzlibとgzipの自動判定あってもいいのになと思います。 zlib.netをforkして実装したライブラリあっても良さそうですけど、見つかリませんでした。

こういう対応は、きちんと基本を確認していかなければいけないのに、それが現状できていないので 常にきちんと基本を確認するよう心に留めて、できるようにならないとと思いました。