Edited at

Mono.Cecil入門

このテーマに関しては初投稿です。

8月2日はマケドニアの革命記念日です。

この記事はUnityゆるふわサマーアドベントカレンダー 2019 #ゆるふわアドカレの2日目の記事です。

前日は@nkjzmさんのUnityのTimelineでオーディオの再生終了をループで待つでした。

翌日はぐるたかさん【Unity】TextMeshProを導入してみる【エラー対応・ 日本語フォント作成あり】です。


まえがき

以下この文章は基本的にC#8に基づいている。

しかし、APIはNet Standard 2.0レベルであるためUnityでもどこでも問題なく使えるだろう。


Mono.Cecilとは

Jb Evainが主にメンテナンスを行っているILバイナリを編纂する為のライブラリである。基本的にECMA標準に従って構成されているDLLバイナリを読み込んでそこに含まれる全情報(型やフィールド、属性など)を供覧し、編集した後有効なDLLとして出力することができる。

UnityにおいてはBurst Compilerが内部的にMono.Cecilを利用している。そのため、実に珍しいことにupmでinstall可能な非Unity由来のパッケージとなっている。


第0章 Mono.Cecilの適用範囲

静的事前コード生成における競争相手はRoslynである。

また、動的コード生成も考慮せねばなるまい。その場合dynamic型、DynamicMethod, AssemblyBuilderが競合技術となるであろう。

Fodyは扱うのが面倒なMono.Cecilのラッパーである。

Mono.CecilはAPIがECMA標準にべったりでかなり低レイヤーであり、Fodyはそれを適切に隠蔽することでプログラマにかかる負担を低減する役目を果たしている。

以上の対抗馬とMono.Cecilを比較し、いかなる場合に使うべきかを判断するべきである。


対Roslyn

元となるソースコード(からコンパイラの出力したDLL)を解析し、それに対応した適切な何かを生成する。これがコード生成の本懐である。

コード生成が必要とされる主な場面はやはりシリアライズであろう。シリアライズしたい型に対して高度に最適化されたシリアライザを生成することは必須である。

動的コード生成が許されているならばDynamicMethodやAssemblyBuilderを用いた方が実行時にしか知りえない情報を用いて極めてアグレッシブな最適化を行えるだろう。だが、動的コード生成が禁止された環境ならば事前コード生成であるMono.Cecilにも必要性が生じる。

この場合は、基本的にRoslynの方が柔軟である。Roslynによるコード生成の最終成果物がC#ファイルであるのに対して、Mono.CecilのそれはDLLファイルである。

デバッガビリティは明らかにC#ファイルを扱うRoslynが優れている。ステップ実行やブレークポイントの挿入などはC#ファイルを相手にせねば到底なしえないからである。

この利点だけでMono.Cecilを使わない十分な理由となるだろう。

では、Mono.Cecilの優越性とはなんであろうか?

それはILを扱うという点にある。

ILにはC#の許可されていない表現を扱える力がある。ILの持つ力の一部はSystem.Runtime.CompilerServices.Unsafeクラスに現れているが、それ以外にも様々なC#では不可能な表現がILでは合法的に存在するのだ。

Mono.Cecilをわざわざ扱うならばそれはIL暗黒魔術を必要としていると言えるだろう。


第1章 UnityへのMono.Cecilインストール

Mono.Cecilは例外的にUPMでインストール可能である。

Packages/manifest.jsonを開き、以下の1行をdependencies内に追記する。

Package Managerから追加することが可能であればそれでもよい。

"nuget.mono-cecil": "0.1.6-preview",

image.png

Package Managerを見ると上のような表記になっていることだろう。

そして、次に適当なAssembly Definition Fileに参照を追加する。

image.png

しかし、通常はMono.Cecilの参照を追加することは出来ない。

InspectorのGeneral/Override Referencesにチェックを入れるのである。

image.png

Assembly Referencesという項目が新たに生じるだろう。

とりあえず4つのDLLを参照しておけばおそらく間違いはない。

image.png

以上の工程を経てMono.CecilをUPM経由で利用できるようになった。


第2章 Hello, world

#nullable enable

using System;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;

class Program
{
static void Main()
{
var mainModuleDefinition = CreateMain();
ModuleDefinition systemModuleDefinition = GetSystemModule();

DefineEntryType(mainModuleDefinition, systemModuleDefinition);
mainModuleDefinition.Write("../HelloWorld.exe");
}

private static void DefineEntryType(ModuleDefinition mainModuleDefinition, ModuleDefinition systemModuleDefinition)
{
TypeDefinition entryTypeDefinition = new TypeDefinition("", "Program", TypeAttributes.Class | TypeAttributes.Public, mainModuleDefinition.TypeSystem.Object);

MethodDefinition constructorMethodDefinition = DefineConstructor(mainModuleDefinition);

MethodDefinition mainMethodDefinition = DefineMain(mainModuleDefinition, systemModuleDefinition);

mainModuleDefinition.EntryPoint = mainMethodDefinition;
entryTypeDefinition.Methods.Add(mainMethodDefinition);
entryTypeDefinition.Methods.Add(constructorMethodDefinition);
mainModuleDefinition.Types.Add(entryTypeDefinition);
}

private static ModuleDefinition CreateMain() =>
AssemblyDefinition.CreateAssembly(new AssemblyNameDefinition("HW", new Version(1, 0, 0)), "HelloWorld", new ModuleParameters()
{
Kind = ModuleKind.Console,
}).MainModule;

private static ModuleDefinition GetSystemModule()
{
const string netStandardDllPath = "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder\\netstandard.library\\2.0.3\\build\\netstandard2.0\\ref\\netstandard.dll";
AssemblyDefinition systemAssemblyDefinition = AssemblyDefinition.ReadAssembly(netStandardDllPath);
ModuleDefinition systemModuleDefinition = systemAssemblyDefinition.MainModule;
return systemModuleDefinition;
}

private static MethodDefinition DefineMain(ModuleDefinition mainModuleDefinition, ModuleDefinition systemModuleDefinition)
{
MethodDefinition mainMethodDefinition = new MethodDefinition("Main", MethodAttributes.Static | MethodAttributes.Public, mainModuleDefinition.TypeSystem.Void);

ParameterDefinition argsParameterDefinition = new ParameterDefinition("args", ParameterAttributes.None, new ArrayType(mainModuleDefinition.TypeSystem.String));
mainMethodDefinition.Parameters.Add(argsParameterDefinition);

MethodBody body = mainMethodDefinition.Body;

static bool Predicate(MethodDefinition method) => method.Name == "WriteLine" && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.Name == "String";

MethodReference writeLine = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Console").Methods.Single(Predicate));

ILProcessor processor = body.GetILProcessor();

processor.Append(Instruction.Create(OpCodes.Ldstr, "Hello, world!"));
processor.Append(Instruction.Create(OpCodes.Call, writeLine));
processor.Append(Instruction.Create(OpCodes.Ret));

return mainMethodDefinition;
}

private static MethodDefinition DefineConstructor(ModuleDefinition mainModuleDefinition)
{
MethodDefinition constructorMethodDefinition = new MethodDefinition(".ctor", MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.Public, mainModuleDefinition.TypeSystem.Void);

MethodBody body = constructorMethodDefinition.Body;
ILProcessor processor = body.GetILProcessor();

MethodReference objectConstructorMethodReference = mainModuleDefinition.ImportReference(mainModuleDefinition.TypeSystem.Object.Resolve().Methods.Single(x => x.Name == ".ctor"));

processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Call, objectConstructorMethodReference));
processor.Append(Instruction.Create(OpCodes.Ret));

return constructorMethodDefinition;
}
}

最初の一歩として適切なものはやはりHello, worldであろう。

今回は一からアセンブリを作成し、そこにProgramクラスを定義し、簡単なコンストラクタとMainメソッドを定義している。

Mainメソッド内ではnetstandardDLLからSystem.Console.WriteLineメソッドを呼び出している。


モジュールとは

モジュールとはアセンブリであり、DLL/EXEファイルと理解すればおおよそ間違いは少ない。

補足ソースコードはコンパイルされるとDLLまたはEXEファイルとなる。

DLLあるいはEXEファイルはアセンブリを1つ含む。

アセンブリは基本的に1つのモジュールを含む。

複数のモジュールを持つことも理論的には可能であるが、MSBuildのサポートが薄いなどの理由で基本的にアセンブリ(DLLファイル)とモジュールは1対1関係にあると見なしてよい。


アセンブリ読み込みと作成

通常Mono.Cecilを利用する場合複数(最低でも2つ)のアセンブリを利用することになるだろう。

本当に基本的な型はModuleDefinition.Typesインスタンスプロパティ(TypeSystem型)のプロパティとして入手できるが、DateTimeやDecimalなどその他の基本的な型を扱えないからである。

本例ではSystem.Consoleを使用するためにnetstandardアセンブリを読み込むことにした。


GetSystem

private static ModuleDefinition GetSystemModule()

{
const string netStandardDllPath = "C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder\\netstandard.library\\2.0.3\\build\\netstandard2.0\\ref\\netstandard.dll";
AssemblyDefinition systemAssemblyDefinition = AssemblyDefinition.ReadAssembly(netStandardDllPath);
ModuleDefinition systemModuleDefinition = systemAssemblyDefinition.MainModule;
return systemModuleDefinition;
}

既にあるアセンブリを読み込む場合はAssemblyDefinition AssemblyDefinition.ReadAssembly(string fileName)を使うことを奨励する。

他のオーバーロードにはStreamを読み込むものも存在するが、ファイルパス指定はかなり手軽である。(なお、読み込むファイルの拡張子は何でも良い。ファイルパス指定は内部でFileStreamを生成して読み込んでいるからである。)

個人的な感想であるが、完全に一からアセンブリを構築するのは勉強目的以外ではオススメしない。ILSpyなどで覗いてみるとわかるが。コンパイラ製DLLには様々な情報が埋め込まれており、それを手作業でプログラミングして埋め込むのは苦行であろう。

余談Mono.Cecilについて本記事以外を読むと例えば次のような記述を見掛けるかもしれない。

MethodReference reference = mainModuleDefinition.ImportReference(typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));

C#標準のリフレクションを使用してType型オブジェクトやMethodBase型オブジェクトを得、それを作業対象モジュールにImportReferenceすることで適切なMethodReferenceを得られる。

基本的にはこれでも何も問題はない。しかし、この方法で得られたMethodReferenceはそのプログラムの実行環境のものとなる。

つまり、netstandardではなくmscorlibやnetcoreappなどになるということである。netstandardを対象ランタイムとしてビルドされたライブラリを編集する場合にはリフレクションを利用してTypeInfoやMethodInfoを得てはならないのである。


モジュールに対してエントリポイントとなるクラスを追加する


DefineEntryType

private static void DefineEntryType(ModuleDefinition mainModuleDefinition, ModuleDefinition systemModuleDefinition)

{
TypeDefinition entryTypeDefinition = new TypeDefinition("", "Program", TypeAttributes.Class | TypeAttributes.Public, mainModuleDefinition.TypeSystem.Object);

MethodDefinition constructorMethodDefinition = DefineConstructor(mainModuleDefinition);

MethodDefinition mainMethodDefinition = DefineMain(mainModuleDefinition, systemModuleDefinition);

mainModuleDefinition.EntryPoint = mainMethodDefinition;
entryTypeDefinition.Methods.Add(mainMethodDefinition);
entryTypeDefinition.Methods.Add(constructorMethodDefinition);
mainModuleDefinition.Types.Add(entryTypeDefinition);
}


モジュールに対して新規に型を定義して追加することは簡単である。

TypeDefinition型のコンストラクタで新たにインスタンスを作成し、それをモジュールのTypesプロパティ(Mono.Collections.Generic.Collection)にAddすることで実現できる。

一応コンストラクタの動きを追いかけてみる。


TypeDefinition.cs

public TypeDefinition(string @namespace, string name, TypeAttributes attributes, TypeReference baseType) : this(@namespace, name, attributes) => this.BaseType = baseType;

public TypeDefinition(string @namespace, string name, TypeAttributes attributes) : base(@namespace, name)
{
this.attributes = (uint)attributes;
this.token = new MetadataToken(TokenType.TypeDef);
}


TypeReference.cs

protected TypeReference(string @namespace, string name) : base(name)

{
this.@namespace = @namespace ?? string.Empty;
this.token = new MetadataToken(TokenType.TypeRef, 0);
}
internal MemberReference(string name) => this.name = name ?? string.Empty;

名前空間、型名、型の属性情報、基底型を設定している。

さて、コンストラクタでTypeDefinitionを作成した後はその型にエントリポイントとなるメソッドとコンストラクタを定義した。

メソッドの定義はDefineMainとDefineConstructorと2メソッドに分割し、必要な要素を新たに加えた。具体的手順の解説については後述する。

エントリポイントの設定方法はモジュールのEntryPointプロパティにMethodDefinitionを代入することである。

そして型にメソッドを追加する場合はTypeDefinition型のMethodsプロパティにMethodDefinitionをAddすることで実現可能である。


エントリポイントを定義する

private static MethodDefinition DefineMain(ModuleDefinition mainModuleDefinition, ModuleDefinition systemModuleDefinition)

{
MethodDefinition mainMethodDefinition = new MethodDefinition("Main", MethodAttributes.Static | MethodAttributes.Public, mainModuleDefinition.TypeSystem.Void);

ParameterDefinition argsParameterDefinition = new ParameterDefinition("args", ParameterAttributes.None, new ArrayType(mainModuleDefinition.TypeSystem.String));
mainMethodDefinition.Parameters.Add(argsParameterDefinition);

MethodBody body = mainMethodDefinition.Body;
ILProcessor processor = body.GetILProcessor();

static bool Predicate(MethodDefinition method) => method.Name == "WriteLine" && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.Name == "String";

MethodReference writeLine = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Console").Methods.Single(Predicate));

processor.Append(Instruction.Create(OpCodes.Ldstr, "Hello, world!"));
processor.Append(Instruction.Create(OpCodes.Call, writeLine));
processor.Append(Instruction.Create(OpCodes.Ret));

return mainMethodDefinition;
}

MethodDefinitonコンストラクタのシグネチャはMethodDefinition(string name, MethodAttributes attributes, TypeReference returnType)である。

MethodAttributesの詳細設定についてはILSpyなどで答えを見て書くのが最も正確である。

さて、本例においてはpublicでstaticで戻り値がvoidなMainメソッドを新規作成し、そのパラメータとしてstringの配列で名前がargsなものを追加した。

メソッド内の具体的処理内容についてはMethodBody型のBodyプロパティからGetILProcessor()を呼び、ILProcessorインスタンスを操作することで記述するのである。

ILProcessorに対してはIL命令を表すInstruction型のインスタンスをAppendする。

Instruction型のインスタンスを作成するのはInstruction.Create静的メソッドまたはILProcessor.Createメソッド(実体は単なるInstruction.Createのラッパー)のどちらかが適切と思われる。

なお、Instruction.CreateはInstructionのコンストラクタのラッパーである。元となるコンストラクタのシグネチャはInstuction(OpCode code, object value)であり、使いにくいのでやはりCreate静的メソッドを使う方が賢明だろうか。

各OpCodeの働きについてはECMAの仕様書英語版Wikipediaの記事を参照してほしい。

OpCodes.Callの引数にはMethodReferenceが必須である。

static bool Predicate(MethodDefinition method) => method.Name == "WriteLine" && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.Name == "String";

MethodReference writeLine = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Console").Methods.Single(Predicate));

故に上記のようにsystemModuleDefinitionからSystem.Console型を検索し、そこに含まれるメソッドからstring1つを引数に取るWriteLineメソッドのMethodReferenceを得、作業対象のモジュールであるmainModuleDefinitionにImportReferenceしたのである。

ImportReferenceの戻り値であるTypeReferenceをOpCodes.Callの引数として与えることでSystem.Console.WriteLine(string)の呼び出しを行うのだ。


拡張メソッドで便利に扱う

ILProcessorは.NETのILGenerator並に扱い辛いAPIである。Sigilのような便利なラッパーは私が調べた範囲では見当たらないので自作する他ない。

一応Mono.Cecil.RocksはUtility的クラスをまとめたアセンブリである。しかし、ILProcessor周りのUtilityとしては使いものにならないので無いと言ってもそこまで間違いはないだろう。

この手の拡張メソッドは実際に使う範囲に対して少しずつ秘伝のタレのように成熟させながら作るようにしている。プロジェクト固有性がかなり高い上に網羅するには膨大すぎるからである。

本例では次のようなHelperクラスとなった。

using Mono.Cecil;

using Mono.Cecil.Cil;

static class Helper
{
public static ILProcessor LdStr(this ILProcessor processor, string value)
{
processor.Append(Instruction.Create(OpCodes.Ldstr, value));
return processor;
}

public static ILProcessor Call(this ILProcessor processor, MethodReference methodReference)
{
processor.Append(Instruction.Create(OpCodes.Call, methodReference));
return processor;
}

public static ILProcessor Ret(this ILProcessor processor)
{
processor.Append(Instruction.Create(OpCodes.Ret));
return processor;
}

public static ILProcessor LdArg(this ILProcessor processor, int index)
{
switch (index)
{
case 0:
processor.Append(Instruction.Create(OpCodes.Ldarg_0));
break;
case 1:
processor.Append(Instruction.Create(OpCodes.Ldarg_1));
break;
case 2:
processor.Append(Instruction.Create(OpCodes.Ldarg_2));
break;
case 3:
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
break;
default:
ParameterDefinition parameterDefinition = processor.Body.Method.Parameters[index];
processor.Append(Instruction.Create(index <= 255 ? OpCodes.Ldarg_S : OpCodes.Ldarg, parameterDefinition));
break;
}
return processor;
}
}

LdArgに関して補足をしよう。出力されるDLLのサイズを小さくすることでメモリに優しく最適化することができる。

そのためにILにはいくつか特別な命令が用意されている。第0引数から第3引数までは1byteでスタックにプッシュできるLdarg_0~3や、第4から第255引数までを2byteでプッシュできるLdarg_Sなどである。

こういう特別な短縮形ではないLdargは命令自体が2byte、命令の引数も2byteで合計4byteの長さとなる。(余談だが、このことから.NET CLRにおける引数の上限は第65535引数であるとわかる。)

統一的に扱うためにこうしてLdArgという拡張メソッドを定義したのである。

また、ここで補足しておくべきことに、Mono.CecilのOpCodes.Ldarg〇〇はParameterDefinitionを引数に取る。元々のILの仕様ではbyteだったりushortだったりするが、引数の番地を表す数値型を受け取る。何故参照型にわざわざするのだろうか?

初心者が犯しがちなミスについても補うならば、ParameterDefinitionやInstructionの使い回しは厳に慎むべきである。

特にInstructionはLd〇〇やSt〇〇など何度も何度も全く同じ内容をインスタンス化する故に無駄に見えるアロケーションが発生するだろう。

だが、使い回しをしてしまうと、後にif文などで制御構造で命令間を飛び回る時にエラーを吐いてしまう。

Mono.Cecilは事前生成であるのだから諦めて全く同じ内容のインスタンスを沢山生成しよう。

この拡張メソッドを用いて書き直すとDefineMainは次のようになる。

private static MethodDefinition DefineMain(ModuleDefinition mainModuleDefinition, ModuleDefinition systemModuleDefinition)

{
MethodDefinition mainMethodDefinition = new MethodDefinition("Main", MethodAttributes.Static | MethodAttributes.Public, mainModuleDefinition.TypeSystem.Void);

ParameterDefinition argsParameterDefinition = new ParameterDefinition("args", ParameterAttributes.None, new ArrayType(mainModuleDefinition.TypeSystem.String));
mainMethodDefinition.Parameters.Add(argsParameterDefinition);

MethodBody body = mainMethodDefinition.Body;

static bool Predicate(MethodDefinition method) => method.Name == "WriteLine" && method.Parameters.Count == 1 && method.Parameters[0].ParameterType.Name == "String";

MethodReference writeLine = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Console").Methods.Single(Predicate));

body.GetILProcessor()
.LdStr("Hello, world")
.Call(writeLine)
.Ret();

return mainMethodDefinition;
}


モジュール出力

mainModuleDefinition.Write("../HelloWorld.exe");

ModuleDefinitionのインスタンスメソッドWriteに出力先のファイルパスまたはStreamを与えることで書き出しを行える。

だが、おそらくMono.Cecilについて他の記事を読まれた方はこの部分に引っかかりを覚えるだろう。

AssemblyDefinitionのWriteではないからである。

内部を覗いてみよう。

public void Write(string fileName)

{
this.Write(fileName, new WriterParameters());
}

public void Write(string fileName, WriterParameters parameters)
{
this.main_module.Write(fileName, parameters);
}

ご覧の通り、内部的にMainModuleのWriteを呼び出している。

故に本例ではAssemblyDefinitionを保持するのも手間なのでModuleDefinitionのみを相手にしている。

以上を以て最初のサンプルであるHello, worldの解説とする。


第3章 簡単なJSONシリアライザを実装する

このサンプルは4つのプロジェクトと2つのソリューションで構成されている。

[SerializeBase.Serialize]を付与された任意の型について、そのpublicフィールド全てをJSONを表すbyte列にシリアライズするシリアライザを生成するものである。

デシリアライザについては今回は無視している。

基本的な実装方針については@neuecc先生のILをいじりつつの動的生成に関する記事を読むのが良い。


  • Base


    • SerializeBase.csproj シリアライザを表すinterfaceと種々の型を処理するシリアライザを登録し、管理するシリアライザリゾルバを表すinterfaceを定義するプロジェクト

    • SerializeTarget.csproj シリアライザを生成したいクラスを含んでいるプロジェクト ここにシリアライザを追加する

    • SerializerGenerator.csproj シリアライザを生成するMainプログラムを含むプロジェクト



  • Program


    • SerializeProgram.csproj 生成したシリアライザの動作確認をするMainプログラムを含むプロジェクト




ソースコード全文


SerializeBase

using System;

namespace SerializeBase
{
public unsafe interface ISerializer<T>
{
long TryWrite(T value, byte* destination, long capacity);
}
public interface ISerializerResolver
{
bool Register<T>(ISerializer<T> serializer);
ISerializer<T>? Get<T>();
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = true)]
public sealed class SerializeAttribute : Attribute {}
}



SerializeTarget

namespace SerializeTarget

{
[SerializeBase.Serialize]
public sealed class Class1
{
public int XYZ;
public int HogeHoge;
}
}


SerializeGenerator

using System.Linq;

using System.Text;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;

namespace SerializerGenerator
{
static class Program
{
static void Main()
{
(ModuleDefinition systemModuleDefinition, ModuleDefinition baseModuleDefinition, TypeDefinition serializeAttributeTypeDefinition, ModuleDefinition mainModuleDefinition) = InitializeModules();
CreateSerializers(mainModuleDefinition, serializeAttributeTypeDefinition, systemModuleDefinition, baseModuleDefinition);
mainModuleDefinition.Write("../../../../SerializeTarget/bin/SerializeTarget.dll");
}
private static (ModuleDefinition systemModuleDefinition, ModuleDefinition baseModuleDefinition, TypeDefinition serializeAttributeTypeDefinition, ModuleDefinition mainModuleDefinition) InitializeModules()
{
ModuleDefinition systemModuleDefinition = GetSystemModule();
ModuleDefinition baseModuleDefinition = GetModule("../../../../SerializeTarget/bin/Release/netstandard2.0/SerializeBase.dll");
TypeDefinition serializeAttributeTypeDefinition = baseModuleDefinition.GetType("SerializeBase", "SerializeAttribute");
ModuleDefinition mainModuleDefinition = GetModule("../../../../SerializeTarget/bin/Release/netstandard2.0/SerializeTarget.dll");
return (systemModuleDefinition, baseModuleDefinition, serializeAttributeTypeDefinition, mainModuleDefinition);
}

private static void CreateSerializers(ModuleDefinition mainModuleDefinition, TypeDefinition serializeAttributeTypeDefinition, ModuleDefinition systemModuleDefinition, ModuleDefinition baseModuleDefinition)
{
TypeDefinition iSerializerDefinition = baseModuleDefinition.GetType("SerializeBase", "ISerializer`1");
TypeReference iSerializerImportReference = mainModuleDefinition.ImportReference(iSerializerDefinition);
TypeDefinition resolverTypeDefinition = baseModuleDefinition.GetType("SerializeBase", "ISerializerResolver");
TypeReference resolverImportReference = mainModuleDefinition.ImportReference(resolverTypeDefinition);
MethodReference objectCtorImportReference = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Object").Methods.First(x => x.Name == ".ctor"));
MethodDefinition getMethodDefinition = resolverTypeDefinition.Methods.First(x => x.Name == "Get");

int count = mainModuleDefinition.Types.Count;
for (var i = 0; i < count; i++)
{
TypeDefinition typeDefinition = mainModuleDefinition.Types[i];
if (!typeDefinition.HasCustomAttributes) continue;
if (!typeDefinition.HasFields) continue;
if (typeDefinition.Fields.All(x => !x.IsPublic)) continue;
if (typeDefinition.CustomAttributes.All(x => x.AttributeType.FullName != serializeAttributeTypeDefinition.FullName)) continue;
mainModuleDefinition.Types.Add(CreateSerializer(typeDefinition, mainModuleDefinition, iSerializerDefinition, iSerializerImportReference, resolverImportReference, objectCtorImportReference, getMethodDefinition));
}
}

private static TypeDefinition CreateSerializer(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, TypeReference iSerializerImportReference, TypeReference resolverImportReference, MethodReference objectCtorImportReference, MethodDefinition getMethodDefinition)
{
TypeDefinition serializer = new TypeDefinition(
typeDefinition.Namespace,
typeDefinition.Name + "Serializer",
TypeAttributes.Public | TypeAttributes.BeforeFieldInit | TypeAttributes.Sealed,
mainModuleDefinition.TypeSystem.Object);

FieldDefinition resolverFieldDefinition = DefineResolverFieldDefinition(resolverImportReference, serializer);
DefineConstructor(mainModuleDefinition, serializer, resolverImportReference, resolverFieldDefinition, objectCtorImportReference);
ImplementISerializer(typeDefinition, mainModuleDefinition, iSerializerDefinition, iSerializerImportReference, serializer, resolverFieldDefinition, getMethodDefinition);
return serializer;
}

private static void ImplementISerializer(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, TypeReference iSerializerImportReference, TypeDefinition serializer, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)
{
serializer.Interfaces.Add(new InterfaceImplementation(new GenericInstanceType(iSerializerImportReference)
{
GenericArguments = { typeDefinition }
}));
DefineTryWrite(typeDefinition, mainModuleDefinition, iSerializerDefinition, resolverFieldDefinition, serializer, getMethodDefinition);
}

private static void DefineTryWrite(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, FieldDefinition resolverFieldDefinition, TypeDefinition serializer, MethodDefinition getMethodDefinition)
{
MethodDefinition tryWriteMethod = new MethodDefinition("TryWrite", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual, mainModuleDefinition.TypeSystem.Int64);
ParameterDefinition valueParam = new ParameterDefinition("value", ParameterAttributes.None, typeDefinition);
ParameterDefinition destinationParam = new ParameterDefinition("destination", ParameterAttributes.None, new PointerType(mainModuleDefinition.TypeSystem.Byte));
ParameterDefinition capacityParam = new ParameterDefinition("capacity", ParameterAttributes.None, mainModuleDefinition.TypeSystem.Int64);
tryWriteMethod.Parameters.Add(valueParam);
tryWriteMethod.Parameters.Add(destinationParam);
tryWriteMethod.Parameters.Add(capacityParam);

FieldDefinition[] fieldArray = typeDefinition.Fields.Where(x => x.IsPublic).ToArray();

ILProcessor processor = tryWriteMethod.Body.GetILProcessor();
switch (fieldArray.Length)
{
case 0:
FillZeroField(processor);
break;
case 1:
FillOneField(mainModuleDefinition, processor, iSerializerDefinition, fieldArray, Encoding.UTF8, resolverFieldDefinition, getMethodDefinition);
break;
default:
FillFields(mainModuleDefinition, processor, iSerializerDefinition, fieldArray, Encoding.UTF8, resolverFieldDefinition, getMethodDefinition);
break;
}
serializer.Methods.Add(tryWriteMethod);
}

private static void FillFields(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, FieldDefinition[] fieldArray, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)
{
Collection<VariableDefinition> variableDefinitions = processor.Body.Variables;
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));
variableDefinitions.Add(new VariableDefinition(new PointerType(mainModuleDefinition.TypeSystem.Byte)));
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));

// ,:0
long initialNeedLength = fieldArray.Aggregate(0, (accumulation, definition) => accumulation + utf8.GetByteCount(definition.Name)) + 1L + fieldArray.Length * 3L;

Instruction il0000 = Instruction.Create(OpCodes.Ldloc_0);

processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Neg));
processor.Append(Instruction.Create(OpCodes.Stloc_0));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Stloc_2));
processor.Append(Instruction.Create(OpCodes.Bgt, il0000));

processor.Append(Instruction.Create(OpCodes.Ldarg_2));
WriteChar(processor, '{');

WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[0], il0000, ref initialNeedLength);

for (var i = 1; i < fieldArray.Length; i++)
{
processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
WriteChar(processor, ',');
WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[i], il0000, ref initialNeedLength);
}

processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)'}'));
processor.Append(Instruction.Create(OpCodes.Stind_I1));

processor.Append(Instruction.Create(OpCodes.Ldloc_2));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.Append(il0000);
processor.Append(Instruction.Create(OpCodes.Ldloc_2));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));
}

private static void WriteField(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition, FieldDefinition fieldDefinition, Instruction jumpDestinationWhenFail, ref long initialNeedLength)
{
byte[] nameBytes = utf8.GetBytes(fieldDefinition.Name);

WriteName(processor, nameBytes);
WriteChar(processor, ':');
processor.Append(Instruction.Create(OpCodes.Stloc_1));

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.AppendRange(LoadConstantInt64(2L + nameBytes.LongLength));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));

processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Ldfld, resolverFieldDefinition));
MethodReference getImportReference = mainModuleDefinition.ImportReference(new GenericInstanceMethod(getMethodDefinition)
{
GenericArguments = { fieldDefinition.FieldType }
});
processor.Append(Instruction.Create(OpCodes.Callvirt, getImportReference));

processor.Append(Instruction.Create(OpCodes.Ldarg_1));
processor.Append(Instruction.Create(OpCodes.Ldfld, fieldDefinition));
processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
MethodReference tryWriteImportReference = mainModuleDefinition.ImportReference(MakeHostInstanceGeneric(iSerializerDefinition.Methods[0], fieldDefinition.FieldType));
processor.Append(Instruction.Create(OpCodes.Callvirt, tryWriteImportReference));

processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Stloc_0));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_0));
processor.Append(Instruction.Create(OpCodes.Conv_I8));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));
initialNeedLength -= 3L + nameBytes.LongLength;
processor.AppendRange(LoadConstantInt64(initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));
}

private static Instruction[] LoadConstantInt64(long value)
{
Instruction[] answer;
switch (value)
{
case -1:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_M1),
Instruction.Create(OpCodes.Conv_I8)
};
break;
case 0:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_0),
Instruction.Create(OpCodes.Conv_I8)
};
break;
case 1:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_1),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 2:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_2),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 3:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_3),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 4:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_4),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 5:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_5),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 6:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_6),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 7:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_7),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 8:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_8),
Instruction.Create(OpCodes.Conv_I8),
};
break;
default:
if (value <= sbyte.MaxValue && value >= sbyte.MinValue)
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)value),
Instruction.Create(OpCodes.Conv_I8),
};
}
else if (value <= int.MaxValue && value >= int.MinValue)
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4, (int)value),
Instruction.Create(OpCodes.Conv_I8),
};
}
else
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I8, value),
};
}
break;
}
return answer;
}

private static void AppendRange(this ILProcessor processor, Instruction[] instructions)
{
for (var i = 0; i < instructions.Length; i++)
{
processor.Append(instructions[i]);
}
}

private static void FillOneField(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, FieldDefinition[] fieldArray, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)
{
Collection<VariableDefinition> variableDefinitions = processor.Body.Variables;
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));
variableDefinitions.Add(new VariableDefinition(new PointerType(mainModuleDefinition.TypeSystem.Byte)));

long initialNeedLength = utf8.GetByteCount(fieldArray[0].Name) + 4L;

Instruction[] firstFailInstructions = LoadConstantInt64(-initialNeedLength);
Instruction[] secondFailInstructions = LoadConstantInt64(-initialNeedLength + 1L);

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Blt_S, firstFailInstructions[0]));

processor.Append(Instruction.Create(OpCodes.Ldarg_2));
WriteChar(processor, '{');
WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[0], secondFailInstructions[0], ref initialNeedLength);

processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)'}'));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength - 1L));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.AppendRange(secondFailInstructions);
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.AppendRange(firstFailInstructions);
processor.Append(Instruction.Create(OpCodes.Ret));
}

private static void WriteChar(ILProcessor processor, char character)
{
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)character));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
}

private static void WriteName(ILProcessor processor, byte[] nameBytes)
{
for (var index = 0; index < nameBytes.Length; index++)
{
WriteByte(processor, nameBytes[index]);
}
}

private static void WriteByte(ILProcessor processor, byte character)
{
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)character));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
}

private static void FillZeroField(ILProcessor processor)
{
Instruction il0000 = Instruction.Create(OpCodes.Ldc_I8, -2L);
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_2));
processor.Append(Instruction.Create(OpCodes.Blt_S, il0000));
processor.Append(Instruction.Create(OpCodes.Ldarg_2));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)0x7B));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)0x7D));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, 2L));
processor.Append(Instruction.Create(OpCodes.Ret));
processor.Append(il0000);
processor.Append(Instruction.Create(OpCodes.Ret));
}

private static FieldDefinition DefineResolverFieldDefinition(TypeReference resolverTypeReference, TypeDefinition serializer)
{
FieldDefinition resolverFieldDefinition = new FieldDefinition("resolver", FieldAttributes.Private | FieldAttributes.InitOnly, resolverTypeReference);
serializer.Fields.Add(resolverFieldDefinition);
return resolverFieldDefinition;
}

private static void DefineConstructor(ModuleDefinition mainModuleDefinition, TypeDefinition serializer, TypeReference resolverTypeReference, FieldDefinition resolverFieldDefinition, MethodReference objectCtorImportReference)
{
MethodDefinition constructorMethodDefinition = new MethodDefinition(".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName, mainModuleDefinition.TypeSystem.Void);
ParameterDefinition resolverParam = new ParameterDefinition("resolver", ParameterAttributes.None, resolverTypeReference);
constructorMethodDefinition.Parameters.Add(resolverParam);
ILProcessor processor = constructorMethodDefinition.Body.GetILProcessor();
processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Call, objectCtorImportReference));
processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Ldarg_1));
processor.Append(Instruction.Create(OpCodes.Stfld, resolverFieldDefinition));
processor.Append(Instruction.Create(OpCodes.Ret));
serializer.Methods.Add(constructorMethodDefinition);
}

public static MethodReference MakeHostInstanceGeneric(MethodReference @this, params TypeReference[] genericArguments)
{
GenericInstanceType genericDeclaringType = new GenericInstanceType(@this.DeclaringType);
foreach (TypeReference genericArgument in genericArguments)
{
genericDeclaringType.GenericArguments.Add(genericArgument);
}

MethodReference reference = new MethodReference(@this.Name, @this.ReturnType, genericDeclaringType)
{
HasThis = @this.HasThis,
ExplicitThis = @this.ExplicitThis,
CallingConvention = @this.CallingConvention
};

foreach (ParameterDefinition parameter in @this.Parameters)
{
reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
}

foreach (GenericParameter genericParam in @this.GenericParameters)
{
reference.GenericParameters.Add(new GenericParameter(genericParam.Name, reference));
}

return reference;
}

private static ModuleDefinition GetSystemModule() => GetModule("C:\\Program Files\\dotnet\\sdk\\NuGetFallbackFolder\\netstandard.library\\2.0.3\\build\\netstandard2.0\\ref\\netstandard.dll");
private static ModuleDefinition GetModule(string path) => AssemblyDefinition.ReadAssembly(path).MainModule;
}
}



SerializeProgram

using SerializeBase;

using SerializeTarget;

namespace SerializeProgram
{
class Program
{
static void Main()
{
ISerializerResolver resolver = DefaultResolver.Default;
resolver.Register(new Class1Serializer(resolver));
}
}
}



解説(Generator以外)

このサンプルはソースコードを元にしてJSONシリアライザを生成している。

型TのシリアライザはTSerializerという名前で生成され、ISerializer<T>を実装する。

public unsafe interface ISerializer<T>

{
long TryWrite(T value, byte* destination, long capacity);
}

T型を受け取り、それをbyte列に書き込むだけの単純なinterfaceである。

約束事としてbyte列のcapacityが足りない場合はバッファオーバーランを発生させずに負の数を戻り値とし、書き込み成功時は書き込んだbyte数を戻り値とする。

私の想定する用途ではシリアライザはシリアライザリゾルバに登録される。

Register<T>で登録し、Get<T>で取得する。

#nullable enable

namespace SerializeBase
{
public interface ISerializerResolver
{
bool Register<T>(ISerializer<T> serializer);
ISerializer<T>? Get<T>();
}
}

今回シリアライザを生成する型の名前はClass1。

シリアライザの名前はClass1Serializerである。 本来ならばClass1のネスト型としてSerializerとしたかったのだが、生成後のDLLを使用するとコンパイル時に筆者としてもよくわからないエラーが生じるので諦めた。


Main

static void Main()

{
(ModuleDefinition systemModuleDefinition, ModuleDefinition baseModuleDefinition, TypeDefinition serializeAttributeTypeDefinition, ModuleDefinition mainModuleDefinition) = InitializeModules();
CreateSerializers(mainModuleDefinition, serializeAttributeTypeDefinition, systemModuleDefinition, baseModuleDefinition);
mainModuleDefinition.Write("../../../../SerializeTarget/bin/SerializeTarget.dll");
}

InitializeModulesでモジュールを適切に読みこみ、CreateSerializersでシリアライザを生成してMainモジュールに追加し、ModuleDefinition.WriteでDLLとして出力している。

InitializeModulesの中身についてはソースコード全文を参照してほしい。第2章の内容とほぼ変わらない。


CreateSerializers

private static void CreateSerializers(ModuleDefinition mainModuleDefinition, TypeDefinition serializeAttributeTypeDefinition, ModuleDefinition systemModuleDefinition, ModuleDefinition baseModuleDefinition)

{
TypeDefinition iSerializerDefinition = baseModuleDefinition.GetType("SerializeBase", "ISerializer`1");
TypeReference iSerializerImportReference = mainModuleDefinition.ImportReference(iSerializerDefinition);
TypeDefinition resolverTypeDefinition = baseModuleDefinition.GetType("SerializeBase", "ISerializerResolver");
TypeReference resolverImportReference = mainModuleDefinition.ImportReference(resolverTypeDefinition);
MethodReference objectCtorImportReference = mainModuleDefinition.ImportReference(systemModuleDefinition.GetType("System", "Object").Methods.First(x => x.Name == ".ctor"));
MethodDefinition getMethodDefinition = resolverTypeDefinition.Methods.First(x => x.Name == "Get");

int count = mainModuleDefinition.Types.Count;
for (var i = 0; i < count; i++)
{
TypeDefinition typeDefinition = mainModuleDefinition.Types[i];
if (!typeDefinition.HasCustomAttributes) continue;
if (!typeDefinition.HasFields) continue;
if (typeDefinition.Fields.All(x => !x.IsPublic)) continue;
if (typeDefinition.CustomAttributes.All(x => x.AttributeType.FullName != serializeAttributeTypeDefinition.FullName)) continue;
mainModuleDefinition.Types.Add(CreateSerializer(typeDefinition, mainModuleDefinition, iSerializerDefinition, iSerializerImportReference, resolverImportReference, objectCtorImportReference, getMethodDefinition));
}
}

Mainモジュールの全型を走査し、SerializeBase.SerializeAttribute型の属性を付与されたpublicフィールドを持つ型に対してCreateSerializerを呼び、シリアライザを生成している。

また、最初の段落はループの共通変数をループ外に出している。


CreateSerializer

private static TypeDefinition CreateSerializer(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, TypeReference iSerializerImportReference, TypeReference resolverImportReference, MethodReference objectCtorImportReference, MethodDefinition getMethodDefinition)

{
TypeDefinition serializer = new TypeDefinition(
typeDefinition.Namespace,
typeDefinition.Name + "Serializer",
TypeAttributes.Public | TypeAttributes.BeforeFieldInit | TypeAttributes.Sealed,
mainModuleDefinition.TypeSystem.Object);

FieldDefinition resolverFieldDefinition = DefineResolverFieldDefinition(resolverImportReference, serializer);
DefineConstructor(mainModuleDefinition, serializer, resolverImportReference, resolverFieldDefinition, objectCtorImportReference);
ImplementISerializer(typeDefinition, mainModuleDefinition, iSerializerDefinition, iSerializerImportReference, serializer, resolverFieldDefinition, getMethodDefinition);
return serializer;
}

CreateSerializerではシリアライザ型を生成する。


  • private readonly ISerializerResolverなresolverフィールド

  • public Class1Serializer(ISerializerResolver resolver) => this.resolver = resolver;

  • long ISerializer<Class1>.TryWrite(Class1 value, byte* destination, long capacity);

上記3つをそれぞれ定義・実装している。


DefineConstructor

private static void DefineConstructor(ModuleDefinition mainModuleDefinition, TypeDefinition serializer, TypeReference resolverTypeReference, FieldDefinition resolverFieldDefinition, MethodReference objectCtorImportReference)

{
MethodDefinition constructorMethodDefinition = new MethodDefinition(".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.RTSpecialName | MethodAttributes.SpecialName, mainModuleDefinition.TypeSystem.Void);
ParameterDefinition resolverParam = new ParameterDefinition("resolver", ParameterAttributes.None, resolverTypeReference);
constructorMethodDefinition.Parameters.Add(resolverParam);

ILProcessor processor = constructorMethodDefinition.Body.GetILProcessor();
// base呼び出し
processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Call, objectCtorImportReference));

// this.resolver = resolver;
processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Ldarg_1));
processor.Append(Instruction.Create(OpCodes.Stfld, resolverFieldDefinition));
processor.Append(Instruction.Create(OpCodes.Ret));

serializer.Methods.Add(constructorMethodDefinition);
}

コンストラクタは.ctorという名前である必要がある。そして特別な名前であることを示すためにMethodAttributes.RTSpecialName | MethodAttributes.SpecialNameをMethodAttributesに含ませる必要がある。

コンストラクタでは必ず基底クラスのコンストラクタを呼び出さなくてはならない。

processor.Append(Instruction.Create(OpCodes.Ldarg_0));

processor.Append(Instruction.Create(OpCodes.Call, objectCtorImportReference));

new object()あるいはbase()を意味する。

そして次の段落で第0引数(this)のresolverフィールドに第1引数resolverを代入する。


ImplementISerializer

private static void ImplementISerializer(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, TypeReference iSerializerImportReference, TypeDefinition serializer, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)

{
serializer.Interfaces.Add(new InterfaceImplementation(new GenericInstanceType(iSerializerImportReference)
{
GenericArguments = { typeDefinition }
}));
DefineTryWrite(typeDefinition, mainModuleDefinition, iSerializerDefinition, resolverFieldDefinition, serializer, getMethodDefinition);
}

ある型にインターフェースを新たに実装させるのは2つの工程からなる。


  • そのTypeDefinition.InterfacesプロパティにAdd(InterfaceImplementation)する


    • 今回具象型ISerializer<Class1>を実装するのでISerialize<T>(型引数を1つ取るジェネリクス型)から生成せねばならない。

    • ジェネリクス型から具象型を生成するのは簡単であり、ジェネリクス型を表すTypeReferenceを元にGenericInstanceType型オブジェクトを生成し、GenericArgumentsプロパティに型引数を与えればよい。



  • インターフェースが要求する全てのメソッドを手作業で実装する


    • 今回はTryWrite1つを実装すればよいのでDefineTryWriteに切り出している。




DefineTryWrite

private static void DefineTryWrite(TypeDefinition typeDefinition, ModuleDefinition mainModuleDefinition, TypeDefinition iSerializerDefinition, FieldDefinition resolverFieldDefinition, TypeDefinition serializer, MethodDefinition getMethodDefinition)

{
MethodDefinition tryWriteMethod = new MethodDefinition("TryWrite", MethodAttributes.HideBySig | MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.NewSlot | MethodAttributes.Virtual, mainModuleDefinition.TypeSystem.Int64);
ParameterDefinition valueParam = new ParameterDefinition("value", ParameterAttributes.None, typeDefinition);
ParameterDefinition destinationParam = new ParameterDefinition("destination", ParameterAttributes.None, new PointerType(mainModuleDefinition.TypeSystem.Byte));
ParameterDefinition capacityParam = new ParameterDefinition("capacity", ParameterAttributes.None, mainModuleDefinition.TypeSystem.Int64);
tryWriteMethod.Parameters.Add(valueParam);
tryWriteMethod.Parameters.Add(destinationParam);
tryWriteMethod.Parameters.Add(capacityParam);

FieldDefinition[] fieldArray = typeDefinition.Fields.Where(x => x.IsPublic).ToArray();

ILProcessor processor = tryWriteMethod.Body.GetILProcessor();
switch (fieldArray.Length)
{
case 0:
FillZeroField(processor);
break;
case 1:
FillOneField(mainModuleDefinition, processor, iSerializerDefinition, fieldArray, Encoding.UTF8, resolverFieldDefinition, getMethodDefinition);
break;
default:
FillFields(mainModuleDefinition, processor, iSerializerDefinition, fieldArray, Encoding.UTF8, resolverFieldDefinition, getMethodDefinition);
break;
}
serializer.Methods.Add(tryWriteMethod);
}

interfaceのメソッドを実装する時は必ずMethodAttributes.NewSlot | MethodAttributes.VirtualをMethodsAttributesに加えるべきである。

MethodAttributes.Virtualはそのメソッドをvtableに加える(ポリモーフィズム)のに必須である。

MethodAttributes.NewSlotはC#コンパイラが混乱しないためのものである。

シリアライズするフィールドの個数に応じてメソッドの中身を変更する。

0個ならば出力JSONが"{}"になるだけの単純な中身に、1個なら"{Hoge:0}"のようなそれに、2個ならば"{Hoge:0,Fuga:1}"のように場合分けが行われる。


FillZeroField

private static void FillZeroField(ILProcessor processor)

{
Instruction il0000 = Instruction.Create(OpCodes.Ldc_I8, -2L);
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_2));
processor.Append(Instruction.Create(OpCodes.Blt_S, il0000));
processor.Append(Instruction.Create(OpCodes.Ldarg_2));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)0x7B));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)0x7D));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, 2L));
processor.Append(Instruction.Create(OpCodes.Ret));
processor.Append(il0000);
processor.Append(Instruction.Create(OpCodes.Ret));
}

出力ILをC#にするとおおよそ次の通りである。

public unsafe long TryWrite(Class1 value, byte* destination, long capacity)

{
if (capacity >= 2)
{
*destination = 123;
destination[1] = 125;
return 2L;
}
return -2L;
}

ここで気を付けるべき陥穽としては、OpCodes.Ldc_I4_SとOpCodes.Ldc_I8の第2引数の型である。

それぞれ対応したsbyteとlongの値を第2引数にしないと例外を投げてしまうのだ。

筆者はbyteやint型の値を第2引数にしたりしたのでエラーになって結構な時間を無駄に費やした。


FillOneField

private static void FillOneField(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, FieldDefinition[] fieldArray, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)

{
Collection<VariableDefinition> variableDefinitions = processor.Body.Variables;
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));
variableDefinitions.Add(new VariableDefinition(new PointerType(mainModuleDefinition.TypeSystem.Byte)));

long initialNeedLength = utf8.GetByteCount(fieldArray[0].Name) + 4L;

Instruction[] firstFailInstructions = LoadConstantInt64(-initialNeedLength);
Instruction[] secondFailInstructions = LoadConstantInt64(-initialNeedLength + 1L);

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Blt_S, firstFailInstructions[0]));

processor.Append(Instruction.Create(OpCodes.Ldarg_2));
WriteChar(processor, '{');

WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[0], secondFailInstructions[0], ref initialNeedLength);

processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)'}'));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength - 1L));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.AppendRange(secondFailInstructions);
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.AppendRange(firstFailInstructions);
processor.Append(Instruction.Create(OpCodes.Ret));
}

フィールド1つのみをシリアライズするものである。

出力ILをC#コードで表すと以下のようになる。

public unsafe long TryWrite(Class1 value, byte* destination, long capacity)

{
if (capacity >= 7L)
{
*destination = 123;
byte* intPtr = destination + 1;
*intPtr = 88;
byte* intPtr2 = intPtr + 1;
*intPtr2 = 89;
byte* intPtr3 = intPtr2 + 1;
*intPtr3 = 90;
byte* intPtr4 = intPtr3 + 1;
*intPtr4 = 58;
byte* ptr = intPtr4 + 1;
capacity -= 5L;
long num;
if ((num = resolver.Get<int>().TryWrite(value.XYZ, ptr, capacity)) >= 0 && (capacity -= num) >= 1)
{
ptr[num] = 125;
return num + 0L;
}
return -6 + num;
}
return -7L;
}

ローカル変数が6つあるように見えるだろう。しかし、実際の所は2つのみである。

Collection<VariableDefinition> variableDefinitions = processor.Body.Variables;

variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));
variableDefinitions.Add(new VariableDefinition(new PointerType(mainModuleDefinition.TypeSystem.Byte)));

各ローカル変数はlong num;とbyte* ptr;を意味している。

byte* intPtr0~4はOpcodes.DupをC#として解釈するために生じた見掛け上のローカル変数である。

long initialNeedLength = utf8.GetByteCount(fieldArray[0].Name) + 4L; はフィールドの名前のバイト長さ+1byte(シリアライズしたら最低限1byte消費するため)+3byte("{:}")という意味である。

firstFailInstructionsは第3引数capacityがinitialLength未満で書き込みできない時に飛ぶ先である。

secondFailInstructionsはフィールドのシリアライズに失敗した時に飛ぶ先である。

private static Instruction[] LoadConstantInt64(long value)

{
Instruction[] answer;
switch (value)
{
case -1:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_M1),
Instruction.Create(OpCodes.Conv_I8)
};
break;
case 0:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_0),
Instruction.Create(OpCodes.Conv_I8)
};
break;
case 1:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_1),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 2:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_2),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 3:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_3),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 4:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_4),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 5:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_5),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 6:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_6),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 7:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_7),
Instruction.Create(OpCodes.Conv_I8),
};
break;
case 8:
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_8),
Instruction.Create(OpCodes.Conv_I8),
};
break;
default:
if (value <= sbyte.MaxValue && value >= sbyte.MinValue)
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)value),
Instruction.Create(OpCodes.Conv_I8),
};
}
else if (value <= int.MaxValue && value >= int.MinValue)
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I4, (int)value),
Instruction.Create(OpCodes.Conv_I8),
};
}
else
{
answer = new[]
{
Instruction.Create(OpCodes.Ldc_I8, value),
};
}
break;
}
return answer;
}

LoadConstantInt64は俗に言うユーティリティ関数である。Int64型の定数をスタック上に最適な命令で置くことができる。

最適な命令が複数個の場合もあるので戻り値はInstruction[]としている。

processor.Append(Instruction.Create(OpCodes.Ldarg_2));

WriteChar(processor, '{');
WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[0], secondFailInstructions[0], ref initialNeedLength);

第2引数byte* destinationを読み込んで1文字目である"{"を書き込み、そしてフィールド名とフィールドのシリアライズ結果を書き込んでいる。

private static void WriteChar(ILProcessor processor, char character)

{
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)character));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
}

WriteCharはASCII文字を対象のアドレスに書き込んでポインタを1つ進めるという働きを持っている。

private static void WriteField(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition, FieldDefinition fieldDefinition, Instruction jumpDestinationWhenFail, ref long initialNeedLength)

{
byte[] nameBytes = utf8.GetBytes(fieldDefinition.Name);

WriteName(processor, nameBytes);
WriteChar(processor, ':');
processor.Append(Instruction.Create(OpCodes.Stloc_1));

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.AppendRange(LoadConstantInt64(2L + nameBytes.LongLength));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));

processor.Append(Instruction.Create(OpCodes.Ldarg_0));
processor.Append(Instruction.Create(OpCodes.Ldfld, resolverFieldDefinition));
MethodReference getImportReference = mainModuleDefinition.ImportReference(new GenericInstanceMethod(getMethodDefinition)
{
GenericArguments = { fieldDefinition.FieldType }
});
processor.Append(Instruction.Create(OpCodes.Callvirt, getImportReference));

processor.Append(Instruction.Create(OpCodes.Ldarg_1));
processor.Append(Instruction.Create(OpCodes.Ldfld, fieldDefinition));
processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
MethodReference tryWriteImportReference = mainModuleDefinition.ImportReference(MakeHostInstanceGeneric(iSerializerDefinition.Methods[0], fieldDefinition.FieldType));
processor.Append(Instruction.Create(OpCodes.Callvirt, tryWriteImportReference));

processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Stloc_0));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_0));
processor.Append(Instruction.Create(OpCodes.Conv_I8));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));

processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));
initialNeedLength -= 3L + nameBytes.LongLength;
processor.AppendRange(LoadConstantInt64(initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));
}

private static void WriteName(ILProcessor processor, byte[] nameBytes)
{
for (var index = 0; index < nameBytes.Length; index++)
{
WriteByte(processor, nameBytes[index]);
}
}

private static void WriteByte(ILProcessor processor, byte character)
{
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)character));
processor.Append(Instruction.Create(OpCodes.Stind_I1));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_1));
processor.Append(Instruction.Create(OpCodes.Add));
}

Encoding.GetBytesでフィールド名をbyte配列に変換し、WriteNameで書き込み、そしてセパレーターである":"を1字書き込む。

processor.Append(Instruction.Create(OpCodes.Ldarg_3));

processor.AppendRange(LoadConstantInt64(2L + nameBytes.LongLength));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));

第3引数capacityから書き込んだbyte数分減算をし、再代入する。

引数への代入操作はIL命令として短縮形があまり用意されていないので避けた方がよい。引数のロード操作はLdarg_0~3と1byte長命令があるのに対してストア操作は最短でも2byteなのである。

processor.Append(Instruction.Create(OpCodes.Ldarg_0));

processor.Append(Instruction.Create(OpCodes.Ldfld, resolverFieldDefinition));
MethodReference getImportReference = mainModuleDefinition.ImportReference(new GenericInstanceMethod(getMethodDefinition)
{
GenericArguments = { fieldDefinition.FieldType }
});
processor.Append(Instruction.Create(OpCodes.Callvirt, getImportReference));

第0引数(つまりthis)のフィールドISerializerResolver resolverをロードし、Get<フィールドの型>を呼び出している。

ISerializerResolver.Get<T>はジェネリックメソッドである。これの具象を呼び出すにはジェネリック型に対するGenericInstanceTypeよろしくGenericInstanceMethodオブジェクトを作成し、GenericArgumentsプロパティに型引数を埋めるのである。

Get<フィールドの型>によりISerializer<フィールドの型>を得た。

フィールドをシリアライズするならばISerializer<フィールドの型>のTryWriteを呼び出さねばならない。

processor.Append(Instruction.Create(OpCodes.Ldarg_1));

processor.Append(Instruction.Create(OpCodes.Ldfld, fieldDefinition));
processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));

追加で3つ引数を充足する。

MethodReference tryWriteImportReference = mainModuleDefinition.ImportReference(MakeHostInstanceGeneric(iSerializerDefinition.Methods[0], fieldDefinition.FieldType));

processor.Append(Instruction.Create(OpCodes.Callvirt, tryWriteImportReference));

ジェネリック型ISerializer<T>を具象型ISerializer<フィールドの型>にして、TryWriteメソッドのMethodReferenceを得、呼び出しを行っている。

Mono.Cecilの不思議な点としては、ジェネリック型を具象型にするのも、ジェネリックメソッドを具象化するのも簡単であるのに、具象型からメソッドを得るのが非常に難しいということである。

TypeReferenceとGenericInstanceTypeにはMethodsプロパティがないのである。

迂回策として次のメソッドを定義して利用した。

public static MethodReference MakeHostInstanceGeneric(MethodReference @this, params TypeReference[] genericArguments)

{
GenericInstanceType genericDeclaringType = new GenericInstanceType(@this.DeclaringType);
foreach (TypeReference genericArgument in genericArguments)
{
genericDeclaringType.GenericArguments.Add(genericArgument);
}

MethodReference reference = new MethodReference(@this.Name, @this.ReturnType, genericDeclaringType)
{
HasThis = @this.HasThis,
ExplicitThis = @this.ExplicitThis,
CallingConvention = @this.CallingConvention
};

foreach (ParameterDefinition parameter in @this.Parameters)
{
reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType));
}

foreach (GenericParameter genericParam in @this.GenericParameters)
{
reference.GenericParameters.Add(new GenericParameter(genericParam.Name, reference));
}

return reference;
}

かーなりダーティである。

processor.Append(Instruction.Create(OpCodes.Dup));

processor.Append(Instruction.Create(OpCodes.Stloc_0));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_0));
processor.Append(Instruction.Create(OpCodes.Conv_I8));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));

TryWriteの戻り値はlong型であり、それが負の数ならば書き込み失敗であるからjumpDestinationWhenFailの示す命令にジャンプする。

processor.Append(Instruction.Create(OpCodes.Ldarg_3));

processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Starg_S, processor.Body.Method.Parameters[2]));
initialNeedLength -= 3L + nameBytes.LongLength;
processor.AppendRange(LoadConstantInt64(initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Blt, jumpDestinationWhenFail));

そして書き込んだbyte数だけ第3引数capacityを減らし、残りの最低限書き込まねばならないbyte数(initialNeedLength)と比較し、書き込めなければ失敗へとジャンプする。

FillOneFieldに戻るが、後は"}"を書き込んで、失敗時の処理をしているだけであり、容易に読み解けるだろう。


FillFields

private static void FillFields(ModuleDefinition mainModuleDefinition, ILProcessor processor, TypeDefinition iSerializerDefinition, FieldDefinition[] fieldArray, Encoding utf8, FieldDefinition resolverFieldDefinition, MethodDefinition getMethodDefinition)

{
Collection<VariableDefinition> variableDefinitions = processor.Body.Variables;
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));
variableDefinitions.Add(new VariableDefinition(new PointerType(mainModuleDefinition.TypeSystem.Byte)));
variableDefinitions.Add(new VariableDefinition(mainModuleDefinition.TypeSystem.Int64));

// ,:0
long initialNeedLength = fieldArray.Aggregate(0, (accumulation, definition) => accumulation + utf8.GetByteCount(definition.Name)) + 1L + fieldArray.Length * 3L;

Instruction il0000 = Instruction.Create(OpCodes.Ldloc_0);

processor.Append(Instruction.Create(OpCodes.Ldc_I8, initialNeedLength));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Neg));
processor.Append(Instruction.Create(OpCodes.Stloc_0));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Dup));
processor.Append(Instruction.Create(OpCodes.Stloc_2));
processor.Append(Instruction.Create(OpCodes.Bgt, il0000));

processor.Append(Instruction.Create(OpCodes.Ldarg_2));
WriteChar(processor, '{');

WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[0], il0000, ref initialNeedLength);

for (var i = 1; i < fieldArray.Length; i++)
{
processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
WriteChar(processor, ',');
WriteField(mainModuleDefinition, processor, iSerializerDefinition, utf8, resolverFieldDefinition, getMethodDefinition, fieldArray[i], il0000, ref initialNeedLength);
}

processor.Append(Instruction.Create(OpCodes.Ldloc_1));
processor.Append(Instruction.Create(OpCodes.Ldloc_0));
processor.Append(Instruction.Create(OpCodes.Conv_I));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ldc_I4_S, (sbyte)'}'));
processor.Append(Instruction.Create(OpCodes.Stind_I1));

processor.Append(Instruction.Create(OpCodes.Ldloc_2));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Ret));

processor.Append(il0000);
processor.Append(Instruction.Create(OpCodes.Ldloc_2));
processor.Append(Instruction.Create(OpCodes.Sub));
processor.Append(Instruction.Create(OpCodes.Ldarg_3));
processor.Append(Instruction.Create(OpCodes.Add));
processor.Append(Instruction.Create(OpCodes.Ret));
}

複数個のフィールドをシリアライズする場合にFillFieldsを用いる。

およそFillOneFieldと同等のことをした後2番目以後のフィールドのシリアライズを繰り返している。


最適化に関する余談

今回のサンプルではフィールドの型に対して愚直にISerializer<T>をISerializerResolverから取得してTryWriteしている。

だが、intなど数値型, string, boolについてはフィールドの型を見て特殊化された処理を埋め込むことでより性能の向上が見込まれる。

また、フィールドの型が同一アセンブリ内に存在するなどして、シリアライザを得られる場合も処理をインラインで埋め込むことで性能を向上できる。

更に、今回はフィールド名を1byteずつ代入しているが、4または8byteにまとめて書き込んだ方がよい。

以上を以て2つ目のサンプルであるJSONシリアライザ生成についての解説とする。


後書き

Mono.Cecilに関する日本語の情報は大体2~3年おきに散発的に記述されるに留まる。

この記事が数年後の誰かの役に立つことを願う。


参考文献