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。