読者です 読者をやめる 読者になる 読者になる

.NET の Stream を CoreGraphics で利用する

※2015/1/23 追記
下記の文書によれば、32-bit環境においても、 sizeof(off_t)=8 です。僕は何となく新しい環境でしかテストしていなかったため、問題が起きませんでしたが、当初、sizeof(off_t)=4と誤解してコードを記述していました。
そのため、 position などが nint になっていましたが、
正しくは、 off_t は long に置き換えないといけません。

Xamarin.iOS でコードを書く場合でも、せっかく、 PCL でコードが共有できるわけですから、できるなら .NET の Stream クラスを使った様々な抽象化のお世話になりたいところです。

画像の入力を行う場合などに、ファイル名や URL を直接渡せる場合には、それらの API を利用すれば良いですが、そうではない場合、 CGDataProvider のお世話になります。

この CGDataProvider は、ストレージへのアクセスを抽象化しており、ストリーム型のデータだけでなく、ランダムアクセス可能ではあるもののメモリ上でもファイル上でもないといったデータストレージに対しても CGDataProviderDirectCallbacks が定義するコールバック関数群を利用してアクセス出来ます。

この構造体の定義は次みたいな感じです:

struct CGDataProviderDirectCallbacks
{
  unsigned int version;
  CGDataProviderGetBytePointerCallback getBytePointer;
  CGDataProviderReleaseBytePointerCallback releaseBytePointer;
  CGDataProviderGetBytesAtPositionCallback getBytesAtPosition;
  CGDataProviderReleaseInfoCallback releaseInfo;
};

typedef struct CGDataProviderDirectCallbacks CGDataProviderDirectCallbacks;

version は、現状、 0 で良くて、
次に、 getBytePointer, releaseBytePointer はサポートしなくて良いなら NULL でも良く、実際に実装すべきは、 getBytesAtPosition だけです。

さて、この関数は、

size_t getBytesAtPosition(void *info, void *buffer, off_t position, size_t count )
{
  // info で示されるストレージの position の場所から、
  // count バイトのデータを取得して buffer にぶち込む
  // 戻り値は読み込んだバイト数
}

みたいな簡単な関数です。ここから、 Stream.Position, Stream.Read などを呼び出せばそのまま buffer にデータをぶち込めますね・・・・・・・いや無理。

残念なことに、 Stream.Read はバッファーとしては、 .NET の配列しか受け付けないので、一時的にバイト配列を確保して、そっちから、 Marshal.Copy などを使って buffer にコピーするしかないですね・・・。本当に残念ですが。

まぁ、あとは一時バッファのサイズをいくつにするとか、これループ回すの?みたいな話はありますが、全体的には些末な話です。

あと、残りの releaseInfo ですが、 CGDataProvider が削除されるタイミングで、 Stream.Dispose を呼び出したいといった理由があれば、実装すべきでしょう。

void releaseInfo(void* info)
{
  // データの解放など
}

P/Invoke

さて、ここまで分かれば、これら構造体、コールバックの定義を C# 側でやってあげるのは簡単です。

[DllImport(Constants.CoreGraphicsLibrary)]
static extern IntPtr CGDataProviderCreateDirect(IntPtr info, long size, CGDataProviderDirectCallbacks callbacks);

[StructLayout(LayoutKind.Sequential)]
struct CGDataProviderDirectCallbacks
{
    public int version;
    public IntPtr getBytePointer;
    public IntPtr releaseBytePointer;
    public CGDataProviderGetBytesAtPositionCallback getBytesAtPosition;
    public CGDataProviderReleaseInfoCallback releaseInfo;
};

// Unified API じゃない場合には、 nint の代わりに、 IntPtr を使ってください
delegate nint CGDataProviderGetBytesAtPositionCallback(IntPtr info, IntPtr buffer, long position, nint count);
delegate void CGDataProviderReleaseInfoCallback(IntPtr info);

ということで、この程度だったら、インラインで new するぜー、みたいなノリで、

Stream stream = ...;

var callbacks = new CGDataProviderDirectCallbacks {
  getBytesAtPosition = (info, buffer, position, count)
  {
    // バッファのサイズとか手抜きバージョン
    var buf = new byte[count];
    stream.Position = position;
    var ret = stream.Read(buf, 0, count);
    Marshal.Copy(buf, 0, buffer, ret);
    return ret;
  }
};
var dataProv = CGDataProviderCreateDirect(IntPtr.Zero, size, callbacks);

みたいなことをしたくなりますが、これ、動きません。
理由は、 Xamarin.iOS では、JIT が使えない制約の一部として、 P/Invoke する関数に渡すことが出来る delegate は、 static じゃないといけない(オブジェクトのメソッドを呼び出せない or クロージャ(ラムダ関数)を呼び出せない)という制約があります。

なので、関数は分けて書かないと行けません。
つまりですね、 stream とか、そういう必要なデータは、 CGDataProviderCreateDirect の第一引数 info として渡さないといけない訳ですね。

そうすると、 GCHandle のお世話にならないと行けませんが、 GCHandle は解放し忘れるとオブジェクトを道連れにしてリークするので、 releaseInfo も必然的に実装することになります。

あとは、 Xamarin.iOS には、CGDataProviderが定義されており、さらに、 CGDataProviderRef を IntPtr の形で取れるコンストラクタもあるので、それを使えば、 CGDataProvider が完成します。

class CGStreamDataProvider
{
  private CGDataProviderFromStream() { }

  Stream _stream;
  bool _disposeOnClose;

  public static CGDataprovider Create(Stream stream, bool disposeOnClose)
  {
    var obj = new CGStreamDataProvider() {
      _stream = stream,
      _disposeOnClose = disposeOnClose };
    var callbacks = new CGDataProviderDirectCallbacks
            {
                getBytesAtPosition = getBytesAtPosition,
                releaseInfo = releaseInfo
            };
    var gcHandle = GCHandle.Alloc(obj, GCHandleType.Normal);
    var dataProv = CGDataProviderCreateDirect(GCHandle.ToIntPtr(gcHandle), (nint)stream.Length, callbacks);
    // CGDataProvider は Xamarin.iOS で定義されている
    return new CGDataProvider(dataProv);
  }

  [MonoPInvokeCallback(typeof(CGDataProviderGetBytesAtPositionCallback))]
  static nint getBytesAtPosition(IntPtr info, IntPtr buffer, long position, nint count)
  {
    // GCHandle からオブジェクトのリファレンスを復帰
    var pthis = (CGStreamDataProvider)GCHandle.FromIntPtr(info).Target;
    // バッファのサイズとか手抜きバージョン
    var buf = new byte[count];
    pthis ._stream.Position = position;
    var ret = pthis ._stream.Read(buf, 0, count);
    Marshal.Copy(buf, 0, buffer, ret);
    return ret;
  }

  [MonoPInvokeCallback(typeof(CGDataProviderReleaseInfoCallback))]
        static void releaseInfo(IntPtr info)
  {
    var gcHandle = GCHandle.FromIntPtr(info);
    var pthis = (CGStreamDataProvider)gcHandle.Target;

    // ストリームを破棄すべきだったら破棄する
    if (pthis._disposeOnClose)
      pthis._stream.Dispose();

    // GCHandle はちゃんと解放する
    gcHandle.Free();
  }
}

// 使い方例
var dataProv = CGStreamDataProvider.Create(stream, true);

上記のコードはエラー処理などが大幅に省略されているので、実コードは下記を参考にしてください。