2020年12月6日日曜日

列挙型の濃ゆい話

この記事はC#アドベントカレンダー2020その2の6日目のはずです。

その昔(*1)、Rico Marian(*2)は言いました。「CLRがfloatまたはdouble型を基になる型として使用する列挙体をサポートしていることをご存知でしたか?」と。

ご存知のように、C#では列挙体の基になる型としてはプリミティブの整数値しか認めていません。具体的には、bytesbyteshortushortintuintlongulongです。 Ricoが言うように、C#ではfloatdoubleをサポートしていません。 さらに、Ricoは「これはECMA標準です」と言っています。 11年前の私はわざわざ検証しませんでしたが、このあたりを深堀していこうと思います。

floatやdoubleを基になる型にする列挙型を定義してみる

念のため、以下のようなC#コードを書いてコンパイルしてみます。

public enum EFloat : float { None = 0, Value = 1.0f }

結果は

error CS1008: byte、sbyte、short、ushort、int、uint、long または ulong のいずれかの型を使用してください

そうですよね。まぁ無理です。

しかし、諦めたらそこで試合終了です。C#コンパイラーでそのような列挙型をコンパイルできないからと言って、C#でそのような列挙型を定義できないことにはなりません。System.Reflection.Emit名前空間にあるリフレクション出力APIを使用して列挙型を定義すればよいのです。C#のコードなのでアドベントカレンダーとしても問題ないはずです。

列挙型というのは、ようは基底クラスがSystem.Enumで、列挙値をpublicな定数として持ち、specialnamertspecialname属性がついて、フィールドの型が基になる型になっているインスタンスフィールドvalue__を持つクラスです。さくっと定義しましょう。なお、面倒なことを少しだけ楽にしてくれるEnumBuilderというクラスがあるのでそれを使います。

var type = typeof(float); var none = 0.0f; var value = 1.0f; var ab = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("Enums"), AssemblyBuilderAccess.Run); var mb = ab.DefineDynamicModule("Enums.dll"); var eb = mb.DefineEnum($"E{type.Name}", TypeAttributes.Public, type); eb.DefineLiteral("None", none); eb.DefineLiteral("Value", value; var t = eb.CreateTypeInfo(); Console.WriteLine($"Success! ({t} : {Enum.GetUnderlyingType(t)})");

こんな感じのコードを書いて実行すると、

Success! (ESingle : System.Single)

と表示されます(floatSystem.Singleを表すC#のキーワードにすぎないので、ESingleという名前になっています)。 なるほど、確かにfloatdoubleを基になる型にした列挙型は存在できるようです。

ECMA-335を見てみる

さて、Ricoは「これはECMA標準です」と言っていました。一応、ECMA-335 Common Language Infrastructureの最新版5th Editionを見てみましょう。以下、該当部分の抜粋です(太字は筆者)。

II.14.3 Enums An enum (short for enumeration) defines a set of symbols that all have the same type. A type shall be an enum if and only if it has an immediate base type of System.Enum. Since System.Enum itself has an immediate base type of System.ValueType, (see Partition IV) enums are value types (§II.13) The symbols of an enum are represented by an underlying integer type: one of { bool, char, int8, unsigned int8, int16, unsigned int16, int32, unsigned int32, int64, unsigned int64, native int, unsigned native int }

……おや。ここにはfloat32float64(ILではfloatdoubleをそのように表現します)がありませんね。どういうことでしょうか。

アーカイブサイトで確認すると、2nd editionまでは以下のようになっていました。

...The symbols of an enum are represented by an underlying type: one of { bool, char, int8, unsigned int8, int16, unsigned int16, int32, unsigned int32, int64, unsigned int64, float32, float64, native int, unsigned native int }

なるほど。3rd editionからECMA標準ではなくなったようですね(*3)。つまり、floatdoubleを基になる型にした列挙型はECMA標準ではありません。CLRやCoreCLRが(確かめていませんがMonoも)これらを認めているのは後方互換性を保つためでしょう(*4)。

整数でないプリミティブ型を基になる型とする列挙型

さて、上記のECMA 335を見ると、以下の型も基になる型として認められていることが分かります。

  • boolSystem.Boolean
  • charSystem.Char
  • native intSystem.IntPtr
  • unsigned native intSystem.UIntPtr

これらについても試してみましょう。

先ほどのコンパイルエラーメッセージから、C#で書けないことはもう分かっているので、いつものようにリフレクション出力APIで作ってみましょう。

先ほどのコードのtypenonevalueを適当にいじって実行してみます。そうすると、boolcharについては動作しますが、IntPtrUIntPtrについては動作しません。

System.ArgumentException: System.IntPtr is not a supported constant type. at System.Reflection.Emit.TypeBuilder.SetConstantValue(ModuleBuilder module, Int32 tk, Type destType, Object value) at System.Reflection.Emit.FieldBuilder.SetConstant(Object defaultValue) at System.Reflection.Emit.EnumBuilder.DefineLiteral(String literalName, Object literalValue)

なるほど、DefineLiteralの引数としてIntPtrは使えないようです。なので、その行をコメントアウトすれば

Success! (EIntPtr : System.IntPtr)

と表示されます。

ちなみに、この定義した列挙値の動作ですが、基になる型がIntPtrとかUIntPtrだと、クラスライブラリ側が完全に対応しておらず、かなり怪しい動きをします。 たとえば、定義済みの列挙値についてToString()すると列挙値の名前(NoneとかValue)が返ってくるはずですが、必ず基になる値(01)で返ってきたりします。

このあたりの動作は、ILASMを使用して列挙型を定義すればできます。C#アドベントカレンダーですが、コンパイル後のコードに触れてはいけないとは書いてないので、興味のある方は試してみてください。

// .NET Frameworkの場合 //#define CORE_ASSEMBLY "mscorlib" //#define CORE_VERSION "2:0:0:0" // .NET Coreの場合 #define CORE_ASSEMBLY "System.Runtime" #define CORE_VERSION "4:2:2:0" #define THIS_ASSEMBLY "NativeIntEnums" #define THIS_MODULE "NativeIntEnums.dll" .assembly extern CORE_ASSEMBLY { .publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) .ver CORE_VERSION } .assembly THIS_ASSEMBLY { .custom instance void [CORE_ASSEMBLY]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) .custom instance void [CORE_ASSEMBLY]System.Runtime.CompilerServices.RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70 4E 6F 6E 45 78 63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 ) .custom instance void [CORE_ASSEMBLY]System.Diagnostics.DebuggableAttribute::.ctor(valuetype [CORE_ASSEMBLY]System.Diagnostics.DebuggableAttribute/DebuggingModes) = ( 01 00 02 00 00 00 00 00 ) .hash algorithm 0x00008004 .ver 1:0:0:0 } .module THIS_MODULE .imagebase 0x00400000 .file alignment 0x00000200 .stackreserve 0x00100000 .subsystem 0x0003 // WINDOWS_CUI .corflags 0x00000001 // ILONLY .class sealed public auto ansi NativeIntEnum extends [mscorlib]System.Enum { .field public specialname rtspecialname native int value__ .field public static literal valuetype NativeIntEnum None = int32(0) .field public static literal valuetype NativeIntEnum Value = int32(1) }

以上、『Framework Design Guidelines 3rd Edition』翻訳の現場からお伝えしました。

*1 『.NETのクラスライブラリ設計』のp90

*2 .NET開発チームの人です。

*3 『.NETのクラスライブラリ設計』が訳されたのが2009年であり、ECMA-335の第3版が2005年であったという事実に気づいてはいけません。次の版では訳注を入れようと思います。

*4 IL2CPPや今は亡きCoreRTが気になりますね。

2017年3月20日月曜日

.NET Coreが動くまで

(なんかこのネタ、誰かがやってそうな気がしつつ)

一部ではバージョンがわかりづらいなどとも言われている .NET Core の起動の仕組みについて、適宜ソースコードを眺めつつ、整理したいと思います。私は中の人ではなく、コードを解析しながら書いているので、おかしな点がありましたら twitter 等で教えていただけると嬉しいです。

構造

ご存知のように、.NET Core は、コンパイル結果の実行可能アプリケーションまたは dotnet コマンドから実行します。
その時の動作の流れを図にすると、以下のようになっています。

この図において、長方形はファイル、角丸四角形はレイヤー、青の長方形は説明のためのグルーピングです。
この図の要素を簡単に説明すると、以下のようになります。

  • dotnet コマンド。別名 .NET CLI と言われているのはこれ(のはず)です。muxer と呼ばれることもあります。実体としては、以下の 2 つになります。
    • dotnet。Windows の場合は dotnet.exe になります。これは後述するアプリケーションランチャーです。
    • dotnet.dll。.NET CLI の本体です。.NET CLI のリポジトリにあるソースコードをビルドすると生成されます。
  • app。特定のランタイム向けに dotnet publish すると生成される実行可能ファイルです。
  • corehost。Windows の場合は corehost.exe となります。コマンドラインプログラムとしての .NET Core のランチャーです。
  • hostfxr。Windows の場合は hostfxr.dll、Mac OS X の場合は libhostfxr.dylib、Linux の場合は libhostfxr.socorehost の実体になります。
  • hostpolicy。Windows の場合は hostpolicy.dll、Mac OS X の場合は libhostpolicy.dylib、Linux の場合は libhostpolicy.so。CoreCLR ホスティング API に渡すための各種情報を初期化します。まさに、「ホスティングポリシー」です。
  • Hosting API。CLR の Hosting API の CoreCLR 版。COM に準拠した CLR の Hosting API に対して、CoreCLR の Hosting API は UNIX の関数に近い形になっています。ICLRRuntimeHost2 のラッパーとしての C 言語の関数です。
  • ICLRRuntimeHost2。CoreCLR の Hosting API の実体です。ちなみに、CLR の Hosting API は ICLRRuntimeHost インターフェイスです。

これらは、ホスティング API(ICLRRuntimeHost2 とラッパーである Hosting API)と、CoreHost(corehosthostfxrhostpolicy に加え、この後説明する dotnet も含まれます)に大別されます。

ホスティング API

ホスティング API は、その名の通り CoreCLR(.NET Core のランタイム)をプロセスにロードして、実行するための API です。.NET Core のプログラムを実行する場合、このホスティング API を何らかの形で実行する必要があります。

.NET Core の場合、以下の 4 つの関数が定義されています。ここで、文字列は、Windows の場合は ANSI、それ以外の場合は UTF-8 で渡します。

// CoreCLR を初期化。戻り値は HRESULT。
int coreclr_initialize(
    const char* exePath,                    // 実行する exe のパス。
    const char* appDomainFriendlyName,      // 初期 AP ドメインの名前。
    int propertyCount,                      // CoreCLR プロパティの数。つまり、propertyKeys と propertyValues の要素数。
    const char** propertyKeys,              // CoreCLR プロパティキー文字列の配列。
    const char** propertyValues,            // CoreCLR プロパティキー値の配列。
    void** hostHandle,                      // ホストへのハンドルが格納される(実体は ICorHost2)
    unsigned int* domainId                  // 初期 AP ドメインの ID が格納される。
);

// CoreCLR のシャットダウン。戻り値は HRESULT。
int coreclr_shutdown(
    void* hostHandle,                       // coreclr_initialize で返されたホストハンドル。
    unsigned int domainId                   // アンロードする AP ドメインの ID。
);

// 任意の静的メソッドへのデリゲートを作成し、それを呼び出すための関数ポインターを取得する。
int coreclr_create_delegate(
    void* hostHandle,                       // coreclr_initialize で返されたホストハンドル。
    unsigned int domainId,                  // 対象のメソッド(を定義する型)がロードされている AP ドメインの ID。
    const char* entryPointAssemblyName,     // 対象のアセンブリの名前。
    const char* entryPointTypeName,         // 対象の型の名前。
    const char* entryPointMethodName,       // 対象のメソッドの名前。
    void** delegate                         // 関数ポインターが格納される。
);

// アセンブリのメインエントリポイント(Main)を実行。戻り値は HRESULT。
int coreclr_execute_assembly(
    void* hostHandle,                       // coreclr_initialize で返されたホストハンドル。
    unsigned int domainId,                  // 対象のメソッド(を定義する型)がロードされている AP ドメインの ID。デフォルトドメインでなければなりません。
    int argc,                               // argv の要素数
    const char** argv,                      // Main メソッドに渡す引数の配列。
    const char* managedAssemblyPath,        // Main を定義するアセンブリへのファイルパス。
    unsigned int* exitCode                  // Main の戻り値。`void` だった場合は `0`。
);

ホスティング API の動き

ホスティング API の実装は、 C 関数版である unixinterface.cpp と、
そこから呼び出す ICLRRuntimeHost2 COM インターフェイスの実装である CorHost2 クラスです

使い方については、ホスティング API のドキュメント を参照しつつ、以下のソースコードを参照するといいでしょう。

  • corerun。CoreCLR に付属する、テスト用の簡易コマンドラインランチャーです。これを使うことで、.NET CLI の開発が追い付いていない状態でも coreclr の開発版を実行できます。
  • Azure IoT Gateway SDK .NET Core Binding。IoT のシステムで、よりセンサーに近いモノ側に配置するプログラム用(いわゆるエッジコンピューティング用)のフレームワークです。.NET Core 以外からのホスティング API の使用例として参考になるでしょう。設計ドキュメントもあるので、こちらの方がわかりやすいかもしれません。
    • 余談ですが、Node.js や Java や CLR をロードするサンプルプログラムとしても面白いかと。

ホスティング API の動きを簡単に説明すると、以下のようになります。

coreclr_initialize

  1. PAL(Platform Abstraction Layer)を初期化します。
  2. ICLRRuntimeHost2::SetStartupFlags() で初期化フラグを設定します(具体的には、*.runtimeconfig.json にある System.GC.Concurrent System.GC.ServerSystem.GC.RetainVM
  3. ICLRRuntimeHost2::Start() でランタイムを起動します。
    • 内部的には、ランタイムの起動カウントをインクリメントし、 InitializeEE() を呼び出します。
    • なお、シャットダウン済みの場合は単に失敗します。
  4. ICLRRuntimeHost2::CreateAppDomainWithManager() を使用して、以下のように AP ドメインを初期化します。
    • APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS を指定して、Any CPU でないアセンブリの実行を許可します。
    • APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP を指定して、P/Invoke と COM Interop を許可します。
    • APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS を指定して、コードアクセスセキュリティを無効化します。
    • AP ドメインマネージャーは指定しません。

coreclr_shutdown

  1. ICLRRuntimeHost2::UnloadAppDomain() で AP ドメインをアンロードします。
  2. ICLRRuntimeHost2::Stop() でランタイムを停止します。
    • 内部的には、ランタイムの起動カウントをデクリメントし、0 になったらシャットダウン済みフラグを設定します。
  3. PAL をシャットダウンします。

coreclr_create_delegate

  1. ICLRRuntimeHost2::CreateDelegate() を呼び出します。
    • 呼び出すメソッドには以下の制限があります。
    • 静的でなければなりません。
    • ジェネリックメソッドであってはなりません。
    • [AllowReversePInvokeCallsAttribute] で修飾されていなければなりません。
    • メソッドはオーバーロードされていてはいけません。

coreclr_execute_assembly

  1. ICLRRuntimeHost2::ExecuteAssembly() を呼び出します。
    • Assembly::ExecuteMainMethod() を呼び出します。
    • メタデータからエントリポイントを探し、現在のスレッドをフォアグラウンドスレッドにし、エントリポイントを実行します。

CoreHost

さて、ホスティング API は API なので、シェルから直接実行するには、何らかの CLI プログラムが必要になります。それがここで紹介する CoreHost です(正式名称ではない気がしますが、そこは目を瞑ってください)。なお、「dotnet は?」とか「publish すると実行可能ファイルが作れるじゃん」とかいう疑問はちょっとわきに置いておいてください。この後ちゃんと説明します。

CoreHost はホスティング API を呼び出す「コマンドラインランチャー」であり、「マルチプレクサー(muxer)」です……意味が分かりませんね。基本的に、このコマンドは corehost <アプリケーション.dll> <引数> という形式でアプリケーションを実行するためのものです(まさに「ランチャー」です)。

そして、以下のように、corehost の実際の動作は、状況に応じて変わります。

  • <自ファイル名>.dll ファイルがあり、かつ以下の条件を満たす場合、「スタンドアローンモード」になります。
    • <自ファイル名>.deps.json が存在する
    • <自ファイル名>.deps.json<自ファイル名>.runtimeconfig.json も存在しない
  • そうではなく、同じディレクトリに coreclr.dll(または libcoreclr.solibcoreclr.dylib)がある場合、「分離 FX モード」になります。
  • そうでない場合、Muxer モードになります。

この後、各モードの詳細について見ていきますが、もう一つ説明しておくことがあります。それは、corehost はそのままではなく リネームして使用 されるということです。
具体的には、以下のようにリネームされて使用されます。

  • <アプリケーション>[.exe]dotnet publish した結果生成される実行可能アプリケーション。
  • dotnet[.exe]。SDK のルートディレクトリに配置される、.NET CLI と呼ばれるものです。

つまり、これまで実行可能アプリケーションや dotnet コマンドだと思っていたものの実体は、単一の corehost だったということです。

それでは、それぞれのモードについて、その使われ方とともに詳細に見ていきましょう。

各モードについて

CoreHost は、前述のように、自分自身のファイル名がどうなっているかと、同じディレクトリにどのようなファイルがあるかによって 3 つのモードがあり、それぞれ動作が変わります。

分離 FX モード

これがおそらく最も基本的なモードです。コード上は split_fx というシンボルになっています。

以下のように、引数で指定された .NET Core のプログラム(マネージドアセンブリ)を実行するモードです。
dotnet <アプリケーション>.dll から実行するときのモードでもあります。

  • 第1引数で指定されたマネージドアセンブリを実行します。
  • 引数として、--depsfile--runtimeconfig をサポートします。

つまり、dotnet app.dll を実行すると、以下のように動作します。

  1. dotnet.exe という名前の corehost.exe が、分離 FX モードで app.dll を実行します。
  2. dotnet.exe はホスティング API を使用して、app.dll に定義されたエントリポイントを呼び出します。

スタンドアローンモード

これは最も基本的ではないモードなのですが、3 番目のモードが複雑なので先に説明します。

これはあたかも自分自身が .NET Core アプリケーションのように動作するモードで、コンパイルした <アプリケーション>.exe を実行するときのモードでもあります。

  • <自ファイル名>.dll という名前のマネージドアセンブリを実行します。
  • 引数として、--depsfile--runtimeconfig をサポートします。

つまり、dotnet publish で生成される app.exe を実行すると、以下のように動作します。

  1. app.exe という名前の corehost.exe が、スタンドアローンモードで app.dll を実行します。
  2. app.exe はホスティング API を使用して、app.dll に定義されたエントリポイントを呼び出します。

Muxer(マルチプレクサー)モード

これは、2 番目に基本的で、おそらく最も目にすることが多いモード、.NET CLI 用のモードです。

  • 第 1 引数が exec というコマンドが実行された場合、第 2 引数のマネージドアセンブリを実行します。つまり、分離 FX モードとして動作します(exec モード)。
  • 引数として、--fx-version--roll-forward-on-no-candidate-fx をサポートします。
  • そうでない場合、(--fx-version で指定された)coreclr 本体と同じディレクトリにある dotnet.dll を実行します
  • これは、dotnet コマンドで CLI を使用するときのモードです。
    • スタンドアローンモードで dotnet を実行する場合、CoreCLR 本体と dotnet.dlldotnet[.exe] 同じディレクトリに存在する必要があり、バージョン切り替えができなくなるのでこのようになっているのでしょう。

つまり、dotnet run app a b c を実行すると、以下のように動作します(説明を簡単にするため、アプリケーションはビルド済みとします)。

  1. dotnet.exe が Muxer モードで動作し、run に拡張子がないため、dotnet.dll を呼び出します。
  2. dotnet.dll は組み込みコマンド run を認識し、RunCommand オブジェクトを初期化します。
  3. RunCommand はプロジェクトファイルを msbuild の API でロードしします。
  4. msbuild は、RunProgram プロパティとして dotnet を、RunArgument プロパティとして exec path/to/bin/app.dll a b c を設定します。
  5. RunCommand RunProgram プロパティと RunArgument プロパティからコマンドラインを生成します。
    • つまり、dotnetexec path/to/bin/app.dll a b c を生成します。
  6. 外部プロセスとして、dotnet exec path/to/bin/app.dll a b c を実行します。
  7. dotnetcorehost)が exec モードの Muxer モードとして、ホスティング API を使用して、path/to/bin/app.dll に定義されたエントリポイントを呼び出します。

補足:CoreHost とアセンブリプロービング

.NET Core におけるアセンブリのプロービングは CLR のものとは異なります。
具体的には、ホスティング API で指定するプロパティによってその動作が変わります。
通常、ホスティング API を呼び出すのは、dotnetcorehost)になるため、プロービングの動作は、corehost の実装に依存することになります。

ざっくりいうと deps.json にない値はカレントディレクトリからロードしません。
具体的に言うと、Assembly.Load などで動的にロードする分には問題ないのですが、そのロードしたアセンブリが参照するアセンブリはロードできません。
詳細についてはこちらを参照してください

プロービングの詳細については、今回は説明しません(また機会があれば……)。
ただ、プロービングの動作がホストプログラムに依存すること、dotnetcorehost)の仕様は coreclr ではなく cli のドキュメントとして存在すること、
そして具体的な動作を調査するための corehost のコードは(なぜか)インストーラー(core-setup)のリポジトリにあることを覚えておいてください。

ランチャーの動作から言えること

これまでの説明をもとに、いくつかの動作が説明できます。

.NET Core 実行可能アプリケーションとは

スタンドアローンモードで説明したように、<アプリケーション名>.exe(Windows 以外では <アプリケーション名>)という名前にリネームされ、スタンドアローンモードで実行される corehost です。
(リネーム処理は .NET Core SDK の Microsoft.NET.Publish.targets ファイルに書かれています)

.NET CLI とは

Muxer モードで動作する corehost.exedotnet.dll に実装されたコマンドフレームワーク(と組み込みコマンド)です。

実行可能ファイルが RID(ランタイム ID)を指定した publish でしか生成されない理由

corehost は配布先のプラットフォーム依存だからですね。

dotnet と dotnet help の違い

しばやんさんのブログでの .NET Core のバージョンが難しいの中で出た、CLI のバージョン番号がよくわからないという話も、これまでの説明にちょっと引数パースの仕様を加えることで、説明できます。

  • dotnet を実行した場合、corehost の必須引数が足りないため、corehost のヘルプが表示されます。そのため、CoreCLR(のランチャーである hostfxr)のバージョンが表示されます。
  • dotnet help を実行した場合、corehost dotnet.dll help 扱いになり、CLI の HelpCommand が実行されます。そのため、.NET CLI のバージョンが表示されます。

まとめ

  • .NET Core プログラムを実行するということは、Hosting API を実行するということ。
  • 通常、Hosting API は corehost[.exe] から実行する。
  • dotnet[.exe] コマンドはリネームされた corehost[.exe] である。
  • 実行可能アプリケーションもリネームされた corehost[.exe] である。
  • corehost はその配置場所に応じて動作を変える。これによって、.NET CLI としての動作や、汎用ランチャーとしての動作、実行可能アプリケーションとしての動作を使い分けている。

参考文献

2016年12月2日金曜日

.NET Standard のおさらい

この記事は、.NET Core Advent Calendar 2016の2日目です。

あまり目新しい内容ではありませんが、.NET Standard と言われるものについて、いまいち情報が整理されていないように思うので、ここで整理してみようと思います。
結構長くなっていますので、時間のない方は まとめ だけどうぞ。

3 つの .NET Standard

個人的な分類 ですが、.NET Standard と言われているものは 3 つあると思っています。

  • 仕様としての .NET Standard
  • TPM としての .NET Standard
    • TPM(Target Platform Moniker)は、NuGet での対象プラットフォームを表す識別子です。
  • NetStandard.Library パッケージ

話をするとき、何かの記事を読むときには、今相手がどの「.NET Standard」のことを言っているのか意識するといいでしょう(xx設計とかxxテストとか、標準化されているようでその実文脈依存な用語には慣れっこですよね)

仕様としての .NET Standard

仕様としての .NET Standard は、CLI の実装系が持つ べき API のセットを定義します。あくまで べき(SHOULD) なので、実装系はその API を実装しない、または実装できないかもしれません。たとえば、.NET Standard の中には実行時 IL 生成の機能がありますが、これは AOT 環境では動作しません。実際、OS で動的コード生成が禁止されている iOS 上で動作する Xamarin.iOS はもちろん、.NET Native でも動作しません(PlatformNotSupportedException がスローされるはずです)。

この仕様としての .NET Standard は、「.NET 互換を名乗るならこのくらいの API を用意しておいてね」くらいの意味合いです。それぞれのバージョンで定義すべき API のセットはgithubで公開されています

より高いバージョンの .NET Standard をサポートするプラットフォームほど高機能と言えるでしょう。

TPM としての .NET Standard

TPM としての .NET Standard は、その NuGet パッケージがどのプラットフォームをサポートできるのかを定義します。
言ってみれば PCL(のプロファイル)の後継です。TPM で .NET Standard を宣言しているということは、2 つのことを意味します。

  • その NuGet パッケージは、宣言したバージョンの .NET Standard で定義されている標準ライブラリのみを使用して動作すること。
  • その NuGet パッケージは、宣言したバージョンの .NET Standard をサポートしているプラットフォーム用のアプリケーションから使用できること。

より低いバージョンの netdstandard TPM でビルドされたライブラリほど、サポートできるプラットフォームが多い、と言えます。

NetStandard.Library パッケージ

さて、NuGet パッケージとしての .NET Standard(NetStandard.Library)は、仕様としての .NET Standard の実装です。個人的には、このパッケージの存在が混乱の元だと思っています。
以前に .NET Fringe Japan 2016 で荒井さんが説明していたように、実体としては、.NET Core 用の実装または既存のプラットフォーム用のファサードアセンブリとなっている様々なパッケージ(System.Runtime など)を参照するメタパッケージです。

なぜこのようなものが必要になったのか、少し歴史を振り返ってみましょう。

当初、.NET Framework のクラスライブラリが出た当初、アセンブリをどう分けるかという方法論はあまりなかったように思います。事実、mscorlib と System が相互参照していたり、Queue や Stack はジェネリック版が mscorlib ではなく System にあったりします。当時は、とにかく mscorlib や System にどんどん型を載せていきました。その結果、アセンブリの肥大化やアセンブリ間の依存関係が複雑化してきてため、モダンアプリケーションの基盤となる Windows Runtime 向けのライブラリ群ではパッケージの分割が行われました。そして、.NET Core でも、パッケージ分割の方針を進めていきました。
しかし、やはりそれではアプリケーションを作るのに多数のパッケージを都度参照させる必要があるため、アプリケーション作成から見ると煩雑になりました。そのため、.NET Standard の仕様で定義された範囲内の corefx パッケージをまとめて参照できる便利パッケージとして、NetStandard.Library メタパッケージが生み出されました。これにより、.NET Standard に含まれるパッケージ群を一発で参照できるようにしたものです。

netstandard.dll

さて、この NetStandard.Library ですが、次にリリースされる .NET Core で実装されると思われる .NET Standard 2.0 では、netstandard.dll というファサードアセンブリになる予定です。つまり、NetStandard.Library がメタパッケージではなく、単一のファサードアセンブリになり、全プラットフォーム向けに提供されるようになるのです。
これが何を意味するのかについて、考察します。

興味がない場合は、次の節まで読み飛ばすことをお勧めします。

.NET Standard 2.0

さて、ここで少し未来に目を向けて、 .NET Standard 2.0 の話をしましょう。github のリポジトリMyGet からダウンロードできるパッケージをもとに、できる限り正確な内容や、それに基づく考察を述べますが、2016/12/2 時点でまだ開発中の内容であり、実際には異なる形でリリースされる可能性があります。

.NET Standard 2.0 は、最近(といっても数か月前ですが)発表された、.NET Standard の新しいバージョンです。
これまでの .NET Standard は、過去の .NET Framework Class Library(FCL)での設計上の過ちや、クロスプラットフォーム対応を考慮し、「本来あるべき」API セットを実装した corefx に対し、既存技術を対応させる形でバージョンを付けているように見えます。
その結果、.NET Standard 1.x による制約は、既存のライブラリの .NET Core 対応に対して大きな制約がありました。たとえば、

  • リフレクション API の設計改善に伴う非互換性。ライブラリやフレームワークはリフレクションを使用するものも多く、単に互換性のない API は移行の障壁となりました。
  • ADO.NET 周りのインターフェイスの廃止に伴う、既存 ORM への影響。
    これは ORM の API として使われているインターフェイスもあり、.NET Core に対応するために、実装だけでなく API の仕様も変えざるを得ないということであり、すなわちそれを利用するアプリケーションにも影響があるということです。
  • 不足する機能。たとえば CodeDOM は Roslyn によって置き換えらえる過去の技術とされていましたが、実際には言語非依存(と言っても実質的には VB と C# を同時サポート)のコード生成技術という点で、CodeDOM は Roslyn を置き換えるものではありませんでした。
  • その他既存のプラットフォームにあったライブラリ(System.Json とか、BinaryFormatter とか)

当初、パッケージ分割や API の整理による利点のメリット(たとえば API の分かりやすさ)は大きく、さらに OSS や SDK などのエコシステムは .NET Core の普及に伴い、自然と新しい API に移行する、と思われていたのでしょう。

しかしながら、.NET のエコシステムは既に想定以上に広がっていました。そして、メジャーなプラットフォームは(Unity を除けば)やはり .NET Framework であり、さらに Xamarin もベースとしている Mono が .NET Framework 互換のため、多くのライブラリは .NET Framework 用にリリースされていました。それらは当初の目論見のように付いていくことはできませんでした。彼らはそもそも毎日のように新しいサービスのために SDK をバージョンアップしたり、余暇を割いて OSS に貢献していたりして、それほど余裕がなかったのかもしれません。

そこで、これまでの欠点を補い、エコシステムを活用できるようにするため、新しい .NET Standard の仕様、実装、ライブラリである .NET Standard 2.0 が提唱されました。
(なお、現状、corefx や .NET Standard のリポジトリでは netstandard1.7 という TPM で表されています)

仕様としての .NET Standard 2.0

さて、.NET Standard 2.0 の目的を果たすためには、仕様の観点から 2 つやることがあります。

  • .NET Standard の範囲を広げ、従来の .NET Framework の API をより多くサポートすること。
  • 既存の .NET Framework 向けのライブラリを .NET Core(など)でそのまま動作できるようにすること。

.NET Standard の範囲の拡大

仕様として、.NET Standard 2.0 には以下のようなライブラリ群が追加されています。

  • BinaryFormatter と、それに伴うインターフェイスやカスタム属性。BinaryFormatter を使う機会が新規にどの程度あるのかについてはコメントを差し控えますが、[Serializable] 属性や ISerializable を実装した型(例外型など)を持つライブラリにとっては朗報でしょう。
  • DataSet。既存 ORM への配慮やもちろん、DbConnection.GetSchema() のサポートができるようになりました。DataSet を使うサーバーアプリケーションの移行先としての価値も上がったかもしれません(WebForm を使っていそうな気もしますが)。

既存ライブラリを使用可能に

.NET Standard 2.0 では、既存の .NET Framework 向けライブラリを(主に .NET Core 上で)実行できるように、FCL 互換のファサードアセンブリが追加されています。
これらは [TypeForwardedTo] 属性が大量に宣言されており、実行時に本来の FCL のアセンブリの型に転送することで、TypeLoadException などで落ちないようにする役割を持ちます。
具体的には、.NET Standard のリポジトリによれば、以下のアセンブリです。

  • mscorlib
  • System
  • System.ComponentModel.Composition
  • System.Core
  • System.Data
  • System.Drawing
  • System.IO.Compression
  • System.IO.Compression.FileSystem
  • System.Net.Http
  • System.Numerics
  • System.Runtime.Serialization
  • System.Web
  • System.Xml.Linq
  • System.Xml

ここにないものは対応していないか、または拡張パッケージということになります。Windows に強く依存している機能(を使うライブラリやアプリケーション)なので、.NET Standard に移行するモチベーションもないだろうということなのでしょう。たとえば、System.EnterpriseServices、ADSI.NET 周りはありません。レジストリや WMI.NET のカスタム属性、CodeDOM や System.Transactions などは、使用頻度が多いからか、拡張ライブラリとして別途互換パッケージが提供されるようです。リポジトリ を見る限り .NET Remoting や Code Access Security もあるようですが、.NET Core で実装される可能性は低いと思われます(もっとも、coreclr のリポジトリを見ると、ランタイムの中に .NET Remoting や Code Access Security 用のコードがそのまま残っているように見えますが)。

TPM としての .NET Standard 2.0

これは素直に netstandard2.0 になると思われます。

corefx の内容を見る限り、新しく corefx に加わる拡張ライブラリ群は netstarndard2.0 向けになるようなので、新しく実装が加わったパッケージに依存するライブラリやフレームワークはこの TPM を使うことになるでしょう。たとえば MS-DTC を使わずに XA トランザクションを実装するライブラリとか、業務アプリケーション用の社内フレームワークとか。拙作 MsgPack for CLI も、CodeDOM を使ったソースコード生成を別パッケージに切り出し、Mac 上の開発環境や Linux 上の CI 環境などでもソースコード生成ができるようにしようと考えています(今でも mono で動かしてもらえば問題なく動くはずですが、古いバージョンだったりするとうまく動かないこともあるようです)。

ORM や Data Provider も、netstandard2.0 向けビルドにしてより多くの API が(.NET Framework 向けビルドと同じくらい)提供されるようになるでしょう。

パッケージとしての .NET Standard 2.0

これまではメタパッケージでしたが、これからは netstandard.dll というファサードアセンブリが提供されるようになる予定です。

なぜでしょうか。blog や github の説明だけだといまいちよくわかりませんが、現状 MyGet に出ているパッケージやソースコードも併せて見ると、以下のようなものと考えられます(あくまで解釈です)。

新規にビルドするアプリケーションが、既存のライブラリを活用するとしましょう。しかも、ライブラリのうち、あるものは .NET Framework 向け、別のものは .NET Standard 1.x 向けにビルドされていたとしましょう。.NET Standard 2.0 なら、それらの両方の API をサポートしているため、すぐに両方が使えるようになる……と言いたいところですが、そうはなりません。なぜならば、使用している型のアイデンティ(ここでは、アセンブリ名と名前空間と名前の組み合わせ)が異なるからです。
たとえば、それらのアセンブリが、System.String を受け取るインターフェイスを持っていたとします。.NET Framework 向けのライブラリは、System.String は(ILDasm 風に書くと)mscorlib!System.String 型だと主張し、.NET Standard 1.x 向けのライブラリは System.Runtime!System.String 型だと主張するでしょう。そうすると、型のアイデンティティが異なるので、片方から返された System.String をもう片方のライブラリに渡せないことになります。
これでは実用になりません。これを解決するために、新しい .NET Standard 2.0 のパッケージには、以下のものが含まれています。

  • FCL 互換の Facade アセンブリ。これらは大量の [TypeForwardedTo] を持ちます。その転送先は netstandard.dll です。
  • 現在の NetStandard.Library メタパッケージに含まれているのと同じ名前の Facade アセンブリ(System.Runtime など)。これらも大量の [TypeForwardedTo] を持ちます。その転送先は netstandard.dll です。
  • netstandard.dll。なお、API の宣言のみがあり、実装はありません。

この仕組みにより、アプリケーションのビルド時には .NET Framework 用のライブラリが主張する型も、.NET Standard 用のライブラリが主張する型も、netstandard.dll にフォワードされます。つまり、先ほどの例で言えば、アプリケーション(のアセンブリ)は、「結局どちらも netstandard!System.String のことを言っているんだな」と解釈します(そのように TypeRef が解決されるはずです)。

現在のところ、MyGet からダウンロードできる、この新しい NetStandard.Library パッケージには実装(つまり lib フォルダー)がありませんが、.NET Standard のリポジトリや MSDN Blog の記事を見る限り、何らかの方法で、以下のような実行時の型転送用の netstandard.dll が提供されると想定されます。

  • .NET Framework や Xamarin 向け
    • FCL のアセンブリへの [TypeForwardedTo] を大量に持つ、.NET Framework や Xamarin 向けにビルドされたアセンブリの実行用の netstandard.dll
    • netstandard.dll へのアセンブリへの [TypeForwardedTo] を大量に持つ、.NET Core 向けにビルドされたアセンブリの実行用の System.Runtime.dll 等のアセンブリ群(最終的には mscorlib.dll などに転送されます)。
  • .NET Core や UWP 向け
    • corefx のアセンブリへの [TypeForwardedTo] を大量に持つ、.NET Core や UWP 向けにビルドされたアセンブリの実行用の netstandard.dll
    • netstandard.dll へのアセンブリへの [TypeForwardedTo] を大量に持つ、.NET Framework や Xamarin 向けにビルドされたアセンブリの実行用の mscorlib.dll 等のアセンブリ群(最終的には System.Runtime.dll などに転送されます)。

具体的に言うと、実行時、アプリケーションアセンブリを JIT コンパイルする(ために型をロードする)タイミングで、ランタイムのタイプローダーが netstandard!System.String を、mscorlib!System.StringSystem.Runtime!System.String としてロードするわけです。そして、この動作は、参照先のライブラリアセンブリ内のコードが JIT コンパイル/型ロードされる場合も同じです(こちらは、いったん netstandard に転送された後、再転送されるでしょう)。

アプリケーション開発者が知っておくべきこと

アプリケーション開発者が考えるべきことはシンプルです。
「で、結局どれが使えるの?」
これだけですよね。

一覧表を見ればわかりますが、ざっくりいうとこんな感じです。

  • .NET Core と Xamarin で動けばいいよね!
    • netstandard 対応であればどんなライブラリでも動きます。
  • 最新のサーバーで動けばいいや。
    • netstandard1.5 まで行けます。.NET Framweork 4.6.1 ならサポートできるからです。
  • UWP です。
    • netdstandard1.4 まで行けます。
  • .NET 4.6.0 をサポートする必要があるんだよね。
    • netdstandard1.3 まで使えます。
  • .NET 4.5.1 をサポートする必要があるんだよね。
    • netdstandard1.2 まで使えます。
    • Windows 8.1(Phone を含む)をサポートする必要がある場合も同じ。
  • .NET 4.5.0 をサポートする必要があるんだよね。
    • netdstandard1.1 まで使えます。
    • 実は Windows 8.0 があって……という場合も同じ。
  • Windows Phone Silverlight 8.0 です。
    • netdstandard1.0 なら使えます。
  • Unity|.NET 3.5|Silverlight5です。
    • 使えません。

また、.NET Core アプリケーションから、従来の .NET Framework 向けのライブラリを使うことはできません。.NET Standard と互換性のある PCL なら大丈夫ですが、パッケージの依存関係によっては PCL 同士の相性がかみ合わない
(もう少し正確に言うと、対象バージョンフラグの論理積が空になり)場合など、うまくいかないことがあります。

そうすると結構なライブラリ(Azure 系の SDK とか)が使えなくなるので、先ほど述べたように .NET Standard 2.0 が実装されようとしているのでした。

あと、標準ライブラリのうち、どのパッケージを参照すればいいの? という問いに対しては、dotnet new で作られる project.json(今後は csproj)では、前述の NetStandard.Library パッケージを間接的に参照している状態で作成されるため、大体の場合は気にしなくていいはずです。

ライブラリ開発者が知っておくべきこと

ライブラリ開発者が考えるべきことは、いかに多くのユーザーに使ってもらえるかです。素敵なライブラリを作ったとしても、多くのユーザーに使ってもらえなければ意味がありません。そのため、これから作成するのであれば、netstandard1.x の、しかもできる限り小さなバージョンでビルドしましょう。

さて、ここで残念なお知らせがあります。

今や .NET ユーザーの大多数を占める(と思われる)Unity ですが、ご存知のように、.NET 3.5 互換です(2016/11/30 現在)。つまり、たとえ netstandard1.0 向けであっても、Unity では動きません。Unity 向けのライブラリを作りたい場合は、素直に .NET 3.5(正確には Mono 2.6)で動くライブラリを .NET 3.5 用にビルドしましょう。ただし、希望はあります。(Mono ランタイムが MIT ライセンスになったこともあり)Unity は .NET 4.6 相当の機能が使えるように改善を計画中で、.NET Standard への対応を進めているようです。あなたのライブラリの開発が終わり、netstandard 環境で十分に有名になったころには、Unity も追いついているかもしれません。

これだけだと何なので、netdstandard1.x の選び方の簡単な基準を書いておきます。

  • 純粋なアルゴリズムのみのようなライブラリであれば、netstandard1.0 にできるでしょう。
  • そうでなくても、多くの場合、netstandard1.1 でビルドできるでしょう。ZIP、HTTP 通信、同時実行コレクションなどがサポートされます。
    • netstandard1.2 は P/Invoke 用の API やタイマーなど多少増えていますが、大差ありません。最も、1.1 は使えるが 1.2 は使えないプラットフォームは .NET 4.5.0 と Windows 8.0 なので、1.1 にする積極的な理由もないかもしれません。
  • 外部プロセス、標準入出力、ファイル入出力などが必要な場合、netstandard1.3 が必要になるでしょう。ただし、.NET 4.5 系はサポートできません。
    • netstandard1.4 は楕円曲線暗号が追加されています。
  • リフレクションを多用する既存ライブラリを .NET Standard 対応にする場合、netdstandard1.5 にすると、多くの API を使用できます。ただし、netstandard1.1 向けであっても、同等の機能は存在しているので、互換レイヤーなどを実装してしまうのも手です。
  • .NET Core でだけ動けばいいのであれば、ケチケチせずに netstandard1.6 にしましょう。Xamarin も大丈夫です。

迷ったなら、netstandard1.3 にしておけばとりあえず問題ないと思います。

なお、netstandard2.0netdstandard1.4 をサポートするプラットフォーム(.NET 4.6.1、UWP、Xamarin、.NET Core)で使用可能になる見込みです。.NET Standard 2.0 が出た暁には、.NET 4.6.0 や Windows Phone 8、Windows 8.x アプリをサポートするのでない限り、netstandard2.0 にしてもいいでしょう(そうすると、netstandard1.5netdstandard1.6 とは何だったのか、という話になりますね……)。

まとめ

  • .NET Standard には 3 つの側面があります。仕様としての .NET Standard、TPM としての .NET Standard、そしてパッケージとしての .NET Standard です。
    • 仕様としての .NET Standard は、CLI の実装系が持つ べき API のセットを定義します。
    • TPM としての .NET Standard は、その NuGet パッケージがどのプラットフォームをサポートできるのかを定義します(PCL の後継)
    • NuGet パッケージとしての .NET Standard は、仕様としての .NET Standard の実装です。
      • 実体としては、.NET Core 用の実装または既存のプラットフォーム用のファサードアセンブリとなっている様々なパッケージ(System.Runtime など)を参照するメタパッケージです。
      • .NET Standard 2.0 では、netstandard.dll というファサードアセンブリになる予定です。
        • それにより、既存の .NET Framework 向けライブラリと、最近の .NET Standard 向けライブラリの両方を、.NET Standard で動作するアプリケーションで使用できるようになるはずです。
  • アプリケーション開発者として知っておくべきこと
    • .NET 4 以降、Xamarin、.NET Core、UWP や Windows Phone アプリケーションは、それぞれのプラットフォーム向けのライブラリに加え、アプリケーション プラットフォームがサポートするバージョンの .NET Standard 用にビルドされたライブラリを使用できます(もちろん対応する PCL も)。
    • .NET Standard 2.0 では、既存の .NET Framework 向けの NuGet パッケージ(つまり、.NET Standard 向けとしてビルドされていないパッケージ)も、.NET Standard 2.0 をサポートするパッケージであるかのように使うことができます。
      • たとえば、ASP.NET Core から、(.NET Core 向けの実装のない)Azure の PaaS の SDK なんかを参照できます。
    • .NET Standard 2.0 リリース後、多くのパッケージは .NET Standard 2.0 対応となることが予想されます。.NET 4.6.1(以降)への移行を計画しておくとよいでしょう。
  • ライブラリ開発者として知っておくべきこと
    • これから開発するのであれば、netstandard1.x 向けのものを作るべきです。
      • このとき、x はビルドできる最小のバージョンにすべきです。
    • 既に .NET 4.5 以降で動作するライブラリを持っているけれども、netstandard1.x に対応するのに躊躇している場合、.NET Standard 2.0 を待つのも手です。
      • おそらく、そのライブラリは netstandard2.0 向けのパッケージと同じように使えるはずです(もちろん、OS やランタイム依存の処理がある場合を除く)
    • Unity はあきらめましょう。ただし、希望はあります。

気付けばかなりの長文となっていますが、現時点での内容を整理すると以上になります。
それでは、happy hacking。

2016年10月4日火曜日

マルチプラットフォーム対応のNuGetパッケージの作り方

まず、2016/10/4 現在、NuGet Docs サイトはリニューアルされ、かなり見やすくなってますので、ぜひそちらも参照ください。
昔のサイトもそれほど読みにくくはありませんでしたが、色々と情報が増えてどこを見ればいいのかわからない感じだったのも事実です。なので、個人的にはかなり良くなったと思います。 全文検索もできますし。
たとえば、docs.nuget.org のトップページにある「Guides」には目的別のガイドラインがありますし、クイックスタート的な記事もあります。このあたりを見れば結構何とかなるはずですし、リファレンスもメニューからたどることができます。
さて、その公式ドキュメントを見ればマルチプラットフォーム対応のやり方もわかると思います。色々書いてありますが(<file src> とか初めて知った)、ようは以下のようにすれば OK です。
  • .nupkg ファイル生成のソースとなるディレクトリツリー内の lib の下に、ターゲットプラットフォームを表す名前(TPM:Target Platform Moniker)のディレクトリを切り、そこに .dll と、IntelliSense 用の XML ドキュメントコメントファイルを入れます(XML 入れなくてもかまいません)
  • 依存先のパッケージがある場合、.nuspec/package/dependencies 要素の下に、TPM ごとに <group targetFramework="{TPM}"> 要素を対象の TPM ごとに記述します。この子要素として、依存先のパッケージを <dependency id="{パッケージの名前}" version="{バージョン}" /> といった具合に書いていきます。
  • .NET Standard 用の TPM を含む場合、サポートしていない古いバージョンの NuGet でよくわからないエラーが出ることがないように、/package/metadata 要素に minClientVersion="2.12" と記述します(これ、docs.nuget.org に書いてませんね……)
参考までに、MsgPack for CLI の .nuspec ファイルはこちら
ちょっと文字ばかりでわかりづらいでしょうか。実際に展開した .nupkgtree /F した結果はこんな感じです。
C:.
│  MsgPack.Cli.nuspec
│  [Content_Types].xml
│
├─lib
│  ├─MonoAndroid10
│  │      MsgPack.dll
│  │      MsgPack.xml
│  │
│  ├─net35-client
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─net45
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─net46
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─sl5
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  ├─windowsphone8
│  │      MsgPack.dll
│  │      MsgPack.XML
│  │
│  └─Xamarin.iOS10
│          MsgPack.dll
│          MsgPack.XML
│
├─package
│  └─services
│      └─metadata
│          └─core-properties
│                  d502c190230a45328393d9c274cf792e.psmdcp
│
└─_rels
        .rels
ターゲットモニカについては、公式ドキュメント を参照してください。
Xamarin についてはちょっとわかりづらいですが、上記の例の記述で動くはずです。なお、monotouch が旧形式の Xamarin iOS、Xamarin.iOS が Unified app 版になります(Xamarin のドキュメント)。なお、旧形式については、Xamarin iOS の 10.0 からサポートされていません
さて、実際に作る際には、以下のようにするといいでしょう。
  • 実際に対応している OSS パッケージのディレクトリ構造や .nuspec ファイルを参考にする。私は JSON.NET や CoreFX のパッケージを参考にしました。
  • ローカルのディレクトリに配置して、動作を確認する。なお、v3 の場合、配置先のディレクトリを nuget init しておき、nuget add でパッケージを追加する必要があります。また、前に試した時、ローカルディレクトリは Windows でしかうまく動作しませんでしたが、最近は変わったかもしれません。
それでは、happy hacking!

.NET Fringe Japan 2016でしゃべりました

少し間が空いてしまいましたが、先週末、.NET Fringe Japanというイベントで(珍しく)しゃべってきました。当日のスライドはこちら

内容としては、まずは準備運動よろしく F# の話から始まり、C# の言語機能への提案方法まさかの .NET じゃない話Xamarin ソースの見方の Deep DiveOSS 開発のためになる話 と続いていき、日が沈むとともに IL を追加する方法の話 に行ったときにはどうなるかと思いましたが、最後は地に足の着いた deep dive でうまく終わりました。いや、松井さん最高です。正直あまり運営のお手伝いはできなかったのですが、色々な人の助けもあり、成功に終わったのかなと思います。いやー、みなさんコンテンツもしゃべりも上手で、そちらの方でも勉強になりました。

セッションスケジュール的には、ちょっと詰め込みすぎでしたかね。ただ、初回ということで、こんなマニアックな会で本当に人が集まるのか、という懸念もあり、1 トラックでまずはやってみようという話になったのでした。雨で、かつ色々と楽しそうな裏番組があった中で 100 人近い来場者があったので、次回は 2 トラックくらいにできるのかもしれません。

さて、私も好き勝手しゃべったわけですが、荒井さんのセッションの質問とか、懇親会での話とかを考えると、そもそもマルチプラットフォーム対応の NuGet パッケージの作り方を普通に説明すればよかったのでは、と気づいたので、一筆書こうと思います。そんなの日本語で話しても需要がないだろうと思っていたのですが、あそこはそういうことをする人たちの集まりなのでした :)

あと、あのメンツはどう見ても亜流とかいう生易しいものじゃなくて過激派グループだと思います

2016年6月24日金曜日

DispatchProxyのPRを送ってみた

前回の続きです。

というわけで、.NET Core の、System.Reflection.DispatchProxy が返すオブジェクトからなぜかプロパティとイベントが取れない問題があって、さすがにこれはどうかなと思ったので、PRを送ってみたので、その顛末でも書きます。ほとんどポエムです。

問題の解説

そもそも、

IFoo foo = DispatchProxy.Create<IFoo, SomeProxy>();
foo.Bar = "ABC";

みたいなコードがコンパイルされ、動作するのに、

foo.GetType().GetRuntimeProperty("Bar");

nullを返すのはなぜでしょうか?
それは、fooオブジェクトの実態である動的に生成された型が、プロパティ(やイベント)のアクセッサーメソッドのみを実装してプロパティやイベントを宣言していないからです。
前回の記事をよく見ると、

動的に生成したクラスに、インターフェイスが宣言しているすべてのメソッドを宣言します。

と書いてありますね。 そう、DispatchProxy.Create<T, TProxy>()は動的に生成した型にアクセッサーメソッドを実装するだけで、それらが属するプロパティやイベントを型で宣言させないのです(もう少し内部的な話をすると、動的に生成されたプロキシ型のメタデータテーブルにプロパティとイベントのエントリを書き出さないということです)。
しかもなんと、インターフェイスを実装した型では、そのインターフェイスのプロパティやイベントのアクセッサーメソッドさえ宣言されていれば、プロパティやイベントは宣言しなくても動作するのです(これが動作するのが正しいかどうかという話はありそうですが)。
C# が出力するのはアクセッサーメソッド呼び出しのILですからちゃんとコンパイルでき、かつ動作するのですが、プロパティやイベントが宣言されていないので、リフレクションでプロパティやイベントそのもののメタデータは取れないというわけです。

ポエム

これだとちょっと困るので、DispatchProxyGenerator の中で、プロキシ型にインターフェイスのプロパティとイベントを宣言させるようにしたPRを送ったよという話です。
ちなみに、diffを見ながらじゃないとついていくのがつらいと思います。

第0の関門:どう実装するか

TypeBuilder.DefineProperty()DefineEvent()ですね。あと、アクセッサーメソッドをPropertyBuilderEventBuilderに設定する必要があります。
なぜかこの辺のAPIは頭に入っているので、実装はあっさり終わりました。

第1関門:設計意図

意気揚々と直しましたが、そういえば、そもそもこれって意図的にプロパティやイベントを実装していないのかもしれません。
なので、とりあえずissueをあげて様子を見ました。
きっとRTM前で忙しいでしょうし、ということで、10日くらい待ちました。
で、音沙汰もないうちにリリース1週間前になり、よく見ると依存先のバージョンがrc3からrc4になってるし、これはもう1.1向けの開発モードになってそうだなということで、「何か問題あるかな? なさそうなら実装持ってるからPRできるよ」って聞いてみたところ、「急いで入れる必要ないよね? 実際に直し始めたら色々聞きたくなると思うけど、他にやることあるからちょっと待ってー」って言われて、「ていうか直したんだったらもうPR投げていいよー」と言われたので、PRすることにしました。

第2の関門:ビルドと単体テスト

PRを出すには、当然単体テストを実行しなければなりません(追加のテストは既に書いたものとします)。
ところが、corefxのプロジェクトは少々特殊なproject.jsonになっているので、素直にビルドできません。
corefxのドキュメントを読めばいいのですが、1.0 RC2 時点で、Windows環境でビルドするには以下のようにします。Visual Studio(ライセンス的に問題がない限り、無料のCommunity Editionでも大丈夫のはずです)が必要です。

  1. Visual Studio 2013の MSBuild へのパスが通っているコマンドプロンプトを開きます(「開発者コマンドプロンプト」とかそういうやつです。ちなみに、私はVS 2015用でやりましたが、問題はありませんでした)。
  2. corefxのルートにある、init-tools.cmdを実行します。
  3. 修正したコードのテストプロジェクトのディレクトリに移動し、msbuild /t:BuildAndTestを実行します。

ちゃんとドキュメントを読んでいればなんてことない話でした。

あと、実は初めてのPRだったのでちょっと戸惑ったり、CLAへの署名とかもありましたが、特に躓きはありませんでした。

第3の関門:レビュー

主にvarの使い方でダメ出しをくらいました。「型が明確な場合を除きvarを使わない」とありますが、これは「コンストラクター呼び出しの結果を割り当てる場合を除き」くらいのニュアンスだったようです。
他には、

  • 変数の宣言位置(実はリファクタリングしたときの修正漏れ)
  • ifの本体の改行位置(普段は中括弧必須派なので、うっかり)

などのポカミスが多かったです。正直手間を取らせて申し訳ない。
そして、次の関門。

第4の関門:MethodInfo.Equalsの罠

MethodInfo.Equalsは.NET Core だと参照比較として実装されているから、MethodInfoDictionaryのキーにしちゃダメよー」というありがたい突っ込みをいただきました。
(.NET メタプログラミング界隈の人たちが青ざめる姿が見えた気がしました)
MetadataToken比較するようなEqualityComparer作ればいいと思うYo」とのアドバイスをもらったので、それはそもそも.NET Coreに入っているべきではと思いつつ、それを英語で記述するよりは C# でコードを書く方が早いので書きました。確かに、メタデータテーブルのインデックスであるMetadataTokenを比較するのは確実で、かつ高速な手段ですしね。

第5の関門:netstandard1.3

はい。MemberInfo.MetadataTokenはnetstandard1.5のAPIなのです(.NET Framework側には昔からありますが)。System.Reflection.DispatchProxyは1.3用のライブラリなので、MetadataTokenは使えません。
なので、愚直に名前とパラメーターの型を比較する実装にしました。

第6の関門:ジェネリクス

意気揚々とプッシュしたEqualityComparer<MethodInfo>実装について、「この比較だとジェネリック型引数見てないからダメじゃね?」という真っ当な指摘を受けました。その通りですね。
ついでに「ECMAがどうかわからないけど、静的メソッドかインスタンスメソッドかも比較しないとまずいんじゃないかなー」と言われたので、ECMA-335を見てみました。
そのものずばりのところはないのですが、「I.8.6.1.6 Signature Matching」を見る限り、

  • 静的メソッドかインスタンスメソッドは比較すべき
  • 戻り値の型も比較すべき
  • 呼び出し規約(CallingConvention)も比較すべき

と判断し、これの真偽(実はいらないんじゃないか)を確かめるよりこの通り実装した方が早いので、実装しました(DispatchProxy.Create()をループの中で呼び出して遅いとかいう人はいないでしょうし、そもそも動的コード生成やJITコンパイルに比べたら大したことないでしょうし)。

第7の関門:raise

ここで、「そういえば、イベントのaddメソッドとremoveメソッドは割り当ててるけど、raiseメソッドも割り当てないとダメじゃないかな」という発言が出ました。こいつ、気付きやがった……。同時に「じゃあotherはどうする?」って聞き返そうかと思いましたが、.NET Coreにotherメソッドを割り当てるためのAPIないし、0x20歳を過ぎた大人なので藪蛇になることは言わないことにしました。

とはいえ、C#コンパイラにraiseメソッドを出力させる方法はありません。つまり、テストコードで動的型生成をしないと行けないということです。それは正直ちょっとやりすぎかなーと思った(この後保守するのが面倒になるんじゃないかなと思った)ので、「ほんとにやる?」って返信して、寝ました。もう丑三つ時回ってましたし。

翌日、仕事から帰ってきたところ、ノーレスだったので、四の五の言わずにやれや、ということだと判断し、あきらめて実装しました。実装はECMA-335を見ながらやったのですが、raiseメソッドのシグネチャはvoid raise_xxx(Event e)でなければならないとか書いてありつつ、Eventが何なのかは一切説明がありませんでした。ただ、普通に考えればイベントハンドラーに渡すデータでよかろうということで、適当にEventArgsを渡すようなメソッドを生成させました(これを書いていて思ったのですが、イベントの型は任意のデリゲートになり得るで、EventArgsではないような気はしますね。じゃあ何なのかというと謎ですが、テストでコード生成例外は起きてないし、所詮テストコードの動作の話なので良しとしましょう)。

あと、このためにテストプロジェクトからSystem.Reflection.Emitパッケージへの参照を増やす必要がありました。

第8の関門:dotnet-bot

10日間も間を開けたのは失敗でした。そう、1.1開発モードにシフトしているので、依存先パッケージのバージョンがガンガンアップデートされていきます(いつの間にかrc4betaになってました)。
そう、raise_メソッドのテストのためにテストプロジェクトのproject.jsonをいじっているので、botによって依存先パッケージのバージョンが書き換わると、修正がコンフリクトするのです。具体的には、変更をプッシュして「コンフリクトがないからマージできるよ」ってGitHubに言われても、数時間のうちにコンフリクト状態になってしまう状態でした。
ところが、レビューしてくれた人たち以外は忙しいのか何なのか、LGTMが増えず、マージされません。
コンフリクトのまま放置していもレビューしてくれなさそうなので、仕事から帰ったらマージして再プッシュとかしてみたものの、毎日再プッシュされてもレビューする側は嫌だと思うので、「毎日project.jsonマージしなおして上げなおしたらうざいと思うんで、レビューできる状態になったら教えて。そのときまとめて再プッシュするから」と言ったら、「いや、もう何も言わないってことはいいってことでしょ」と言われ、マージされたわけです。

結論

  • corefxの「varは控えめ」は本当に控えめ。
  • さすがにレビューはしっかりやってる
  • project.jsonいじると頻繁にマージコンフリクトするから気を付けよう

感想としては結構楽しかったです(レビューコメントの英語も勉強になりましたし)。

余談

レビューしてくれた人、日本人っぽい名前だったのですが、日本時間の23:00(シアトルは7:00)台にレスくれるので、早起きなのか、日本にいる人が夜中に対応してくれてたのかがよくわからず。
いずれにせよ、深夜早朝に丁寧な対応ありがとうございました。

DispatchProxy

.NET Core には、System.Reflection.DispatchProxy という型が追加されています。
こいつが何かというと、インターフェイスベースの AOP を実現するプロキシを生成するための仕組みです(Java の Proxy に近いといえばわかる人もいるでしょうか)。
DispatchProxy を使用して特定のインターフェイスのプロキシを作成すると、返されたオブジェクトに対するメソッド呼び出しに対する割り込みがかけられるようになります。

この説明でピンと来る人はいないでしょうから、もう少し丁寧に説明していきます。

使い方

まず、割り込み処理を実行するプロキシクラスを作ります。これは、System.Reflection.DispatchProxy 抽象クラスを継承したクラスです。
基底型である DispatchProxy クラスには、以下のような抽象メソッドが定義されています。

protected abstract object Invoke(MethodInfo targetMethod, object[] args);

どう見ても、メソッド呼び出しに割り込んで、処理を実装しろと言っていますね。この中で、

  • 割り込み処理を行う
  • 本来のディスパッチ先のメソッドを呼び出す
  • メソッド呼び出しの戻り値を返す(targetMethod の戻り値型が void なら null で OK)

などをすればOKです。
後は、やりたいことに応じて、このプロキシにプロパティやメソッドを生やしてください。
ただし、このプロキシ型はpublicでなければならず、さらにpublicなデフォルトコンストラクターが必要です(後述しますが、つらい)。
そして、DispatchProxy.Create<T, TProxy>() 静的メソッドで、プロキシ型のインスタンスを作成します。このとき、任意のインターフェイス型と、先ほど作成したプロキシ型を指定します。

IMyModel obj = DispatchProxy.Create<IMyModel, MyAopProxy>();

この戻り値のobjのメンバーを呼び出すと、MyAopProxyInvoke(MethodInfo,object[])が呼び出されます。やった。

さっそく例を見てみましょう。
昔から、AOPと言えばトランザクション管理とログ出力と相場が決まっています。
たとえば、IMyModelというインターフェイスと、実装であるMyModelがあり、呼び出しのトレースを出力する TracingAopProxy を作るとしましょう。
まず、プロキシクラスを以下のように記述します。追加のプロパティはAPからは呼んでほしくないので、internalにしてます。

public sealed class TracingAopProxy : DispatchProxy
{
    // トレース出力先
    private TraceSource _trace;
    // 実際の処理を行うオブジェクト。
    private object _model;
    // インターフェイスメソッドと
    private Dictionary<MethodInfo, MethodInfo> _dispatchTable;

    public TracingAopProxy() { }

    // コンストラクターが引数を宣言できないので、初期化メソッドを用意。
    internal void Initialize(Type interfaceType, object model, string sourceName)
    {
        _model = model;
        _trace = new TraceSource(sourceName);
        _dispatchTable = new Dictionary<MethodInfo, MethodInfo>(new MethodInfoEqualityComparer());
        foreach (var method in interfaceType.GetRuntimeMethods())
        {
            // .NET Core には GetInterfaceMap() が実装されていないので、頑張って比較。
            // 厳密にやるなら、ReturnType と CallingConvention も。
            // インターフェイスの明示的な実装とかあるので、public メソッドかどうかは問わない。
            var target = model.GetType().GetRuntimeMethods().Single( m =>
                             m.Name == method.Name
                             && m.GetGenericArguments().SequenceEqual(method.GetGenericArguments())
                             && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(method.GetParameters().Select(p => p.ParameterType))
                         );
            _dispatchTable.Add(method, target);
        }
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        _trace.TraceEvent(TraceEventType.Verbose, "Enter: {0}", targetMethod);
        try
        {
            var result = _dispatchTable[targetMethod].Invoke(_model, args);
            _trace.TraceEvent(TraceEventType.Verbose, "Success: {0}", targetMethod);
            return result;
        }
        catch(TargetInvocationException ex)
        {
            _trace.TraceEvent(TraceEventType.Error, "Error: {0}", ex.InnerException);
            throw;
        }
    }

    // .NET Core では MethodInfo.Equals が参照比較で、うまく比較できないことがあるので必要
    private class MethodInfoEqualityComparer : EqualityComparer<MethodInfo>
    {
         public MethodInfoEqualityComparer() { }

         public override bool Equals(MethodInfo left, MethodInfo right)
         {
              return left?.MetadataToken == right?.MetadataToken;
         }

         public override int GetHashCode(MethodInfo obj)
         {
              return (obj?.MetadataToken.GetHashCode()).GetValueOrDefault();
         }
    }
}

で、以下のようなラッパーメソッドを作ってあげればOKです。

public static T WithTrace<T, TImpl>(TImpl model)
    where TImpl : T
{
    var proxy = DispatchProxy.Create<T, TracingProxy>();
    ((TracingProxy)(object)proxy).Initialize(typeof(T), model, "the.source");
    return proxy;
}

ここでのポイントは、DispatchProxy.Create<T, TProxy>() の戻り値の型は T 型ですが、同時に TProxy の派生クラスでもあるということです。
そのため、TProxy 型にキャストすることで、追加の初期化処理を呼び出すことができます(上記の例では、C# コンパイラーをごまかすために、いったんobjectにキャストしています。理由は本筋ではないので割愛します)。

内部動作の説明

DispatchProxy.Create<T, TProxy>() は、ざっくりいうと以下のような処理をやってくれます(他にも細かいことをいろいろやっていますので、興味のある方はソースを見るといいでしょう)。
1. TProxy クラスを継承したプロキシクラスを動的に生成します。
2. 動的に生成したクラスに、指定したインターフェイス(T型)を実装させます。
3. 動的に生成したクラスに、インターフェイスが宣言しているすべてのメソッドを宣言します。
4. 現在のメソッド情報をMethodInfoとして取得するILを出力します。
5. 実引数を格納するobject[]をインスタンス化するILを出力します。
6. 実引数をobject[]につめるILを出力します。
7. Invoke(MethodInfo, object[])を呼び出すILを出力します。
8. メソッドが戻り値を持つ場合、Invoke(MethodInfo, object[])の戻り値をキャストして戻します。

注意点

  • プロキシ型はpublicである必要があります。ライブラリやフレームワークを作るお仕事の人は「まじかよ……」と思ったかと思いますが、そう思った方は我慢せずにIssueを投げてみるといいと思います(自分は、後述するように他にもっと気になったことがあったので投げてないです)
  • 巷のよくある AOP の仕組みと同様に、インスタンスメソッドだけに割り込めます。
  • プロキシ型のInvoke(MethodInfo, object[])の実装によりますが、上記の例のように素直に実装してしまうと、呼び出し先で発生した例外がTargetInvocationExceptionになったりして透過的な割り込みではなくなります。気を付けましょう。式木か何かでMethodInfo.Invokeせずに呼び出すとか。
  • DispatchProxy.Create() で返されたオブジェクトに対してリフレクションを実行した場合、なぜかインターフェイスが実装しているはずのプロパティとイベントの情報が取れません。
    たとえば、
public interface IFoo
{
    string Bar { get; set; }
}

というインターフェイスに対して、

IFoo foo = DispatchProxy.Create<IFoo, TracingAopProxy());

とすると、

foo.Bar = "A";

というコードは(もちろん)コンパイルするし動作するにもかかわらず、

foo.GetType().GetRuntimeProperty("Bar")

nullを返します。なので、このオブジェクトをリフレクションベースのツールやライブラリに渡すと、うまく動作しないかもしれません。
対処方は今のところありません(次回に続く)