はじめに
今回はIBufferWriter<T>
の使い方・実装の仕方について書きたいと思います。
IBufferWriterの利用者視点での使い方
IBufferWriter
は基本的に以下の3ステップで実行していきます。
IBufferWriter<T>.GetSpan
かIBufferWriter<T>.GetMemory
を用いて書き込む先のメモリ領域を取得Span
かMemory
を経由してメモリに書き込みを行う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 |
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)); } } }
解説するとざっと以下の通り。
_buffer
はArrayBufferWriter
が保持するデータ(byte[]
)_index
が_buffer
のどこまで書き込まれたかを意味するAdvance
で_index
(どこまで書き込んだか)を進めるGetMemory
・GetSpan
で要求された以上のサイズを持つメモリ領域を返す(このときサイズが足りなければ_buffer
のサイズを拡張)
案外見てみるとシンプルな構造してますね。
応用
例えばCySharp/NativeMemoryArray
というUnManaged
な配列を扱えるライブラリではCreateBufferWriter
メソッドというIBufferWriter
を返すメソッドがありますね。
IBufferWriter<T> CreateBufferWriter()
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; }
これによって書き込み先がメモリだろうとファイルだろうと意識することなく、書き込みを行うことが可能になっています。
File書き込みとかにも利用したい
IBufferWriter
を実装したクラスでファイル書き込みとかができたら便利ですよね。ただ公式ではまだ実装されていません。
C#
だとFileStream
を経由したファイル書き込みが基本なので、Stream
(NetworkStream
とかも利用できるように)を保持したIBufferWriter
を実装したクラスを自前で実装するしかないでしょう。