IntPtrが示すバイト列のポインタをGZipStream/DeflateStreamで圧縮・展開する

.NET Frameworkでは、System.IO.Compression.GZipStreamSystem.IO.Compression.DeflateStreamクラスを用いて、データを圧縮・展開することができます。この2つのクラスは全く同じように使えるため、以下ではGZipStreamで話を進めます。


以下のコードは、指定したbyte配列をGZipStreamで圧縮してMemoryStreamに出力するサンプルです。MemoryStreamのところをFileStreamにすればメモリではなくファイルに書き出すこともできます。

using System.IO;
using System.IO.Compression;
using System.Text;

class Program
{
    static void Compress(MemoryStream ms, byte[] data)
    {
        using (GZipStream gzs = new GZipStream(ms, CompressionMode.Compress))
        {            
            gzs.Write(data, 0, data.Length);
        }
    }
    static void Main(string[] args)
    {
        MemoryStream ms = new MemoryStream();
        byte[] data = Encoding.UTF8.GetBytes("hogehogefugafugapiyopiyo");
        Compress(ms, data);
    }
}

このように、ReadWriteメソッドを使ってデータを読み書きするのですが、これらのメソッドは引数にbyte[]をとります。ですから、アンマネージなバイト列の先頭ポインタ(IntPtr)を持っていた場合はそのままでは圧縮できません。これを何とかしてがんばる方法をいくつか考えてみました。

なお、展開の方も同様の考え方で行えるため、ここでは割愛します。

1. IntPtrからbyte[]に変換する

すぐ思いつくのはこの方法です。バイト列と同じ大きさのマネージbyte配列を別に用意して、Marshal.Copyを使ってコピーし、GZipStreamに丸投げするだけです。

static void Compress(MemoryStream ms, IntPtr data, int size)
{
    byte[] buffer = new byte[size];
    Marshal.Copy(data, buffer, 0, size);
    using (GZipStream gzs = new GZipStream(ms, CompressionMode.Compress))
    {            
        gzs.Write(buffer, 0, size);
    }
}

しかしバイト長sizeが大きい場合は、bufferの確保とコピーのコストが高くつきます。

2. 1バイトずつコピーする

最もメモリは節約できます。

static void Compress(MemoryStream ms, IntPtr data, int size)
{
    using (GZipStream gzs = new GZipStream(ms, CompressionMode.Compress))
    {            
        for(int offset = 0; offset < size; offset++)
        {
            gzs.WriteByte(Marshal.ReadByte(ptr, offset));
        }
    }
}

しかし処理速度は期待できません。

3. 少しずつコピーする

1と2の折衷案。さすがに1バイトごとはやりすぎだけど、一気にコピーするほどメモリは無い。そこで例えば128KBずつコピーする、ということにします。

static void Compress(MemoryStream ms, IntPtr data, int size)
{
    // バッファ長
    const int Length = 1024 * 128;
    byte[] buffer = new byte[Length];
    using (GZipStream gzs = new GZipStream(ms, CompressionMode.Compress))
    {            
        for (int offset = 0; offset < size; offset += Length)
        {
            // バイト列から読み込むバイト長。
            // 残りの長さがLengthを切ったらその残りの長さにする。
            int length = (offset + Length < size) ? Length : size - offset;
            // 読み込むバイト列の先頭ポインタをずらす
            IntPtr ptr = new IntPtr(data.ToInt64() + offset);
            Marshal.Copy(ptr, buffer, 0, length);
            gzs.Write(buffer, 0, length);
        }
    }
}