.NET Core 2016年の締め – .NET Core Advent Calendar 2016

この記事は「.NET Core Advent Calendar 2016」の25日目の記事です(遅刻しました)。

今年は.NET CoreのRTMがあったので、知名度も上がったのではないかと思います。その辺りを適当に回想したいと思います(深い話はなし)。


.NET Coreという存在

.NET Coreは次世代の.NET Framework、のようなざっくりとした紹介をされる事が多いです。製品的にはそのような位置づけであり、実際初期のプロジェクトはそういう方向性であったのではないかと思いますが、現在はちょっと印象が異なります。

netcore1.NET Coreが議論に上がる頃にMSが度々提示していた図がコレです。図自体もシンプルで、.NET Framework 4.6と.NET Core 5が両立され、従来の.NET Framework 4.6は互換性のために維持され、.NET Core 5はマルチプラットフォーム対応となることが示されています。

netcore2次のこれは、.NET Core 5がリバージョニングされて.NET Core 1.0となり、正式リリースされた頃の図です。見たところ、.NET Frameworkと.NET Coreの位置づけは変わっていません。そして、このころはまだ.NET Coreの役割としては「ASP.NET Core」に向けられていたことが分かります。

これを見ると、この時点ではまだ「Xamarin」が壇上にないですね。実際、まだMSはXamarinを買収していません。

netcore3この辺から変化が始まります。まず、.NET CoreとUWPの関係が示されるようになりました。それどころか「Any other app model」のような何かが示されていますね。そして、それらを統括するかのような「Unified BCL (Base Class Library)」という中間レイヤーのようなものが鎮座するようになりました。

# この図では、.NET Frameworkは省かれています。

netcore41一時、もっとも状況を良く表した図がコレだと思います。C#とVB.NETが上辺に、それらを使って従来通りのWebFormやMVC、WebAPIが.NET Framework上でサポートされる一方、新しいASP.NET CoreはCore CLRと.NET Native(あまり語られていないように思うのですが、.NET Coreはコンパイル時にネイティブバイナリを生成できます)が.NET Coreの上で動くこと。そして、.NET Framework上でもASP.NET Coreが動くことが示されました。

この図から、各コンポーネントの関係性が段々と複雑化してきていることがうかがえます。

netcore5最近語られるのがこの図です。Xamarinが正式に.NETのエコサイクルの一部として認知され、.NET Coreは「ランタイム」と「インターフェイス仕様」に二分されました。.NET FoundationというOSS団体が、「.NETの基本となるライブラリの外部規約」(例えば、どんなクラスがありどんなメンバが定義されているべきか)を査定し、これに「.NET Standard」と名前を付けました。

.NET Framework、.NET Core、Xamarin (Mono) など、各個別の.NET実装はこの外部規約に沿って再定義されたバージョンを、リリースする(あるいはリリースする予定)となりました。


.NET Standard(仕切り直し)

開発者は.NET Standardの規約を想定して開発を行うことで、これらのプラットフォームで共通に使用可能なライブラリのセットを想定できるようになりました。但し、これにはバージョンがあり、1.0~1.6、そして現在2.0を正式査定する準備を行っています。

netcore6この図は、.NET Standardのバージョンと、各プラットフォーム別のバージョンの対応を示した図です。わかりにくいのですが、例えば.NET Core 1.0の環境を使う場合、.NET Standard 1.0~1.6の規約を想定できる、と言うように読みます。

最も歴史の古い.NET Frameworkについてみてみると、.NET Framework 4.6の環境を使う場合は.NET Standard 1.0~1.3までの規約が想定できるわけです。また、.NET Standard 2.0を見ると、.NET Framework 4.6.1を想定するように「後方互換性」を取り戻していることも読み取れます。.NET Frameworkの更新が追い付かない(.NET Frameworkのランタイムの更新が早すぎることを良しとしない顧客の都合のような)事が見て取れます。

# 注: 私の勝手な想像です。

「Windows」というのは、紛らわしいのですが、Windows 8.1までの「ストアアプリ」に相当するプラットフォームの事です。ごく自然な流れでUWPに移行してしまったのと、Windows 8系列が失敗とみなされている(たぶん)事から、影が薄い存在です。ストアアプリでは、.NET Standard 1.0~1.2までを想定出来ますが、1.3以降の規約は「使用できない」ことも読み取れます。

Xamarin環境は、全部.NET Standard 2.0以降の規約でしか使用できないように読み取れます。実際には従来通り、PCL (Portable Class Library) を使えば、ほかのプラットフォームとの共通利用可能なライブラリを使う/作ることが出来ます。将来的には対応してくはずです。

.NET Standardは従来のPCLの規約を置き換えるものですが、主な動機付けとしては、PCLは各プラットフォーム毎に上位互換とか下位互換とかの概念が希薄で、それぞれのプラットフォームの都合に合わせて共通仕様を決めていた都合で、プラットフォームの増加に合わせて非常に複雑化してしまった、という事があります。その複雑性は組み合わせで増加するため、何か新しいプラットフォームに対応させようとすると、もう人間には正確に判断出来ない所まで来てしまっていました。.NET Standardに規約を統合していくのは、仕方のない面もあります。

そのようなわけで、私は.NET Standardを、.NETのライブラリ規約に置いての「仕切り直し」と位置付けています。

OSSに引っ張られ始めたマイクロソフト

dotnet_logo先日、某氏と話をしていて改めて再認識したのですが、MSがOSSに舵を切り始めた時から、MSの製品プロジェクトは遅かれ早かれOSSプロジェクトに引っ張られざるを得ない、という仮説です。と言うのも、この記事を書いていても思ったのですが、上のバージョンの表からも、.NET Framework(厳密に言うなら: プロプラエタリなWindows上の.NETランタイム実装)のリリースタイミングを、.NET Standardの歩調に合わせられていない、という事実です(もちろん、様々な要因があるのだとは思いますが)。

Windows環境は、環境を固定的に扱ったり想定したりする用途が多いように思います。何か問題が発生することを極度に恐れ、変化を肯定的に捉えられない環境で使われるために、ちょっとした問題が大きく取り上げられたりします(主観なので、もちろんそうではない環境もあると思います)。そういう環境での変化が許されないという文化が、.NET Frameworkの想定バージョンをあえて戻している(4.6.1)ようにも思えます。

しかし、そんなことはお構いなしに、OSSプロジェクトは進行します。また、多くの人がかかわっているので、もやはこの流れを止めることは出来ないでしょう。また、もたもたしていると、競合に技術で抜かされてしまいます。そうなっては、何のためのプロジェクトかわかりません。

# しかも競合はそういう足かせが無かったりするので、あまり拘っているとそもそも土俵にすら登れない。

結果として、プロプラエタリ製品はOSSプロジェクトの都合に引っぱられるようになるのではないかと予想していました。これが解消されるためには、プロプラエタリ製品もその進行の速さにしがみ付いていくしかないんじゃないかなと思っています。それはWindows Updateのようなサブコンポーネントにも表れるだろうし、その土台の上で開発をする私たちにも影響すると思います。

MSが批判を受けて、何らかの形で.NET Foundationを操ろうとしてももう無理なところまで来ているし、以前にも増して、.NETとMSの行動を「厳しい目で見ている人たち」が増えているはずです。私自身は、今後MSがどのようにこの問題に対処していくのかを、楽しみに見ています。


project.json(安楽死)

netcore7project.jsonは.NET Core 1.0で導入された、新しいプロジェクトの管理手法で使うファイルです。JSON形式で、従来*.csprojや*.fsprojが担っていた「プロジェクトの構成情報」を格納しておきます。何故*.csprojをやめたのかと言うのは良く知らないのですが:

  • XMLでの管理がイケてない – 最近のjs界隈ではJSONで管理するのが流行っているので、それに合わせたい(?) ひょっとするとVSCodeのようなエディタ回りを拡張する開発環境での発展を見込んだという事もあるかもしれません。
  • .NET Coreはマルチプラットフォームなので、MSBuildが使えない – MSBuildはWindowsに依存する部分が多いため、移植を端から諦めて、全く新しい方法に乗り換えることを模索した(?)
  • NuGetによるライブラリの管理が複雑化しそうなので、これを統合したい。

で、.NET Coreのビルド制御には「dotnetコマンド」を使うので、こいつが直接JSONを解析すればいいのでは? と言う発想から生まれたような感じがしています(主観)。しかし、皆さんご存知のようにRTM直前になって急にproject.json方式は放棄すると言い出した… あまりに反対が多かったからと言う事のようです。

ですが、結局対策はRTMに間に合わず、.NET Core 1.0ではproject.jsonを使うという仕様で見切り発車。で、どうなったかと言うと、MSBuildをオープンソース化してこれをマルチプラットフォーム対応させるように移植して、*.csprojを復活させるという、初めからやれば順当な手段を採用することになりました。

# ここでもプロプラエタリ技術がOSSに振り回されている、という見方も出来る…
# (否定的文だけど、私的にはむしろ歓迎)

MSBuildの移植は結構問題もあったようですが、現在は.NET Standard 2.0の規約に合わせて(つまりVS2017のリリース?)使えるようにしようとしているようです。すると、.NET Core 1.0で使えたproject.jsonは、さっそく.NET Standard 2.0で非推奨となると。まあ、死ぬなら早い方が良いですね、どうせまだ本気で.NET Core使っているところは少ないだろうし(こなみ

それに、MSBuildが復活することで、これまでMSBuildスクリプトで使えたテクニックが再び使用可能になるのは大きいです。おまけにNuGetのどうしようもないアレな仕様も、とうとう完全にMSBuildに統合され、さんざん煮え湯を飲まされてきた仕様が一気に解決に向かいそうです。例えば:

  • NuGetパッケージを導入すると*.csprojを書き換えたりしますが、これが失敗して*.csprojが壊れたりおかしなことになったり。
  • NuGetパッケージの位置を示すパスがバージョンの更新でなぜか正しいパスを示さなくなってビルドに失敗するとか。
  • 古いパッケージの定義がゴミとして残り続けたりとか。
  • 古いNuGetを想定しているパッケージが、PowerShellで無理やり構成書き換えてて壊れるとか。

振り上げた拳の着地点に毎回困ったりしたのですが、NuGet 4.0と新しいMSBuildでかなり解消しそうです。MSBuild自体どうなのかという感もあるにはあるのですが、カオスな状態を一度はきちんとした着地点に落として、それから次に進んでもいい内容だと思います。


総評

.NET Core 1.0は決して順調な仕上がりではありませんでした。ただ、楽観視はしています。何故なら、OSSプロジェクトで進行することにより、開発プロセスが多くの人の目にとまり、明らかにおかしな方向に行くことに対してフィードバック(ツッコミ)が早く入れられるようになった事が大きいです。project.jsonのような過ちへの対処も、いつまでも汚い手法で姑息に対処し続けるのではなく、可能な方法で最善な手法は何か、と言う事に、多くの頭脳が取り組んでいると思います。

従来のWindows周りの開発では「変わらない」と言う事を主軸に、一種の安心感を持ってプラットフォームを採用してきたと思います。これが未来永劫うまくいくという確信がある話なら、全く問題ない、むしろ安定していて欲しいと願うばかりですが、残念ながら現実はそうじゃない。であれば、変化に追従できるような柔軟さを、.NET自身も、技術者の我々も、持ち続けられるようにしたいと思います。

来年は.NET Standard 2.0のリリースとともに.NET Coreの新しいバージョンもリリースされます。バラバラになっている様々な要素が次第に近寄ってくることになり、ようやくシナジーが生まれ始める年になるんじゃないかなと楽しみです。

.NETの新たな幕開けに期待して。

About Expandable F# Compiler project – F# Advent Calendar 2016 (Japan)

christmas_zangyou_manこの記事は、「F# Advent Calendar 2016」の23日目の記事です。やっばいギリギリ。

英語版はこちら (For English post: https://www.kekyo.net/2016/12/23/6305)

前日は、ぜくるさんのTypeProviderのお話です。

今私は、「F#のコンパイラの代替え品」を作っています。とは言っても、一からF#のコンパイラを自作できるような技量はあるはずがなく、「F# Compiler Service」をバックエンドとして使っています。

F# Compiler Serviceは、C#でいう所のRoslynに相当して、F#ソースコードをパースして構文木(Abstract Syntax Tree)を取得し、さらにそれを使って実際にコンパイル(つまりMSILを生成してPEを出力する)ところまでをプログラマブルに実行できるライブラリです(他にも、fsiに相当するInteractive interpreterのエンジンも持っています)。

何故、代替えとなるコンパイラを作ろうとしているのかとか、背景についての紹介をしたいと思います。


Expandable F# Compiler project (fscx)

fscx_512Expandable F# Compiler (fscx)は、GitHubで公開しています。ちなみに現在のバージョンは0.6.14、まだ正式リリースには至っていません。仕様は予告なく変更される可能性があります :)

fscxは、標準のF#コンパイラ(fsc.exe)の代わりに使用します。目的とかポイントは以下の通りです:

  • コンパイル時に「ソースコードの自動変形」を可能にしたい – これは、ソースコードのテキストベースの置換(例えば正規表現などを使って)するのではなく、F# Compiler Serviceで構文木を取得した後、その構文木に直接手を入れて変形させることを目指しています。テキストベースの置換では、明らかに不正な変形を行ってしまう危険性がありますが、構文木ベースであればそのような問題は発生しません(実際には非常に難しいのですが)。
  • この自動変形を行う処理を「フィルタライブラリ」と呼び、フィルタライブラリは自由に拡張出来るようにする – 例えば、当初想定しているのは、目的となるメソッドの呼び出しコードの前後に、自動的に追加コードを(構文木ベースで)挿入するフィルタライブラリです。しかし、これに限らず、様々な変形をプラガブルに実現できるようにします。
  • 導入が簡単である事 – コンパイラパスの手動構成やVSIXではなく、「NuGetパッケージ」として公開し、NuGetインストールするだけでfscxを使用するようにできる事。これは、以前「NuGetでビルドプロセスに介入するパッケージを作る」で書いた通り、MSBuildのスクリプトを書くことで実現できました。

使用者から見た構造

slide1fscxの使用者は、fscx自体を直接扱うわけではありません。目的別に作られた「フィルタライブラリ」がNuGetで導入できるようになるため、これを導入することで、依存関係によりfscxが導入されて、標準のfsc.exeを置き換えます。

この図では、「sample-filter.nupkg」というNuGetのパッケージがあり、これを自分のF#プロジェクトに導入すると、fscx(実際には”FSharp.Expandable.Compiler.Build”)も一緒に導入され、fsc.exeではなく、fscxを使ってコンパイルを行うようになります。そして、sample-filter.nupkgに含まれるフィルタライブラリを使って構文木を変形し、バイナリを生成します。

例えば、以下のようなコードを書いていたとします:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    System.String.Format("{0} = {1}", a + b, c)

もし、sample-filterが「関数(メソッド)の呼び出し時にログ出力を挿入する」フィルタライブラリだったとすると:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    // 以下のコードが自動的に挿入される
    System.Diagnostics.Trace.WriteLine("Log: format {0} {1} {2}", a, b, c)
    System.String.Format("{0} = {1}", a + b, c)

のような変形を「コンパイル時」に実現します。もちろん、ソースコードファイル自体に一切変更は加えません。そして、使用者はsample-filter.nupkgを導入するだけでこれが実現できるようになります。

使用者がフィルタの詳細に関与することなく、フィルタパッケージの導入だけで実現する – このようなツールセットが透過的に振る舞う必要があると言う点で、非常に重要だと考えています。

フィルタライブラリ開発者から見た構造

slide2フィルタライブラリ側は、やや複雑です。

まず、フィルタ処理はF# Compiler Serviceの構文木を使うため、必ずF# Compiler Serviceへの参照が必要になります。また、fscxの中核となるライブラリも参照が必要です。fscxは”FSharp.Expandable.Compiler.Core”で公開されているので、自分のフィルタライブラリプロジェクトに導入しておきます。

そして、実際にフィルタコードを書けばよいのです…が…


フィルタコードの実装手段

フィルタライブラリを作る際に、2通りの方法を選択することが出来ます:

  • 構文木に対して、レガシーなビジターパターンを使う – これは、.NET LINQで使われている「ExpressionVisitorクラス」と同じようなクラスを使って、いわゆるOOPベースのビジターパターンで構文木の変形を行う方法です。なぜExpressionVisitorクラスをそのまま使わないのか?と思うかもしれません。これはF# Compiler ServiceやRoslynを使ったことがある方ならわかると思いますが、構文木の構造は言語毎に大きく異なるのです。同じRoslynでも、実はC#とVisual Basicが似て異なる構文木クラス群を使うように、F#も全く異なる構文木型を使います。つまり、既存のVisitorクラスはどれも流用出来ないため、専用の基底Visitorクラスを用意しています。
  • F# Compiler Serviceは、ビジターパターンを適用できる標準的なインフラを用意していません – 何故なら、関数プログラミングとパターンマッチングを使うと、わざわざ基底Visitorクラスのようなものを用意しなくても、簡単にビジターパターンを適用できるからです。とは言っても、再帰的な探索のうち、構文木のリストなどは式で書くと煩雑になりがちです。そこで、関数プログラミングスタイルで使用できる、再帰探索の実装を簡略化可能な補助ライブラリを用意しました。

例えば、基底Visitorクラスを使うと、以下のようにコードを書くことが出来ます:

type InsertLoggingVisitor() =
  inherit AstInheritableVisitor()

  override __.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range) =
    // DEBUG
    printfn "%A" operator
    base.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range)

  override this.VisitExpr_App(context, exprAtomicFlag, isInfix, funcExpr, argExpr, range) =
    match funcExpr with
      // ...

関数ビジターパターンのための補助ライブラリを使った場合:

let outerVisitor
   (defaultVisitor: (NoContext * SynExpr -> SynExpr),
    context: NoContext,  // (Non custom context type)
    expr: SynExpr) : SynExpr option =

  match expr with
  | SynExpr.Quote(operator, _, _, _, _) ->
    // DEBUG
    printfn "%A" operator
    None  // (None is default visiting)

  | SynExpr.App(exprAtomicFlag, isInfix, funcExpr, argExpr, range) ->
    match funcExpr with
      // ...

  | _ ->
    None  // (None is default visiting)

どちらが望ましいかは、一長一短あります。基底Visitorクラスを使う場合は、すべての構文木ノードをビジターの対象に出来ますが、ネストした構文木の解析は煩雑です。関数ビジターパターンは解析の自由度が高く、パターンマッチングを使用して容易に構文木を特定できますが、ビジターの対象はSynExprのみです。

これらの補助ライブラリは、fscx自身に直接依存しないため、「FSharp.Compiler.Service.Visitors」という独立したパッケージにしてあります。そのため、fscxを使わないとしても、このライブラリだけで、F# Compiler Serviceのビジターパターンの実装に流用することもできます。

# このライブラリは0.7.1で大体固まった感があるので、正式リリースまで大きく変わらないと思います。


構文木変形の難しさ

構文木の変形には、一種のロマンがあります(たぶん ;)

しかし、実際には「死ぬほど大変」です。この記事を読んでいる人は、おそらくF# Compiler ServiceやRoslynを日常的に触っていると思う :) ので、それがいかに大変かおわかりいただけると思います。LINQのExpressionTreeも大概ですが、あれが更に複雑になったものと思ってもらえれば良いと思います。

# なお、ExpressionTreeについては、Advent LINQの後半の記事や、Final LINQ Extensions IIIが参考になると思います。

前述のsample-filterで示したような変形を実現するには、狙った場所に対応する構文木がどのような構造で現れ、それをどのように変形すれば目的を達成できるのかを、慎重に検討しなければなりません。

構文木は再帰構造で定義されます。そのため、いい加減な判定を行うと、意図しない箇所を変形してしまう可能性があります。また、ソースコード上では些細な違いでも、構文木上ではドラスティックに変わってしまったりすることもあります。

例えば、以下のようなコードを考えます:

// 一般的な.NETのメソッド(タプル形式)
let output1 (a: int, b: string, c: double) =
  System.Console.WriteLine("output1: {0}:{1}:{2}", a, b, c)

// F#関数(カリー化可能形式)
let output2 (a: int) (b: string) (c: double) =
  System.Console.WriteLine("output2: {0}:{1}:{2}", a, b, c)

output1は、典型的な.NET標準のメソッドと呼べるでしょう。しかし、output2はF#らしい「関数」です。このoutput2を呼び出すコードを書いたとします:

// 一般的な.NETのメソッド呼び出し(タプル形式)
// (int * string * double) -> unit
output1 (123, "ABC", 456.789)

// F#関数の呼び出し(カリー化可能形式)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

.NETメソッド形式は、以下のF#関数と区別して「タプル形式」と呼んでいます。構文木上でも、引数部分が「タプル」のように見え、一つのタプル値を引数に渡しているかのように解釈されます。F#関数は「カリー化可能形式」です。カリー化可能の場合の関数呼び出しは、部分適用された関数が連続的に適用されていくような感じに解釈されます。そして、構文木上もネストされた関数呼び出し(Appノード)で表現されます。

構文木が可視化できないと、ここら辺の検討が絶望的に大変なので、小さなツールですが構文木をF#コードっぽくダンプするツールを作りました(可視化が目的だったので、完成度は高くありません)。これを使うと、以下のような出力が得られます:

// 以下がoutput1の呼び出し
Ast.SynExpr.App( (* expr1 *)          // App - output1(...)
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.Ident( (* funcExpr *)
    output1 (* Item *)),
  Ast.SynExpr.Paren( (* argExpr *)    // Paren --> Tuple : タプル形式の引数
    Ast.SynExpr.Tuple( (* expr *)
      [ (* exprs *)                   // タプルの中身をリストで表現
        Ast.SynExpr.Const( (* [0] *)
          Ast.SynConst.Int32( (* constant *)
            123 (* Item *)));
        Ast.SynExpr.Const( (* [1] *)
          Ast.SynConst.String( (* constant *)
            "ABC" (* text *)));
        Ast.SynExpr.Const( (* [2] *)
          Ast.SynConst.Double( (* constant *)
            456.789 (* Item *)))],
      [ (* commaRanges *)]),
    range>.Some( (* rightParenRange *)))),

// 以下がoutput2の呼び出し
Ast.SynExpr.App( (* expr2 *)          // App - 456.789の適用
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.App( (* funcExpr *)     // App - "ABC"の適用
    ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
    false (* isInfix *),
    Ast.SynExpr.App( (* funcExpr *)   // App - output2に対して123の適用
      ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
      false (* isInfix *),
      Ast.SynExpr.Ident( (* funcExpr *)
        output2 (* Item *)),
      Ast.SynExpr.Const( (* argExpr *)
        Ast.SynConst.Int32( (* constant *)
          123 (* Item *)))),
    Ast.SynExpr.Const( (* argExpr *)
      Ast.SynConst.String( (* constant *)
        "ABC" (* text *)))),
  Ast.SynExpr.Const( (* argExpr *)
    Ast.SynConst.Double( (* constant *)
      456.789 (* Item *))))),

出力が冗長で見難いのですが、output1とoutput2では、引数がリストで表現されているか、ネストされた関数として表現されているかで全く構造が異なることが分かります。カリー化可能形式とは、実は以下のような呼び出しであることが分かります:

// F#関数の呼び出し(カリー化可能形式)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

// 実はこういう事:
((output2 123) "ABC") 456.789

// さらに分解すると:
let f1 = output2 123  // f1は高階関数
let f2 = f1 "ABC"     // f2は高階関数
f2 456.789

ソースコードの些細な違いが、構文木上で大幅に異なる表現となる場合があるため、ビジターパターンで探索する場合に注意が必要です。実際のところ非常に難しい…

# 余談ですが、構文木の解析が非常に大変だった経験から、先日のNGK2016BのLTネタ: 「You will be assimilated. Resistance is futile.」が生み出された、みたいな背景があります。Roslynなので、fscxとは全然関係ないのですが :)


フィルタのミドルウェア

このように、フィルタライブラリの独自実装自体は、構文木の扱いが非常に複雑であるため、特に想定されるシナリオのために、ミドルウェアとなる補助ライブラリ”FSharp.Expandable.Compiler.Aspect”を用意しています。

これは、「AOP(アスペクト志向パラダイム: Aspect-Oriented-Paradigm)」で知られているような、任意の処理の直前と直後に、安全に処理を挿入できる機構を持った手法と同じことを、fscx上で実現させるための補助ライブラリです。

フィルタライブラリを開発する際に、一からフィルタライブラリを実装するのではなく、この補助ライブラリを使うと、fsprojのプロパティに指定した情報と「アスペクトクラス」を指定して、構文木の操作を一切実装することなく、安全にAOPを実現できます。

例えば、以下のようなコードを用意しておきます:

// コンテキストクラス(関数を抜ける際に実行するメソッドを定義)
type SampleAspectContext internal (body: string) =
  // Finish aspect (trigger are leaved method with return value)
  member __.Leave(result: 'T) : 'T =
    Console.WriteLine("Leave: " + body)
    result
  // Finish aspect (trigger are leaved method with exception)
  member __.Caught(ex: exn) : unit =
    Console.WriteLine("Caught: " + body + ": " + ex.ToString())

// 関数を呼び出す際に呼び出されるメソッドを定義
type SampleAspect() =
  // Start aspect (trigger are entered method)
  static member Enter(methodName: string, fileName: string, line: int, column: int, args: obj[]) =
    let body =
      String.Format
        ("{0}({1}, {2}): {3}({4})",
         fileName,
         line,
         column,
         methodName,
         String.Join(", ", args |> Seq.map (sprintf "%A")))
    Console.WriteLine("Enter: " + body)
    new SampleAspectContext(body)

このアスペクトコードは、関数の入り口と出口で、自動的にコンソールにログを出力します。SampleAspect.Enterが関数の入り口で呼び出され、関数名やソースコードの位置、そして渡された引数を入手できます。この情報をもとに、ログを出力しています。

また、Enterが返すクラスのインスタンスが「アスペクトコンテキスト」として扱われ、LeaveまたはCaughtが関数の出口で呼び出されます。命名の通り、正常に抜ける場合にはLeaveが、例外で抜ける場合にはCaughtが呼び出されます。それぞれ、戻り値と例外のインスタンスが引数で指定されるので、これを元にログを出力しています。

これらのクラスや関数とその引数は、完全なダックタイピングです。特定のクラスやインターフェイスの実装は不要で認識されます。本当はインターフェイスで縛ったりしたいのですが、そうすると余計なアセンブリ参照が増え、最終的にはNuGetのパッケージングで非常に苦労することになるからです。

これで、アスペクトを「SampleAspect」として定義できたので、FSharp.Expandable.Compiler.Aspectに認識させるために、以下のコードを定義しておきます:

// SampleAspectクラスをアスペクトとして使用するフィルタを定義する
[<Sealed>]
type SampleAspectLoggerDeclaration() =
  // 依存関係を増やさないために、クラス名を文字列で指定
  inherit DeclareFscxInjectAspectVisitor("SampleAspectLogger.SampleAspect")

アスペクトコード自体とこの定義型の実装は、C#でも記述できるように、型の要求を緩くしてあります。

この定義がフィルタライブラリに含まれていると、fscxによって以下の操作が行われます:

  • 対象の関数の入り口に、SampleAspect.Enterを呼び出すコードを、構文木の挿入で実現する。
  • 対象の関数の出口に、SampleAspectContext.LeaveまたはCaughtを呼び出すコードを、構文木の挿入で実現する。Leaveは正常終了した際の戻り値を、Caughtはtry-withブロックによってキャッチした例外を伴って呼び出す。

このようなコードが:

let f11 (a: int, b: string, c: int) =
  output1(a + c, b, 123.456)

fscxによってこのように変形されます:

let f11 (a: int, b: string, c: int) =
  let __arg_0 = a + c
  let __arg_1 = b
  let __arg_2 = 123.456
  let __context =
    SampleAspectLogger.SampleAspect.Enter
      ("f11", "SampleCode.fs", 2, 3, [|__arg_0, __arg_1, __arg_2|])
  try
    __context.Leave(output1(__arg_0, __arg_1, __arg_2))
  with
  | ex ->
    __context.Caught(ex)
    reraise()

ここでは、タプル形式のメソッドを呼び出していますが、当然カリー化可能形式にも対応しています。

アスペクトクラス(SampleAspect, SampleAspectContext)と、アスペクトクラスを定義する型(SampleAspectLoggerDeclaration)以外に、構文木を操作するためのコードは一切不要であることに注目してください。

なお、現在FSharp.Expandable.Compiler.Aspectは実装を行っている最中で、特にどの関数を変形のターゲットとするかという指定方法を詰めている所です。


まとめ

このように、fscxはこれを土台として、FSharp.Expandable.Compiler.Aspectを使えばAOPを簡単に実現させることも出来、それ以上の変形も(難易度は高いですが)自在に可能でかつ、これをフィルタライブラリとして配布・再利用可能にします。

例えば、F#でOOPをやるのは非常にダルい説があります。何故なら関数プログラミングスタイルになれると、クラスとかインターフェイスを定義したり使ったりするのが面倒なためです(人によるかも知れませんが)。

そのようなわけで、F#でUIプログラミングをやるために、INotifyPropertyChangedのハンドリングとかも面倒この上ないのですが、fscxがあれば、インターフェイスの自動実装を行う変形フィルタライブラリを作ったりとかも出来るのです(しかも、コンパイル時に変形するのでランタイムコストがありません)。そういうライブラリがあれば、普通にコードを書いて、フィルタライブラリのパッケージをNuGetで導入するだけで、即自動実装させることが出来ます。

正式リリースまでには、まだいくつか詰める必要があるのですが、着地点は見えてきた感じです。

謝辞: 株式会社オンザロードさんと、ぶれいすさんに、このプロジェクトの協力を頂いています。ありがとうございます。

今年も残すところあとわずかですね。それではまた!

About Expandable F# Compiler project – F# Advent Calendar in English 2016

This post is 23th “F# Advent Calendar in English 2016.”

For Japanese post: (https://www.kekyo.net/2016/12/23/6263)

Hi, all F#er!! My name is Kouji Matsui, I live in Japan and I’m architect/developer.

I’m interesting to C#, LINQ, F#, IL and metaprogramming. I know about what/how asynchronosly execution between user mode .NET handler and kernel-mode driver DPC/hardware interruption. Titled Microsoft MVP for VS and DevTech. Holds CSM, CSPO. And bicycle hobby rider. I’m already released “F# FusionTasks.”

If you want contact to me, please use twitter: @kekyo2 or gitter: kekyo/public

# Sorry I have poor English, this post is very challenging for me :)

Currently, I’m developing an alternate compiler for F#. This is not meaning I’m compiler specialist :) This project uses the “F# Compiler Service.”

F# Compiler Service is likely “Roslyn” under C#. That’s parsing for F# source code and output AST (Abstract syntax tree). Then can compile and output to executable binary from ASTs. Another usage, F# Compiler Service have capability of interactive interpreter for F# (backend for fsi).

I try to write this project’s background: Why do I develop alternate compiler for F#?


Expandable F# Compiler project (fscx)

fscx_512“Expandable F# Compiler (fscx)” is an open source project on GitHub. Current version is 0.6.14, not reached official release. This post’s details will be implicitly changing :)

We can replace standard F# compiler (fsc.exe) to fscx. fscx’s goals are:

  • I want to auto translate the F# source codes – We can translate source codes by text-based (For example use by regular expressions). But this project not uses text-based translate. I’m designing for use F# Compiler Service and get ASTs, then translate ASTs directly. Because try with text-based translates then broken codes easier. AST based approach will not break and safe (But it’s very difficult for me, see below section :)
  • This auto translate library called “Filter library” and I want to can be extendable and expandable for non limitation – For example, a filter library can handles for insert another code fragments to before/after method body (by uses AST translate. likely AOP) Sure this is example, we can handle ANYTHING by AST translations.
  • Better easier installation – NOT customize for compiler executable path, NOT require install VSIX plugin. fscx is packaging by NuGet, we can use fscx on target F# project, require only fscx install via NuGet, simply then enables fscx. This senario already examine how to effect for building system by MSBuild (Post: “How to intercept for building system by NuGet,” In Japanese)

Structure viewed from the filter library user

slide1For fscx users, fscx uses not directly. We are implicitly using for fscx depended from the “Filter library” via NuGet. We can select filter library from the purpose. If installed fscx then auto replace the F# compiler (fsc.exe) to fscx.

For example, this slide identicate the box “sample-filter.nupkg.” This is packaged filter library on NuGet. We download and install this package via NuGet. NuGet client auto install depended packages include fscx named “FSharp.Expandable.Compiler.Build.” And ready for auto translate ASTs by filter library and fscx. Then compile and output translated binary.

Example in this code fragment:

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    System.String.Format("{0} = {1}", a + b, c)

If “sample-filter.nupkg” contains AST translator implementation for: “Insert logging codes on function entry point:”

module SampleModule =
  let format (a: int) (b: int) (c: string) =
    // Auto inserted logging code below:
    System.Diagnostics.Trace.WriteLine("Log: format {0} {1} {2}", a, b, c)
    System.String.Format("{0} = {1}", a + b, c)

This translation executes at compile time. Sure no text-based translation. Then we can effect this translation for only installing filter library via NuGet.

Very important at this point: Users don’t know about filter implementation details. I’m designing for fscx to do in transparents.

Structure viewed from the filter library developer

slide2More complex for filter library side.

Filter library must use F# Compiler Service’s AST types, so library has assembly reference to F# Compiler Service and the fscx core library too. fscx core library package is naming for “FSharp.Expandable.Compiler.Core.” This package install to filter library project via NuGet.

Ready for can develop filter implemens, but…


How to implemens filter library

You can choice “Visitor patterns” on the helper library:

  • The legacy OOP based visitor pattern – This is most known technics for recursive tree structure on OOP. This approach likely .NET LINQ’s “ExpressionVisitor class,” named “AstInheritableVisitor class.” Why do we use AstInheritableVisitor instead ExpressionVisitor? Because F#, C# or any other languages (include .NET Expression-tree) different expression nodes smaller and larger. (You are already known things if you are using for F# Compiler Service or/and Roslyn :)
  • F# Compiler Service not provides standard visitor pattern infrastructures – Because we can implement visitor pattern with both the functional programming (FP) technics and pattern matching. But code fragments must has frequently recursive calls. So I developed visitor pattern helper functions on FP-based contains recursive callers.

For example, How to do using for OOP based visitor class:

type InsertLoggingVisitor() =
  inherit AstInheritableVisitor()

  override __.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range) =
    // DEBUG
    printfn "%A" operator
    base.VisitExpr_Quote(context, operator, isRaw, quoteSynExpr, isFromQueryExpression, range)

  override this.VisitExpr_App(context, exprAtomicFlag, isInfix, funcExpr, argExpr, range) =
    match funcExpr with
      // ...

How to do using for FP based helper functions:

let outerVisitor
   (defaultVisitor: (NoContext * SynExpr -> SynExpr),
    context: NoContext,  // (Non custom context type)
    expr: SynExpr) : SynExpr option =

  match expr with
  | SynExpr.Quote(operator, _, _, _, _) ->
    // DEBUG
    printfn "%A" operator
    None  // (None is default visiting)

  | SynExpr.App(exprAtomicFlag, isInfix, funcExpr, argExpr, range) ->
    match funcExpr with
      // ...

  | _ ->
    None  // (None is default visiting)

Which choice of better technics? There is both merits and demerits which is desirable. If you use OOP based, you can receive all AST nodes at visit timing but more complex and difficult for how to identicates structurable-nested AST nodes. If you use FP based, can easier structurable-nested AST nodes (by pattern matching) but you can receive only “SynExpr” nodes at visit timing.

These helpers are totally not depended on fscx. I separate visiter pattern helpers from fscx and construct standalone package for “FSharp.Compiler.Service.Visitors.” If you are thinking about how to apply visitor pattern for your application, try to use this package.

# This package fixed for the most part in version 0.7.1.
# Maybe not change for public declarations.


What difficults for AST translations?

The AST translation code has cool things (Maybe ;)

But AST translation always most difficult technics in real story. You are the man of always designing for F# Compiler Service/Roslyn’s AST everydays :) So you are understand it’s really difficult for meaning and knowning. F# ASTs are likely Expression-tree classes, but translation is difficult more than ExpressionVisitor experience…

# If you want more informations about Expression-tree, read these posts: Second half of Advent LINQ 2013 (In Japanese) and/or “Final LINQ Extensions III” (In Japanese).

We are trying to implement the “sample-filter” above: We need knowns about what realize AST structures and where AST nodes appearance for? And how to translate AST nodes?

AST nodes constructs for recursivility. So if you identicate AST nodes by rough technics, results are broken easier. (OK, F# Compiler Service find invalid AST structures and report errors at compile time. The world maybe safe :)

This example illustlated for how different AST structures on source code by small different:

// The standard .NET method signature (Tupled arguments)
let output1 (a: int, b: string, c: double) =
  System.Console.WriteLine("output1: {0}:{1}:{2}", a, b, c)

// F# function (Curryable arguments)
let output2 (a: int) (b: string) (c: double) =
  System.Console.WriteLine("output2: {0}:{1}:{2}", a, b, c)

“output1” method is most popular standard .NET method. “output2” function is F# friendly the “Function.” If you write to call these members:

// Call the standard .NET method signature (Tupled arguments)
// (int * string * double) -> unit
output1 (123, "ABC", 456.789)

// Call the F# function (Curryable arguments)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

The “Tupled arguments” method different for “Curryable arguments” function. We are finding for differents in AST nodes. Tupled arguments realize for SynExpr.Tuple AST node. But curryable arguments realize for nested SynExpr.App AST nodes.

It’s difficult that different on ASTs. I want to see AST structures. So I make smaller tool for dump AST nodes by F# Compiler Service (This tool for smaller hacks, very rough quality.)

This is output (Only contains interested fragments):

// For output1 method:
Ast.SynExpr.App( (* expr1 *)          // App - output1(...)
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.Ident( (* funcExpr *)
    output1 (* Item *)),
  Ast.SynExpr.Paren( (* argExpr *)    // Paren --> Tuple : Tupled arguments
    Ast.SynExpr.Tuple( (* expr *)
      [ (* exprs *)                   // Arguments declaring by list
        Ast.SynExpr.Const( (* [0] *)
          Ast.SynConst.Int32( (* constant *)
            123 (* Item *)));
        Ast.SynExpr.Const( (* [1] *)
          Ast.SynConst.String( (* constant *)
            "ABC" (* text *)));
        Ast.SynExpr.Const( (* [2] *)
          Ast.SynConst.Double( (* constant *)
            456.789 (* Item *)))],
      [ (* commaRanges *)]),
    range>.Some( (* rightParenRange *)))),

// For output2 function:
Ast.SynExpr.App( (* expr2 *)          // App - Apply 456.789
  ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
  false (* isInfix *),
  Ast.SynExpr.App( (* funcExpr *)     // App - Apply "ABC"
    ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
    false (* isInfix *),
    Ast.SynExpr.App( (* funcExpr *)   // App - Apply 123 for output2
      ExprAtomicFlag.NonAtomic (* exprAtomicFlag *),
      false (* isInfix *),
      Ast.SynExpr.Ident( (* funcExpr *)
        output2 (* Item *)),
      Ast.SynExpr.Const( (* argExpr *)
        Ast.SynConst.Int32( (* constant *)
          123 (* Item *)))),
    Ast.SynExpr.Const( (* argExpr *)
      Ast.SynConst.String( (* constant *)
        "ABC" (* text *)))),
  Ast.SynExpr.Const( (* argExpr *)
    Ast.SynConst.Double( (* constant *)
      456.789 (* Item *))))),

It’s contains too many verbose fragments so ugly (sorry). That means output1’s arguments declarates by list, output2’s arguments declarates by nested “SynExpr.App” (Apply argument).

That is the “Curryable arguments,” means:

// Call the F# function (Curryable arguments)
// int -> string -> double -> unit
output2 123 "ABC" 456.789

// It's meaning for expression:
((output2 123) "ABC") 456.789

// Split expressions:
let f1 = output2 123  // f1 is higher order function
let f2 = f1 "ABC"     // f2 is higher order function
f2 456.789

The AST structure different for smaller on original source codes. So we are sensitive for analyse by visitor pattern. This is just difficult…

# I have an idea from these things. I have a session for “You will be assimilated. Resistance is futile.” on “NGK2016B” LT conference (This is joke session :)


The filter’s middleware

So we can do anything by filter library AND implementation are very hard work. Then I’m thinking about how to do easier it…
I’m trying to design the helper library: “FSharp.Expandable.Compiler.Aspect.”

Will we have what intercept anything (by fscx)? This library is “Middleware” for fscx, we can safe implements for insert custom code between enter and leave function points. Today, we are calling the “AOP (Aspect oriented paradigm)” for this technics.

If we use this helper library by develop filter library in this case, our filter code are simple, safe and more easier.

For example below:

// This is "Aspect context" class
type SampleAspectContext internal (body: string) =
  // Finish aspect (trigger are leaved method with return value)
  member __.Leave(result: 'T) : 'T =
    Console.WriteLine("Leave: " + body)
    result
  // Finish aspect (trigger are leaved method with exception)
  member __.Caught(ex: exn) : unit =
    Console.WriteLine("Caught: " + body + ": " + ex.ToString())

// This is "Aspect" class
type SampleAspect() =
  // Start aspect (trigger are entered method)
  static member Enter(methodName: string, fileName: string, line: int, column: int, args: obj[]) =
    let body =
      String.Format
        ("{0}({1}, {2}): {3}({4})",
         fileName,
         line,
         column,
         methodName,
         String.Join(", ", args |> Seq.map (sprintf "%A")))
    Console.WriteLine("Enter: " + body)
    new SampleAspectContext(body)

This sample codes (Called “Aspect code”) are printing to console for function call message between enter and leave (include caught exception). “SampleAspect.Enter(…)” method is calling from before function call and “SampleAspectContext.Leave(…)” or “Caught(…)”
methods are calling from after function called.

“Enter(…)” method received the method name, source code file name, line/column number and will be calling with arguments. This sample codes prints these information to Console and create instance for “SampleAspectContext” class and return.

“Leave(…)” or “Caught(…)” handles to print leave messages. “Leave(…)” can receive for function called result. “Caught(…)” can receive for exception instance.

These aspect codes are truly “Duck-typed.” fscx will be identicate and not require any base classes and any interfaces. Because if request these types, we have .NET assembly referencing problem and fix this problem for very hard on packaging by NuGet :(

Ready for aspect named “SampleAspect” class. Next step, how to identicate this aspect for “FSharp.Expandable.Compiler.Aspect?” :

// This is declaring for aspect: "SampleAspect" class.
// Class name is free.
[<Sealed>]
type SampleAspectLoggerDeclaration() =
  // Construct with aspect class name. This is duck-typed naming (by string.)
  inherit DeclareFscxInjectAspectVisitor("SampleAspectLogger.SampleAspect")

Totally aspect tips: Aspect codes and this declaration requests loose types, so we can write aspects by C#.

This declaration contains on filter library, fscx can find and translate:

  • If fscx is targeting for a function, fscx insert custom AST nodes before calling function. Call to “SampleAspect.Enter(…)”
  • And fscx insert custom AST nodes after called function. Call to “SampleAspectContext.Leave(…)” or “Caught(…).” Leave with called function result value, Caught with exception instance by (inserted) try-with block.

This sample code fragment:

let f11 (a: int, b: string, c: int) =
  output1(a + c, b, 123.456)

Translate to:

let f11 (a: int, b: string, c: int) =
  let __arg_0 = a + c
  let __arg_1 = b
  let __arg_2 = 123.456
  let __context =
    SampleAspectLogger.SampleAspect.Enter
      ("f11", "SampleCode.fs", 2, 3, [|__arg_0, __arg_1, __arg_2|])
  try
    __context.Leave(output1(__arg_0, __arg_1, __arg_2))
  with
  | ex ->
    __context.Caught(ex)
    reraise()

This sample code declare “f11(…)” by tupled arguments method, sure can translate for function by curryable arguments.

Totally at this point: We have only the aspect classes “SampleAspect, SampleAspectContext” and aspect declaration “SampleAspectLoggerDeclaration”. These are contains no AST translation codes… Oh I made it! :)

Currently, I’m developing “FSharp.Expandable.Compiler.Aspect” now and not finish, because how to set where insert aspects to the target function.


Conclusion

fscx is challenging project for me. This project has several points:

  • fscx is easy-usage (by install NuGet) for any filter users.
  • We can develop any auto translation by ASTs (but very difficult).
  • We can develop easier any AOP based senarios by using fscx’s middleware.
  • All filter libraries can deploy by NuGet packaged.

The myth: We write OOP based codes by F# that’s hassle. Because if we become to FP, a little bit tired for declare many class and/or interface word elements. (It’s myth! don’t make seriously :)

We often write for UI codes, for example how to implement “INotifyPropertyChanged.” fscx is capable for can filtering auto-implement this interface, these senarios capsule to the middleware and running this code by no runtime cost. Finally we can construct ecosystem for filter libraries.

Acknowledgments: Thanks cooperation for On the road corporation and Mr. bleis.

Thank you for reading, welcome any comments! Happy christmas!!

できる!C#で非同期処理(Taskとasync-await)

asyncexception21ヤヴァいタイトル付けてしまった…. ええと、これはAdvent Calendarではありませんが、勢いで書いています(C# ADは既に埋まっていた…)

それには、こんな事情があったのです:

  • 「.NET非同期処理(async-await)を制御する、様々な方法」の記事がコンスタントにPVを稼いでいる
    (その割に、他の非同期関連の記事は読まれない)
  • asyncやawaitキーワードの使い方を度々聞かれる。
  • Task.Run()とか、Task.Factory.StartNew()とか、Task.Start()とか使ってるコードを頻繁に見る(不要なのに)。
  • Task.Wait()とか、Task.Resultとか使ってるコードを頻繁に見る(不要なのに)。
  • いまだにThreadクラス直接使ってるしかもその中でTask.Wait()とか(?!?!)

私のセッションや記事は、「なぜなのか」を深掘りして問題の根本を理解してもらう事を念頭に置いているのですが、さすがにこれだけ頻出してるのは、業界的に非常に問題があるんじゃないだろうかと思いました。特にXamarinや.NET Coreが注目されると、今までC#でコード書いた事が無い、と言う人が、見よう見まねで書き始める時期じゃないかと思います(それで何となく書けてしまうのが、C#の間口の広いところではあるのですが)。

もう一つ、現在、「非同期処理の必要性」のような記事を書いているのですが、まだ完成していないので、もうちょっとライトなやつを書いてさっさと公開した方が良い気がしてきました。

「できる(なんちゃら)」じゃないけど、そういう資料も切り口を丁寧に詰めればありかも知れない…

そういうわけで:

  • C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、以下の事を知っていれば9割がたの問題は回避できる!!
  • ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!
    (知っててやってる人はいいんです。こんな記事見る必要はありません)

章立てを2章だけにして、極限まで絞り込みました。ではどうぞ。

# 補足: F#erの人には説明は不要と理解しております :)
# コードはやや冗長に書いています。


1. 同期処理と非同期処理を対比させてみる

以下のコードは、ウェブサイトから「同期的に」データをダウンロードします。要するに、普通のコードです。WebClientクラスを使い、OpenReadメソッドでStreamを取得して、StreamReaderで文字列として読み取ります(クラス定義などは省略):

using System.IO;
using System.Net;

public static string ReadFromUrl(Uri url)
{
	using (WebClient webClient = new WebClient())
	{
		using (Stream stream = webClient.OpenRead(url))
		{
			TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
			string body = tr.ReadToEnd();
			return body;
		}
	}
}

public static void Download()
{
	Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
	string body = ReadFromUrl(url);
	Console.WriteLine(body);
}

これを非同期処理として変形します。変形した箇所にはコメントを入れておきました。番号の順にやればスムーズに行くと思います:

using System.IO;
using System.Net;
using System.Threading.Tasks;

// Step5: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
// Step6: メソッド内でawaitキーワードを使ったら、戻り値の型はTask<T>型で返さなければならない。
//        T型は本来返すべき戻り値の型。
// Step7: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。
public static async Task<string> ReadFromUrlAsync(Uri url)
{
	using (WebClient webClient = new WebClient())
	{
		// Step1: OpenReadから非同期対応のOpenReadTaskAsyncに変更する。
		// Step2: OpenReadTaskAsyncがTask<Stream>を返すので、awaitする。
		//        awaitすると、Streamが得られる。
		using (Stream stream = await webClient.OpenReadTaskAsync(url))
		{
			TextReader tr = new StreamReader(stream, Encoding.UTF8, true);
			// Step3: ReadToEndから非同期対応のReadToEndAsyncに変更する。
			// Step4: ReadToEndAsyncがTask<string>を返すので、awaitする。
			//        awaitすると、stringが得られる。
			string body = await tr.ReadToEndAsync();
			return body;
		}
	}
}

// Step10: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
// Step11: メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。
//         ここでは元々返す値が無かった(void)なので、非ジェネリックなTask型を返す。
// Step12: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する(慣例なので必須ではない)。
public static async Task DownloadAsync()
{
	Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
	// Step8: ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。
	// Step9: ReadFromUrlAsyncがTask<string>を返すので、awaitする。
	//        awaitすると、stringが得られる。
	string body = await ReadFromUrlAsync(url);
	Console.WriteLine(body);
}

# 補足: HttpClientではなくWebClientを使ったのは、対比させやすいためです。非同期前提で書くなら、始めからHttpClientを使うと良いでしょう。WebClientの代わりにHttpClientで同じ処理を書いてみるのは、良い練習になると思います。

asyncpractice1出来るだけわかりやすくなるようにコメントを入れたので、「ひたすらめんどくさそう」に見えますが、よく見れば大した作業ではない筈です。ReadFromUrlとDownloadのそれぞれのメソッドが、どのように変形されるのかを見てください。そうすると何となく法則が見えてくると思います。

また、この作業で、2つのメソッド「ReadFromUrl」と「Download」が、両方とも非同期メソッド化された事にも注意してください。

asyncpractice2つまり、特別な理由がない限り、非同期処理は呼び出し元から末端まで、「全て非同期メソッドで実装する必要がある」、と言う事です。ここで示した変形を行うと、連鎖的に、すべてのメソッドが非同期化されます。

これが核心です。以下にまとめます:

  • 非同期対応メソッドが存在する場合は、そのメソッドを呼び出すように切り替えます。上記では、「OpenRead」「OpenReadTaskAsync」に変えました。目的の非同期メソッドは、「TaskやTask<T>を返すメソッドがあるかどうか」や「~Async」と命名されているメソッドがあるかどうか、を目印に探すと良いと思います。また、この理由のために、メソッド名を「~Async」と命名する慣例が生きてきます。
  • TaskやTask<T>は、そのままでは戻り値T型になりません。これらの値は「await」キーワードを使う事で、戻り値を得る事が出来ます。awaitを実行している時、つまりそれが「非同期的に結果を待機している」と言う事です。
  • 自分で定義したメソッド内で「await」キーワードを使った場合は、メソッド先頭に「async」キーワードを追加する必要があります。「async」キーワードの効能はそれだけです!!! 以下のTask型云々は関係ありません。
  • 非同期メソッドの戻り値は、Task型、Task<T>型、または「void」とする必要がありますが、voidは殆ど使用しません(次章)。
  • 同期メソッドの戻り値の型がstringなら、Task<string>のように、ジェネリックなTask<T>型を使います。同期メソッドの戻り値がない(void)場合は、Task型(非ジェネリック)を返します。

これだけです。これだけですよ奥さん!!! 英語の動詞変形みたいな話です。この法則だけ覚えていれば、一旦同期的にコードを書いてみて、それから非同期処理に置き換えるという「練習」が出来ます。しばらくやっていれば、最初から非同期で全部書けるようになります。

ここまで、Task.Run()も、Task.Factory.StartNew()も、Task.Start()も、Task.Wait()も、Task.Resultも出ませんでしたね? これらを使う事はまずありません(断言)。

それでも、という方、では第2章へどうぞ。


2. 非同期操作のルーツ

非同期操作が考案された由来、とかそういう話ではなく、「どこからが非同期処理として扱われるか」あるいは「どこからを非同期処理として扱うのか」、という話です:

// これはWPFのウインドウ実装です(但し、WPFであることはあまり重要ではありません):
public partial class MainWindow : Window
{
    // 参考:
    //   Window.Loadedイベントの定義:
    //     public event RoutedEventHandler Loaded;
    //   RoutedEventHandlerデリゲートの定義:
    //     public delegate void RoutedEventHandler(object sender, RoutedEventArgs e);

    // コンストラクタ
    public MainWindow()
    {
        InitializeComponent();

        // ウインドウ表示開始時のイベントをフックする
        this.Loaded += MainWindow_Loaded; 
    }

    // ウインドウが表示されると呼び出される
    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        // 第1章で実装したReadFromUrlを呼び出して、テキストボックスに表示する
        Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
        string body = ReadFromUrl(url);
        textBox.Text = body;
    }
}

# XAMLは省略します。TextBoxコントロールに”textBox”という名前を付けて定義してある前提です(また、本当はデータバインディングでやるべきです)。

このコードだと、ダウンロードが完了するまで、UIが固まってしまいますね。では、これを非同期処理に対応させます:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded; 
    }

    // Step3: メソッド内でawaitキーワードを使ったら、メソッドの先頭でasyncを加えなければならない。
    // Step4: メソッド内でawaitキーワードを使ったら、戻り値の型はTask型で返さなければならない。
    //        しかし、Loadedイベントのシグネチャは変更できない。従って「void」のままとする。
    // Step5: 非同期処理対応メソッドは、「慣例」で「~Async」と命名する。
    //        しかし、値を返さないためawait忘れの可能性はない事と、あくまで慣例であるため
    //        ここでは「~Async」と命名しない。
    private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        Uri url = new Uri("https://github.com/Microsoft/dotnet/blob/master/README.md");
        // Step1: ReadFromUrlから非同期対応のReadFromUrlAsyncに変更する。
        // Step2: ReadFromUrlAsyncがTask<string>を返すので、awaitする。
        //        awaitすると、stringが得られる。
        string body = await ReadFromUrlAsync(url);
        textBox.Text = body;
    }
}

asyncpractice31MainWindow_Loadedメソッドと、第1章のDownloadAsyncメソッドとの違いを確かめてください。Loadedイベントのシグネチャは「RoutedEventHandler」デリゲートであり、「delegate void RoutedEventHandler(object, RoutedEventArgs)」です。つまり、戻り値を返さない「void」であることが決められています。

非同期メソッドはTask型かTask<T>型を返すべきですが、ここでは型が合いません。値を返さない場合に限り、このような非同期メソッドを「void」で定義します。

ポイント:

  • 戻り値を返さず「void」と定義するメソッドは、イベントをフックしたハンドラメソッドのような場合しかありません。戻り値をvoidとした場合、呼び出し元は非同期処理の結果を追跡できず、処理結果を感知しません。
  • 逆に考えると、Loadedイベントの場合、そもそもイベントの呼び出し元は非同期処理の結果など気にしていません(voidなので)。だから、voidとしても良い、と解釈することもできます。
  • 「Task」を返そうが「void」を返そうが、「await」キーワードを使ったので「async」キーワードを追加する必要があります。

(何らかの)処理の開始位置が、このハンドラメソッドにある事にも注目してください。このように、呼び出し元(WPFのLoadedイベント処理)が、呼び出し先(MainWindow_Loadedメソッド)の事について感知していないような場合にのみ、呼び出し先の非同期メソッドの実装方法を考える必要があります。

コンソールアプリケーションの非同期処理ではどうでしょうか:

// コンソールアプリケーションのメインエントリポイント
// awaitを使っているのでasyncを追加する(?)
public static async void Main(string[] args)
{
    // Task型が返されるので、この処理を待機するためawaitする(?)
    await DownloadAsync();
}

asyncpractice4第1章のDownloadAsyncメソッドを呼び出してみました。もし、この呼び出しをawaitで待機する場合、Mainメソッドの先頭にasyncと付ける必要があります。このコードは問題なくコンパイルできますが、実行すると(殆どの場合)ダウンロードが完了することなくプログラムが終了します。

Mainメソッドは、DownloadAsyncメソッドの非同期処理をawaitで待機します。しかしこれは「非同期的に待機」しているので、「Mainメソッドの処理(メインスレッド)は継続的に実行されている」のです。すると、メインスレッドはそのままMainメソッドを抜けてしまい、一瞬でプログラムが終了してしまいます。

したがって:

public static void Main(string[] args)
{
    // Step1: Task.Wait()を使わざるを得ない。
    Task task = DownloadAsync();
    task.Wait();
}

asyncpractice5このような場合にのみ、Task.Wait() 又は Task.Resultで待機する必要があります。GUIアプリケーション(WPF/UWP/Xamarinなど)を実装している皆さんには、まったく縁のない話なので、このことは忘れても構いません。GUIアプリケーションでTask.Wait() や Task.Resultを使うとデッドロックを起こします。

大事な事なのでもう一度: 「デッドロックします。使ってはいけません」

今Task.Wait() や Task.Resultを呼び出してうまく動いていますか? そうですか、いつかTaskに「後ろから刺される」覚悟はしておいた方が良いでしょう…
awaitで待機するように記述する事を「大前提」として、そうするにはどうやって書いたらいいかを考えると良いと思います。


まとめ

これだけです。とりあえずこれだけで、「マトモ」な非同期処理コードを書くことが出来るはずです。繰り返しますが:

  • C#の非同期処理(Taskとasync-await)を使ってコードを書くなら、これらの事を知っていれば9割がたの問題は回避できる!!
  • ここで示した方法から外れたことをしようとしている場合は、殆ど間違いなく誤った記述をしようとしているので、もう一度何をしようとしているのか整理しよう!!

これを読んで新たに疑問が湧いてくるかもしれません(例えば、どうしてデッドロックするのか、voidの非同期メソッドの結果はどうなるのか、など)。それは良い事です。そのような場合は、より深く解説した記事を読んでみてください。

それでは!

回転寿司をVisual Studioに降臨させる – Visual Studio Advent Calendar 2016

この記事は、Visual Studio Advent Calendar 2016の4日目の記事です。

sushi_rotation_on_mbp少し前に、新しいMacBook Proで回転寿司が話題になりましたね。こんな奴です:

centerclr-sushirotatorこれが転じて、最終的にVisual Studioの拡張機能として、こんなものを作りました:

Visual Studioの拡張機能… 私は度々dis… もとい、非常に難易度が高いという話をしているのですが、この拡張機能の実装はそれほど難しくありません。とは言え、罠が至る所に待ち構えています。今回は、拡張機能を作るうえで躓いてツマラナイ思いをしないように、導入部分のガイドを記録に残しておこうと思います。

なお、この拡張機能はGitHubで公開しています: CenterCLR.SushiRotator
すぐに試してみたい方は: Visual Studio Market place、または拡張機能から「sushi」で検索すれば出てきます。

# やじうまの杜でも取り上げられました: MacのTouch Barが回転寿司に! エディターでも続々カイテン


事前に考える必要のある事

Visual Studioの拡張機能は、俗に「VSIX」と呼ばれています。これは、拡張機能のパッケージに「CenterCLR.SushiRotator.vsix」のような拡張子が付いていることでもわかります。
ややこしいのは、拡張機能のすべてにvsix拡張子がついているわけではなく、一般的なセットアップインストーラーだったり、msi形式だったする事もあることです。

VSIX形式は、Visual Studio(のVSIXInstaller.exe)を使ってインストールします。また、VSIXでは、Visual Studio Market placeからの自動更新に対応していて、ユーザーレベルでのインストールにも対応しているため、ユーザーからすると扱いやすいものになっています。例えば、拡張機能がGACへのインストールを必要としているなどの複雑な処理を行う場合は、VSIX形式とすることはできないため、msiや独自のインストーラーを作る必要があります。

今回は、要するにエディタを拡張するだけであり、エディタの拡張ならVSIXだけで完結するため、標準的なVSIX形式で作ります。

また、対象のVisual Studioを何にするかも考える必要があります。Visual Studioの拡張機能は、以下のエディション(以上)でサポートされます:

  • Visual Studio 2005 Professional
  • Visual Studio 2008 Professional
  • Visual Studio 2010 Professional
  • Visual Studio 2012 Professional
  • Visual Studio 2013 Community
  • Visual Studio 2015 Community
  • Visual Studio 2017RC Community

より古いバージョンに対応させようとすると、それだけ拡張可能な機能が制限されます(機能拡張に対応するインターフェイスが定義されていないなど)。

これ以前のバージョン(2003など)でも、基本的なインターフェイスは実装されています(それどころか、古のVisual InterDevやJ++でも)。しかし、これらのバージョンでの拡張は、マイクロソフトと特別な契約を結んだデベロッパだけが開発することが出来たため、ほとんどの皆さんは対応させることが出来ません(まあ実際には書いたら動くのかもしれませんが。それから、既にサポート対象外なので、実質無視して良いと思います)。

また、以下の制限もあります:

  • 各バージョンのExpress Editionは、拡張機能に対応していません。
  • VSIXパッケージをサポートするのは、Visual Studio 2010以上です。2010未満は、カスタムインストーラーが必要です。当然、Market placeでの自動更新はできません。
  • 今回対象とする「テキストエディタの拡張」は、Visual Studio 2010以上でのみサポートされます。これは、2010以降はVisual StudioがWPFで書き直され、WPFコントロールを使う前提となっていることが遠因となっていると思われます。
  • Visual Studio 2012以降は、後述のvsixmanifestファイルの形式が変更されたため、2010をサポートする場合は古い形式で記述する必要があります。
  • それぞれの拡張機能を実装するには、対応するVisual Studio SDKが必要です。例えば、2010の場合は、VSSDK2010が必要となります。これらは下位互換性があるため、普通は目指すバージョン群の最も高いバージョンに対応するSDKをインストールすれば、問題ありません。
  • 使用する.NET Frameworkのバージョンにも注意して下さい。対応させる最も古いVisual Studioで開発可能な、最後のバージョンを選択します。(大丈夫だと思いますが、.NET Coreは使えません、念のため)。

vs2017rc_install_vsix上記の事を考え、今回はVisual Studio 2012 Professional以上、つい先日リリースされた2017RCまでを対象とします。SDKは2017RCのものが必要ですが、これは2017RCのインストーラーで選択すればインストール出来ます。.NET Frameworkは4.5を使用します。

ややこしいのは、拡張機能の開発自体は、対応させる最も新しいVisual Studioを使う必要がある、と言うことです。したがって、2017RCを使用します。


拡張機能のプロジェクトテンプレート

vsix1VSIXの拡張機能を作るには、プロジェクトの新規作成から「Extensibility」にある「VSIX Project」でプロジェクトを作成します:

vsix2次に、プロジェクトにエディタ拡張を追加します。これも、Extensibility配下から選択します。いくつか候補がありますが、ここでは「Editor Margin」を選択します。

vsix3このEditor Marginが、テキストエディタの一部の領域を占有する事が出来る拡張です。これを使って寿司を流します。

これらのテンプレートですが、以前は本当にくぁwせdrftgyふじこlp… いや、色々と問題があった… のですが、2017RCでは非常にすっきりしたテンプレートとなっていて、扱いやすく修正されました。基本となるVSIX Projectでプロジェクトを作っておいて、そこに必要なitemとして後から追加する感じです。但し、拡張機能で実現可能な機能に対して、圧倒的にテンプレートが足りてません。大半は一から手でクラスを書いたりする必要があります。

# 何故かテキストエディタに対する拡張のテンプレートだけが色々あります… もちろん、本当はもっと多彩な事が出来ます。


Editor Margin

Editor Marginは、テキストエディタの一部領域が、拡張機能のために予約され、そこに自由に何かを表示させることが出来るVSIXの拡張機能です。この一部領域とはつまり、WPFのCanvasであり、まあ、あとはWPFで好きなようにやってよ、という感じです。Editor Marginクラスを加えると、以下のようなテンプレートコードが生成されます:
(Disposeの面倒を見るコードもあるんですが、長いので省略)

namespace VSIXProject1
{
    /// <summary>
    /// Margin's canvas and visual definition including both size and content
    /// </summary>
    internal class EditorMargin1 : Canvas, IWpfTextViewMargin
    {
        /// <summary>
        /// Margin name.
        /// </summary>
        public const string MarginName = "EditorMargin1";

        /// <summary>
        /// Initializes a new instance of the <see cref="EditorMargin1"/> class for a given <paramref name="textView"/>.
        /// </summary>
        /// <param name="textView">The <see cref="IWpfTextView"/> to attach the margin to.</param>
        public EditorMargin1(IWpfTextView textView)
        {
            this.Height = 20; // Margin height sufficient to have the label
            this.ClipToBounds = true;
            this.Background = new SolidColorBrush(Colors.LightGreen);

            // Add a green colored label that says "Hello EditorMargin1"
            var label = new Label
            {
                Background = new SolidColorBrush(Colors.LightGreen),
                Content = "Hello EditorMargin1",
            };

            this.Children.Add(label);
        }

        #region IWpfTextViewMargin
        /// <summary>
        /// Gets the <see cref="Sytem.Windows.FrameworkElement"/> that implements the visual representation of the margin.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public FrameworkElement VisualElement
        {
            // Since this margin implements Canvas, this is the object which renders
            // the margin.
            get
            {
                this.ThrowIfDisposed();
                return this;
            }
        }
        #endregion

        #region ITextViewMargin
        /// <summary>
        /// Gets the size of the margin.
        /// </summary>
        /// <remarks>
        /// For a horizontal margin this is the height of the margin,
        /// since the width will be determined by the <see cref="ITextView"/>.
        /// For a vertical margin this is the width of the margin,
        /// since the height will be determined by the <see cref="ITextView"/>.
        /// </remarks>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public double MarginSize
        {
            get
            {
                this.ThrowIfDisposed();

                // Since this is a horizontal margin, its width will be bound to the width of the text view.
                // Therefore, its size is its height.
                return this.ActualHeight;
            }
        }

        /// <summary>
        /// Gets a value indicating whether the margin is enabled.
        /// </summary>
        /// <exception cref="ObjectDisposedException">The margin is disposed.</exception>
        public bool Enabled
        {
            get
            {
                this.ThrowIfDisposed();

                // The margin should always be enabled
                return true;
            }
        }

        /// <summary>
        /// Gets the <see cref="ITextViewMargin"/> with the given <paramref name="marginName"/> or null if no match is found
        /// </summary>
        /// <param name="marginName">The name of the <see cref="ITextViewMargin"/></param>
        /// <returns>The <see cref="ITextViewMargin"/> named <paramref name="marginName"/>, or null if no match is found.</returns>
        /// <remarks>
        /// A margin returns itself if it is passed its own name. If the name does not match and it is a container margin, it
        /// forwards the call to its children. Margin name comparisons are case-insensitive.
        /// </remarks>
        /// <exception cref="ArgumentNullException"><paramref name="marginName"/> is null.</exception>
        public ITextViewMargin GetTextViewMargin(string marginName)
        {
            return string.Equals(marginName, EditorMargin1.MarginName, StringComparison.OrdinalIgnoreCase) ? this : null;
        }
        #endregion
    }
}

コンストラクタで渡される引数IWpfTextViewと、IWpfTextViewMarginというインターフェイスの実装が、拡張機能をサポートする要となっています。また、このクラスはCanvasクラスを継承していますね。サンプルの実装は、このCanvasにLabelを手動生成して追加しています。やる気になれば普通にXAMLで書いても動きそうな気配です(試してません)。

コンストラクタの引数は依存注入されるのでしょう(Visual StudioはMEFを使用しているので、MEF経由で渡されるようです)。(間違えました、以下で示すファクトリクラスから生成しています)注意すべき点としては、このクラスにはstaticフィールドを含めることが出来ない、と言う事です。含めた場合、拡張機能のロード時に失敗します(結構悩んだ。Visual Studioのバグかもしれない)。staticフィールドが欲しい場合は、別のクラスに定義して、そこを参照するようにします(回転寿司でもやっています)。

サンプルのコードはコンストラクタ内で、Heightを20pxに指定しています。Canvasなので、このサイズが表示領域を規定することになります。また、WPFは標準でクリッピングを行いません。手動描画した場合に描画がはみ出ると、エディタ領域が酷いことになってしまうため、ClipToBoundsをtrueとしているようです。はみ出す可能性がなく、パフォーマンスに問題がある拡張機能を実装するのであれば、これをfalseとしても良いでしょう(どんな拡張機能なのかは良くわからない…)。

まんまWPFなので、もちろんアニメーションも使えます。回転寿司では、寿司を流すのにWPFのアニメーションを使っています。なので、非常に滑らかに流れることがわかると思います(上のGIFアニメーションはガタガタですが、ぜひ実際にインストールして見て下さい)。


Editor Margin Factory

Editor Marginクラスは、ファクトリクラスとの関連付けで実行されます。ファクトリクラスはEditor Marginテンプレートを追加した際に、自動的に追加されているはずです。以下に例を示します:

namespace VSIXProject1
{
    /// <summary>
    /// Export a <see cref="IWpfTextViewMarginProvider"/>, which returns an instance of the margin for the editor to use.
    /// </summary>
    [Export(typeof(IWpfTextViewMarginProvider))]
    [Name(EditorMargin1.MarginName)]
    [Order(After = PredefinedMarginNames.HorizontalScrollBar)]  // Ensure that the margin occurs below the horizontal scrollbar
    [MarginContainer(PredefinedMarginNames.Bottom)]             // Set the container to the bottom of the editor window
    [ContentType("text")]                                       // Show this margin for all text-based types
    [TextViewRole(PredefinedTextViewRoles.Interactive)]
    internal sealed class EditorMargin1Factory : IWpfTextViewMarginProvider
    {
        #region IWpfTextViewMarginProvider
        /// <summary>
        /// Creates an <see cref="IWpfTextViewMargin"/> for the given <see cref="IWpfTextViewHost"/>.
        /// </summary>
        /// <param name="wpfTextViewHost">The <see cref="IWpfTextViewHost"/> for which to create the <see cref="IWpfTextViewMargin"/>.</param>
        /// <param name="marginContainer">The margin that will contain the newly-created margin.</param>
        /// <returns>The <see cref="IWpfTextViewMargin"/>.
        /// The value may be null if this <see cref="IWpfTextViewMarginProvider"/> does not participate for this context.
        /// </returns>
        public IWpfTextViewMargin CreateMargin(IWpfTextViewHost wpfTextViewHost, IWpfTextViewMargin marginContainer)
        {
            return new EditorMargin1(wpfTextViewHost.TextView);
        }
        #endregion
    }
}

初見殺し的なひどいコードですが、このクラスの属性から、Visual StudioがEditor Marginのコードを認識して、実行可能となります。いじるのは、色々わかってからの方が良いでしょう。


vsixmanifestファイル

VSIX Projectプロジェクトを生成すると、*.vsixmanifestというファイルも追加されます。このファイルが、VSIXをパッケージングするのに使われる定義体で、中身はXMLです。NuGetで言う所のnupkgに対応するnuspecファイルです。

<?xml version="1.0" encoding="utf-8"?>
<PackageManifest Version="2.0.0" xmlns="http://schemas.microsoft.com/developer/vsx-schema/2011" xmlns:d="http://schemas.microsoft.com/developer/vsx-schema-design/2011">
  <Metadata>
    <Identity Id="VSIXProject1.Kouji Matsui.86820e26-2ec6-496e-ad20-38a2ba88bf46" Version="1.0" Language="en-US" Publisher="Kouji Matsui" />
    <DisplayName>VSIXProject1</DisplayName>
    <Description>Empty VSIX Project.</Description>
  </Metadata>
  <Installation>
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[15.0]" />
  </Installation>
  <Dependencies>
    <Dependency Id="Microsoft.Framework.NDP" DisplayName="Microsoft .NET Framework" d:Source="Manual" Version="[4.5,)" />
  </Dependencies>
  <Prerequisites>
    <Prerequisite Id="Microsoft.VisualStudio.Component.CoreEditor" Version="[15.0,16.0)" DisplayName="Visual Studio core editor" />
  </Prerequisites>
  <Assets>
    <Asset Type="Microsoft.VisualStudio.MefComponent" d:Source="Project" d:ProjectName="%CurrentProject%" Path="|%CurrentProject%|" />
  </Assets>
</PackageManifest>

XMLはこのような形式です(上で述べたように、2010に対応させるには異なる形式で記述する必要があります)。但し、SDKがインストールされていれば、ソリューションエクスプローラーからダブルクリックで開くと、以下のようにビジュアル編集出来ます:

vsix4パッケージをMarket placeで公開する場合は、Identity・DisplayName・Descriptionなどをきちんと埋めておきましょう。他にもIcon・PreviewImage・License・Tagsなどが指定出来ます。

vsix5また、重要な項目として、Dependenciesの.NET Frameworkのバージョンと、Prerequisitesの「Visual Studio core editor」のバージョンを合わせておく必要があります。

vsix6Version Rangeの指定は、数学で使うような、以上・未満の記号による指定を行います。バージョン11(2012)以上~16(2018?)未満の場合、”[11.0, 16.0)”のように、以上を角括弧、未満を丸括弧で示します。このとき、”[11.0, 15.0]”としてはいけません。これでは、2017のマイナーバージョンアップに対応出来なくなってしまいます(15(2017)「以下」なので、マイナーバージョンが上がると外れてしまう)。

InstallationTargetは複数定義できます。最初に検討した、対応させるVisual Studioのバージョンとエディションの組み合わせが複雑になるので、以下のように定義するとよいと思います:

  <!-- これは回転寿司で使っている定義です。
       InstalledByMsiはmsiでインストールするかどうか、AllUsersは昇格が必要かどうかです。 -->
  <Installation InstalledByMsi="false" AllUsers="false">
    <!-- VS2012 -->
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[11.0,12.0)" />
    <!-- VS2013- -->
    <InstallationTarget Id="Microsoft.VisualStudio.Community" Version="[12.0,16.0)" />
    <InstallationTarget Id="Microsoft.VisualStudio.Pro" Version="[12.0,16.0)" />
  </Installation>

いつも通りと言うか、一旦vsixmanifestを生成したら、後は手動でXMLを弄った方が早い感じがひしひしと…


VSIXをデバッグする

このプロジェクトをビルドすると、bin\Debug(またはbin\Release)に、*.vsixが生成されます。デバッグ実行すると、検証用のVisual Studioが別途起動し、そこにVSIXが自動登録されてデバッグ可能になります。以下はデバッグ実行した例です。右側が開発中のVisual Studio、左側がデバッグ実行で新たに起動した「実験的なインスタンス」と命名されているVisual Studioで、その上で寿司が流れています。

vsix7

Visual Studio 2015以降で特徴的な、WPFライブビュー用のツールバーも表示されていることがわかります。

ところで、デバッグしていると途中で動作がおかしくなったりすることがあります。特に、変更したコードが反映されていないなどです。これは、VSIXのインストールフォルダが何らかの問題で破損した場合に発生します。インストールフォルダはユーザー毎に作られます。場所は、「C:\Users\[ユーザー名]\AppData\Local\Microsoft\VisualStudio」のような場所です。この配下に、Visual Studioのバージョン番号毎にフォルダが作られ、その中にインストールされます。

デバッグ実行した場合も、このフォルダ配下にコピーされます。2017RCの場合、サブフォルダは「15.0_31113400Exp」です。最後の「Exp」が「実験的なインスタンス」に対応します。何か動きがおかしいと思ったら、このフォルダを丸ごと削除して再度ビルドしてみて下さい(Visual Studioは一旦終了させる必要があるかもしれません)。削除しても、次回起動時には再生成されます。なお、経験的には「わりとよく壊れます」。アッハイとかつぶやきながら、削除すると良いでしょう。

それから、最新のVisual Studio対応バージョンを上げる場合(例えば、2013対応のものを2017RCに対応させる)、デバッグ実行時に起動するdevenv.exeへのパスも更新することをお忘れなく。「実験的なインスタンス」へのデプロイは最新のVisual Studioに行われる(これはSDKの機能で行われる)のに、起動するVisual Studioが古いバージョンのままだと、VSIXがロードされないように見えて混乱します。この問題も忘れたころにハマるパターンです。

C#のプロジェクトは、デバッグ時の起動パスを*.csproj.userに保存します。通常、Gitでソースコード管理していると、このファイルは管理対象外とする(.gitignoreで除外)のですが、ここにパスが書かれているため、git cloneしてデバッグしようとして失敗するのも良くハマります。*.csprojに手動で定義を移しておくとベターですが、そうすると今度は*.csproj.userでいつの間にかオーバーライドされていてハマるという、非常に苦しい展開が待っています…

# オーバーライドを無視するか警告する属性が欲しい…


VSIXのNuGetと低いバージョンに対応させる話

2017RCのVSIX Projectでプロジェクトを作ると、VSIXで必要なライブラリが、NuGet経由で参照されます。このパッケージは「Microsoft.VSSDK.BuildTools」「Microsoft.VisualStudio.CoreUtility」「Microsoft.VisualStudio.Text.UI」などで公開されています。これらのパッケージが公開されていることを知ったのは今回の回転寿司によるんですが、昔はNuGetパッケージ化されていなかったため、VSIXを含むプロジェクトのビルドは、非常に面倒な問題を含んでいました(当然、CIするにはVisual Studio本体が必要だったり)。

今はこのように、VSIXテンプレートとは別体となったおかげで、ずいぶんやりやすくなったと思います。NuGet経由での参照となったのは把握していませんが、ごく最近のバージョンからの筈です。

が、このパッケージの最低バージョンが2013以降のようで、2012やそれ以前のバージョンには対応していません。もし、古いバージョンも対応させる予定であるなら、別のパッケージ「VSSDK.CoreUtility」が公開されているので、これを使うと良いと思います(StyleCop Analyzersのauthorですね)。NuGetで「VSSDK」で検索すると、細分化されたパッケージのリストが表示されるので、必要なものを追加します。但し、VSIXのビルドシステムだけは最新のパッケージを使う必要があります(vsixmanifestの最新のスキーマを認識可能にするため)。つまり、「Microsoft.VSSDK.BuildTools」は、最初に挙げたもの(の最新版)を使うことをお勧めします。

昔のVSIXテンプレートから生成したプロジェクトを保守している皆様… ご愁傷さまです。手動でゴリゴリcsprojを書き換える未来が待っております… (泣


まとめ

全体的なコードは、回転寿司を参考にして見て下さい。VSIXのパッケージをローカルデバッグ出来ていよいよ公開できるようになったら、Visual Studio Market placeで公開できます。公開は無料で可能です。公開プロセスは変更中のようで、現在は Visual Studio Team Onlineで使う拡張機能と統合したいのか、変に中途半端になっている感じです。Market placeからのリンクで申請しようとすると VSTO 向けの公開として扱われて、認証プロセスがどうのとか面倒な事になります。

vsix8「Visual Studio Gallery」と呼ばれているこっちから行き、中ほどにある「アップロード」をクリックすると確実です。アップロードする前に、vsixmanifestの内容を見直してください。また、バージョンアップする場合は、vsixmanifestのバージョン番号を更新することをお忘れなく。

一応、アップロードした時点で、パッケージングが正しいかどうかの検証は行ってくれるようです。なお、更新されたパッケージをアップロードすると、使用中のユーザーにはおなじみのように「拡張機能の更新があります」と通知されます。

何か面白い拡張機能作って放流してください :)
それでは。