やりたいこと
byte配列 から 固定サイズの配列を含む構造体 に変換(デシリアライズ)したいです。
固定サイズの配列を含む構造体 は、『他の言語またはプラットフォームのデータ ソースと相互運用する』のような場面で有用かと思います。(MS公式 の文言を拝借)
データ構造
今回は以下のデータ構造を想定しています。
先頭に 4Byte の単一データがあり、以降に 3Byte のデータが隙間なく 128個 並んでいます。
対象データ構造の実装イメージ
直観的には以下のように書きたいですが、C#8.0 ではビルドできません。
// 3Byteの構造体(ここはビルドできます)
[StructLayout(LayoutKind.Sequential, Size = 3)]
readonly struct My3Bytes
{
public readonly byte b0, b1, b2;
}
// データ構造の実装(C#8.0ではビルドできません)
[StructLayout(LayoutKind.Sequential, Size = 4 + (3 * 128))]
readonly struct DeserializedData
{
public readonly int Param;
public readonly My3Bytes FixedBuffer[128]; // これで固定サイズ配列を定義できない
}
実装方法
実装1. 固定サイズバッファ
構造体内に固定サイズ配列を定義する手段として、固定サイズバッファ(fixed T[])が用意されています。
これを使えば良いのですが、制約が多く扱い難い印象です…
- 配列型 に 組み込み型(byte, short, int, ...)しか指定できない(3Byteの配列を作れない)
- 当該フィールドを(構造体全体を) readonly にできない
- unsafe必須
投稿時のサンプルコード
// 固定サイズバッファ
[StructLayout(LayoutKind.Sequential)]
unsafe struct DeserializedData // unsafe必須 / readonly不可
{
public readonly int Param;
public fixed byte FixedBuffer[3 * 128]; // 組み込み型限定 / readonly不可
}
// 固定サイズバッファ
[StructLayout(LayoutKind.Sequential)]
unsafe struct DeserializedData // unsafe必須 / readonly不可
{
private const int BUFFER_SIZE = 4 + 3 * 128;
private fixed byte buffer[BUFFER_SIZE]; // 組み込み型限定なのでMy3Bytes使用不可 / readonly不可
//直接デシリアライズするなら不要なコンストラクタ
public DeserializedData(byte[] sourceData)
{
for (int i = 0; i < BUFFER_SIZE; i++) // sourceDataの長さチェックは省略
buffer[i] = sourceData[i];
}
public readonly int Param
=> ((int)buffer[3] << 24) + ((int)buffer[2] << 16) + ((int)buffer[1] << 8) + buffer[0];
public readonly My3Bytes GetFixedBuffer(int index) // indexの範囲チェックは省略
=> new My3Bytes(buffer[4+index*3], buffer[4+index*3+1], buffer[4+index*3+2]);
}
@albireo さんのコメントありがとうございました!
実装2. べた書き
固定サイズの配列を分解して1行ずつべたで書けば、固定サイズバッファ を使用せずに同じデータ構造を定義できます。
固定サイズバッファの制約(組み込み型 / readonly / unsafe)がなく自由度は高いですが、想像するだけで げんなり します。(宣言だけでなくgetterも必要ですし…)
// べた書き
[StructLayout(LayoutKind.Sequential, Size = 4 + (3 * 128))]
readonly struct DeserializedData
{
public readonly int Param;
public readonly My3Bytes FixedBuffer0;
/* 宣言略(げんなり) */
public readonly My3Bytes FixedBuffer127;
public readonly My3Bytes GetFixedBuffer(int index) =>
index switch
{
0 => FixedBuffer0,
/* getter略(うんざり) */
127 => FixedBuffer127,
_ => throw new IndexOutOfRangeException()
};
}
実装3. その他
良い方法ありましたら教えて下さい!
どう対応したか?
T4 テキスト テンプレート を使ってべた書きしました。 力こそパワー💪
T4テキストテンプレートとは?
詳細は MS公式 で確認して下さい。(私は全部読まずに雰囲気で使っちゃっています…)
ポイントは以下と思っています。
- T4 テキスト テンプレートには、"実行時" と "デザイン時" の 2 種類があり、今回使うのは "デザイン時" の方
- テンプレートテキスト(.tt)から テキストファイル (.cs や .txt)を作成できる
- テキストファイルは即時作成される(ビルド時でない。テンプレートファイルを変更しただけで保存しなくても作成される)
テキストテンプレート
プロジェクトにファイルを追加します。
追加 → 新しい項目 → テキストテンプレート(.tt)テンプレートを書きます。
C# と同じノリで書けて、また変換テキストをすぐに確認できるので雰囲気で使えます。
今回は対応していませんが、GetEnumerator() や インデクサ も書いておくと便利そうです。
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #> //★出力するファイルの拡張子を指定(デフォは .txt)
<# //★ソース生成に使用されるコード
Func<int, int, string> CreateDeclarationFields = (start, count) =>
{
return string.Join(Environment.NewLine, Enumerable.Range(start, count)
.Select(i => "public readonly My3Bytes FixedBuffer" + i.ToString() + ";"));
};
Func<int, int, string> CreateSwitchFields = (start, count) =>
{
return string.Join(Environment.NewLine, Enumerable.Range(start, count)
.Select(i => i.ToString() + " => FixedBuffer" + i.ToString() + ","));
};
#> //★以降がテキストファイルとして生成される
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY T4. DO NOT CHANGE IT. CHANGE THE .tt FILE INSTEAD.
// </auto-generated>
using System;
using System.Runtime.InteropServices;
namespace CSharpT4Samples
{
[StructLayout(LayoutKind.Sequential, Size = 3)]
readonly struct My3Bytes
{
public readonly byte b0, b1, b2;
}
<# {
int count = 128; //★固定配列のサイズ
#>
//★<#= #> 内がソースコードに展開される
[StructLayout(LayoutKind.Sequential, Size = 4 + (3 * <#= count #>))]
readonly struct DeserializedData
{
public readonly int Param;
<#= CreateDeclarationFields(0, count) #> //★固定配列分フィールドを定義
public readonly My3Bytes GetFixedBuffer(int index) =>
index switch
{
<#= CreateSwitchFields(0, count) #> //★Indexのフィールドを参照
_ => throw new IndexOutOfRangeException()
};
}
<# } #>
}
生成されたテキストファイル
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY T4. DO NOT CHANGE IT. CHANGE THE .tt FILE INSTEAD.
// </auto-generated>
using System;
using System.Runtime.InteropServices;
namespace CSharpT4Samples
{
[StructLayout(LayoutKind.Sequential, Size = 3)]
readonly struct My3Bytes
{
public readonly byte b0, b1, b2;
}
[StructLayout(LayoutKind.Sequential, Size = 4 + (3 * 128))]
readonly struct DeserializedData
{
public readonly int Param;
public readonly My3Bytes FixedBuffer0;
/* 宣言略(べた書きだけど手書きじゃないのでOK) */
public readonly My3Bytes FixedBuffer127;
public readonly My3Bytes GetFixedBuffer(int index) =>
index switch
{
0 => FixedBuffer0,
/* getter略(べた書きだけど手書きじゃないのでOK) */
127 => FixedBuffer127,
_ => throw new IndexOutOfRangeException()
};
}
}
困ったこと
テンプレートコード内で、ローカル関数 や 文字列補間($"") を使うと、VSにシンタックスエラーを指摘されました。(コードは生成されますが、エラーが取れないので気持ち悪い…)
今回は以下でお茶を濁しました。
- ローカル関数 → Func を使う
- 文字列補間 → 使わない
まとめ
固定サイズの配列を持つ構造体 の実装で、T4 を使ってフィールドをべた書きしてみました。
固定サイズバッファ(fixed T[])を使用しないことで、奇数サイズの配列 や readonly属性 に対応することができます。
よりスマートな実装がありましたらコメントで教えて頂きたいです。
環境
Visual Studio Community 2019 16.7.4
.NET Core 3.1
C# 8.0