はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】IBufferWriter<T>の使い方・実装の仕方について学んでいく(公式のArrayBufferWriter<T>の内部実装を見ながら)

IBufferWriterの利用者視点での使い方

IBufferWriterは基本的に以下の3ステップで実行していきます。

  1. IBufferWriter<T>.GetSpanIBufferWriter<T>.GetMemoryを用いて書き込む先のメモリ領域を取得
  2. SpanMemoryを経由してメモリに書き込みを行う
  3. IBufferWriter<T>.Advanceでどれくらいメモリに書き込んだかを通知する
// 公式のArrayBufferWriter<T>はIBufferWriter<T>を実装している
// NOTE: ManagedHeap上の配列に対しての書き込みが行われる
IBufferWriter<byte> writer = new ArrayBufferWriter<byte>();
        
// 操作1. IBufferWriterからGetSpanかGetMemoryを用いて書き込む先のメモリ領域を取得
// NOTE: sizeHintはあくまでヒントであり、span.Lengthと一致するわけではないことに注意(sizeHint以上は保証)
Span<byte> span = writer.GetSpan(sizeof(int) * 3);

// 操作2. メモリに書き込みを行う
BitConverter.TryWriteBytes(span.Slice(0, sizeof(int)), 1);
BitConverter.TryWriteBytes(span.Slice(sizeof(int), sizeof(int)), 1);
BitConverter.TryWriteBytes(span.Slice(sizeof(int) * 2, sizeof(int)), 1);

// 操作3. どれくらいメモリに書き込んだかを通知する
writer.Advance(sizeof(int) * 3);
        
// 以下、実験用
// 256 : sizeHintはあくまで以上だということ
Console.WriteLine(span.Length);
        
// 1, 0, 0, 0
Console.WriteLine(string.Join(",", BitConverter.GetBytes(1).Select(x => x.ToString("x"))));
        
// 1,0,0,0,1,0,0,0,1,0,0,0
Console.WriteLine(string.Join(",", span.Slice(0, sizeof(int) * 3).ToArray().Select(x => x.ToString("x"))));

IBufferWriterの実装者視点での使い方

IBufferWriter<T>を実装するには以下のメソッドを用意してあげる必要があります。

namespace System.Buffers
{
    public interface IBufferWriter<T>
    {
        void Advance(int count);

        Memory<T> GetMemory(int sizeHint = 0);

        Span<T> GetSpan(int sizeHint = 0);
    }
}
メソッド名 意味
Advance(Int32) count データ項目が出力 Span または Memory に書き込まれたことを IBufferWriter に通知します。
GetMemory(Int32) 少なくとも要求されたサイズを持つ (sizeHint で指定します)、書き込み先の Memory を返します。
GetSpan(Int32) 少なくとも要求されたサイズを持つ (sizeHint で指定します)、書き込み先の Span を返します。

https://learn.microsoft.com/ja-jp/dotnet/api/system.buffers.ibufferwriter-1?view=net-7.0

それぞれのメソッドの使い方は前述の通りです。

これらを実現するためにどうすれば良いのか、公式の実装をまずは参考にしてみましょう。(コメントが邪魔なので一部消してあります)

namespace System.Buffers
{
#if MAKE_ABW_PUBLIC
    public
#else
    internal
#endif
    sealed class ArrayBufferWriter<T> : IBufferWriter<T>
    {
        private const int ArrayMaxLength = 0x7FFFFFC7;

        private const int DefaultInitialBufferSize = 256;

        private T[] _buffer;
        private int _index;

        public ArrayBufferWriter()
        {
            _buffer = Array.Empty<T>();
            _index = 0;
        }

        public ArrayBufferWriter(int initialCapacity)
        {
            if (initialCapacity <= 0)
                throw new ArgumentException(null, nameof(initialCapacity));

            _buffer = new T[initialCapacity];
            _index = 0;
        }

        public ReadOnlyMemory<T> WrittenMemory => _buffer.AsMemory(0, _index);
        public ReadOnlySpan<T> WrittenSpan => _buffer.AsSpan(0, _index);

        public int WrittenCount => _index;
        public int Capacity => _buffer.Length;
        public int FreeCapacity => _buffer.Length - _index;

        public void Clear()
        {
            Debug.Assert(_buffer.Length >= _index);
            _buffer.AsSpan(0, _index).Clear();
            _index = 0;
        }

        public void Advance(int count)
        {
            if (count < 0)
                throw new ArgumentException(null, nameof(count));

            if (_index > _buffer.Length - count)
                ThrowInvalidOperationException_AdvancedTooFar(_buffer.Length);

            _index += count;
        }

        public Memory<T> GetMemory(int sizeHint = 0)
        {
            CheckAndResizeBuffer(sizeHint);
            Debug.Assert(_buffer.Length > _index);
            return _buffer.AsMemory(_index);
        }

        public Span<T> GetSpan(int sizeHint = 0)
        {
            CheckAndResizeBuffer(sizeHint);
            Debug.Assert(_buffer.Length > _index);
            return _buffer.AsSpan(_index);
        }

        private void CheckAndResizeBuffer(int sizeHint)
        {
            if (sizeHint < 0)
                throw new ArgumentException(nameof(sizeHint));

            if (sizeHint == 0)
            {
                sizeHint = 1;
            }

            if (sizeHint > FreeCapacity)
            {
                int currentLength = _buffer.Length;

                // Attempt to grow by the larger of the sizeHint and double the current size.
                int growBy = Math.Max(sizeHint, currentLength);

                if (currentLength == 0)
                {
                    growBy = Math.Max(growBy, DefaultInitialBufferSize);
                }

                int newSize = currentLength + growBy;

                if ((uint)newSize > int.MaxValue)
                {
                    // Attempt to grow to ArrayMaxLength.
                    uint needed = (uint)(currentLength - FreeCapacity + sizeHint);
                    Debug.Assert(needed > currentLength);

                    if (needed > ArrayMaxLength)
                    {
                        ThrowOutOfMemoryException(needed);
                    }

                    newSize = ArrayMaxLength;
                }

                Array.Resize(ref _buffer, newSize);
            }

            Debug.Assert(FreeCapacity > 0 && FreeCapacity >= sizeHint);
        }

        private static void ThrowInvalidOperationException_AdvancedTooFar(int capacity)
        {
            throw new InvalidOperationException(SR.Format(SR.BufferWriterAdvancedTooFar, capacity));
        }

        private static void ThrowOutOfMemoryException(uint capacity)
        {
            throw new OutOfMemoryException(SR.Format(SR.BufferMaximumSizeExceeded, capacity));
        }
    }
}

github.com

解説するとざっと以下の通り。

  • _bufferArrayBufferWriterが保持するデータ(byte[])
  • _index_bufferのどこまで書き込まれたかを意味する
  • Advance_index(どこまで書き込んだか)を進める
  • GetMemoryGetSpanで要求された以上のサイズを持つメモリ領域を返す(このときサイズが足りなければ_bufferのサイズを拡張)

案外見てみるとシンプルな構造してますね。

応用

例えばCySharp/NativeMemoryArrayというUnManagedな配列を扱えるライブラリではCreateBufferWriterメソッドというIBufferWriterを返すメソッドがありますね。

IBufferWriter<T> CreateBufferWriter()

github.com
github.com

IBufferWriterを経由してUnManagedな配列に書き込みができるというわけです。


またCySharp/MessagePack-CSharpという高速なシリアライザーライブラリにはBufferWriterというクラスがあり、コンストラクタにてIBufferWriterを受け取っています。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public BufferWriter(IBufferWriter<byte> output)
{
    _buffered = 0;
    _bytesCommitted = 0;
    _output = output ?? throw new ArgumentNullException(nameof(output));

    _sequencePool = default;
    _rental = default;

    var memory = _output.GetMemoryCheckResult();
    MemoryMarshal.TryGetArray(memory, out _segment);
    _span = memory.Span;
}

github.com

これによって書き込み先がメモリだろうとファイルだろうと意識することなく、書き込みを行うことが可能になっています。

File書き込みとかにも利用したい

IBufferWriterを実装したクラスでファイル書き込みとかができたら便利ですよね。ただ公式ではまだ実装されていません。

C#だとFileStreamを経由したファイル書き込みが基本なので、Stream(NetworkStreamとかも利用できるように)を保持したIBufferWriterを実装したクラスを自前で実装するしかないでしょう。