C#のSpan<T>やReadOnlySpan<T>と戯れる
最近、フォント(.ttf)の読み込みや描画について調べていたところSpan<T>とMemoryMarshalクラスのメソッド群に触れる機会が増えたのでその備忘録です。
Span<byte>
からunmaneged type
を作る
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential,Pack=1)]
readonly struct A
{
public readonly int X;
public readonly int Y;
}
var bytes = new byte[]{0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00}.AsSpan();
var a = MemoryMarshal.AsRef<A>(bytes);
Console.WriteLine($"{a.X} {a.Y}"); // 1 2
MemoryMarsha.AsRef<T>.(Span<byte> span)
を使う場合、構造体T
は必ずunmanaged
型のみで構成されている必要があります。本来の制約としてはstruct
ですが参照型を含めると例外が発生します。
また、[StructLayout(LayoutKind.Sequential,Pack=1)]
はほぼ必須です。同じサイズの型が続いている場合、Pack=1
は省略可能ですが、int
, byte
, int
のような順序で並ぶ場合はPack
を設定しないとbyte
のフィールドと2つ目のint
のフィールドの間に2byteの隙間が挿入されるので想定通りの値とならない場合があります。
ビッグエンディアンのバイナリから構造体を作成する
OpenType(TrueType)フォーマットのフォントファイルからバイナリを読んでTable Directory(オフセットテーブルとも呼ばれる)の構造体を作成したい時、.ttf
はビッグエンディアンなので少し工夫が必要です。
[StructLayout(LayoutKind.Sequential, Pack=1)]
readonly struct TableDirectory
{
public readonly ushort RangeShift;
public readonly ushort EntrySelector;
public readonly ushort SearchRange;
public readonly ushort NumTables;
public readonly uint SfntVersion;
}
var path = @"yumin.ttf";
using var fp = File.OpenRead(path);
Span<byte> buffer = stackalloc byte[Unsafe.SizeOf<TableDirectory>()];
// TableDirectoryのサイズ(12byte)分読み取り
fp.Read(buffer);
buffer.Reverse();
var dir = MemoryMarshal.AsRef(buffer);
Console.WriteLine($"0x{SfntVersion:X8} {NumTables} {SearchRange} {EntrySelector} {RangeShift}");
0x00010000 21 80 256 4
こちらは游明朝のフォントファイルを読んだ時の値です。
Span<byte>
からプリミティブ型を作成する
エンディアンを指定してSystem.Buffers.Binary.BinaryPrimitives
クラスを使うと便利です。
前述のTable Directory読み取りも特に構造体を用意しない場合は以下のような形になります。
using var fp = File.OpenRead(@"yumin.ttf");
Span<byte> buffer = stackalloc byte[sizeof(ushort) * 4 + sizeof(uint)];
fp.Read(buffer);
var sfntVersion = BinaryPrimitives.ReadUInt32BigEndian(buffer.Slice(0,sizeof(uint)));
var numTables = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(sizeof(uint),sizeof(ushort)));
// 略
内部的にはMemoryMarshal.Read<T>
でSpan<byte>
を読んだあと、BitConverter.IsLittleEndian
を判定したのちReverseEndianness
メソッドで転回させています。
Span<TFrom>
からSpan<TTo>
を作成する
[StructLayout(LayoutKind.Sequential,Pack=1)]
readonly struct B
{
public readonly ushort B1;
public readonly ushort B2;
}
var bytes = new byte[]{0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00}.AsSpan();
var intSpan = MemoryMarshal.Cast<byte, int>(bytes);
foreach(var i in intSpan)
{
Console.WriteLine(i);
}
var foo = MemoryMarshal.Cast<int, B>(intSpan);
foreach(var f in foo)
{
Console.WriteLine($"{f.B1} {f.B2}");
}
1
16
1 0
16 0
ある型のSpanを別の型へ変換することもできます。
バイナリデータから構造体のSpan
や配列を作るのに便利です。
string
からSpan<char>
を作成して書き換える
少々邪悪かもしれませんがReadOnlySpan<char>
をSpan<char>
へ変換して中身を変更することができます。
unsafe
無しにこれが出来ていいのだろうか……?
var str = "hello";
var span = MemoryMarshal.CreateSpan<char>(ref MemoryMarshal.GetReference(str.AsSpan()), str.Length);
span.Reverse();
Console.WriteLine(str);
span[0] = 'l';
Console.WriteLine(str);
olleh
llleh
GetReference<T>(Span<T> span)
ではspan[0]
の参照を返します。どういう訳かReadOnlySpan<T>
を引数に取るメソッドもあります。
似たようなメソッドとしてはGetArrayDataReference<T>(T[] array)
という配列の先頭の参照を返すものもあります。
MemoryMarshal.CreateSpan<T>
はinternal
となっていて直接は呼べないnew Span<T>(ref T ptr, int length)
のコンストラクタを呼んでいます。
これで晴れてstring
の文字を直接操作できるようになりました。
配列の先頭の参照からSpanを作る
T[]
の先頭の参照からCreateSpan
でSpan<T>
を作成するのを利用するとこんなこともできます。
ref int GetArray() => ref new int[]{1,2,3,4,5}[0];
ref int a = ref GetArray();
var span = MemoryMarshal.CreateSpan(ref a, 5);
foreach(var i in span)
{
Console.WriteLine(i);
}
1
2
3
4
5
CreateSpan
の第二引数をGetArray()
で返した配列の要素数以上にすると...
1
2
3
4
5
0
0
0
-1675796560
32763
length
の値は一切チェックされない為、初期化されていない部分が覗けます。特に例外も発生しませんがかなりアヤシイ。
終わりに
MemoryMarshal
で定義されているメソッドはSpan<T>
やReadOnlySpan<T>
と関連した型の相互運用の為に作られたように見えます。内部的にはUnsafe
クラスのメソッドを使用しており、一見安全そうな顔をしていますが全然安全ではなさそうです。一周回ってポインタめいた話に戻ってきたところはありますが、上手く利用してハイパフォーマンスなコードを書いていきたいですね。業務でプログラミングしてませんが…。
Discussion