ぷろじぇくと、みすじら。

RoslynをT4テンプレート内で使う

Created at:

Visual StudioにはT4テンプレートというソースコードを生成する機能があるのですが、その中からRoslynを使おうというお話です。

T4テンプレートはVisual Studioと統合されていることとC#でテンプレートのコードを書いて、C#, VB, TypeScriptなどのコードを吐き出せます。

例えば…

のような感じで、アセンブリを読み込んでコードを生成するという使い方はよく見かけます(プロキシコードの生成とか)。

アセンブリを読み込んでコードを生成するということは「アセンブリがコンパイルされている」ことが前提になります。

例えばプロキシクラスみたいなものはそれで問題ないので、大体はそれでいいのですが「元になるコードと生成したコードを同一のアセンブリに含めたい」場合にはニワトリ卵というか「元になるコードをコンパイル」「コンパイル結果をもとにコード生成」「コード生成を含めてコンパイル」という手順が必要です。

もちろんリフレクションとかファクトリとか登録する式にすればコード生成いらないよね?という場合もあると思いますが、そこはまあ要件しだいというやつです。生成したいときもある、はず…。

例えばEnvDTEを避ける

そんなわけで「コンパイルしてできたアセンブリを元にコードを生成」するのではなく、「現在のコードを元にコードを生成」したいというときはどうするのかというとコードの構造を読み取るためにVisual Studioにアクセスする方法をとります。

一般的にT4テンプレートからVisual StudioにアクセスするにはEnvDTEというかっこいいオブジェクトモデルと仲良くすることになります。EnvDTEはVisual Studioの機能を公開するCOMライブラリなのなので、絶妙な使いづらさがあります。何よりEnvDTEはVisual Studioにくっついている都合、ConsoleApplicationやLINQPadなどで試しに書いてみるというのが難易度高いです。

まあサンプルコードを書く気にならない程度には面倒だということです。

そこでVisual Studio 2015以降ではコンパイラー基盤がRoslynになっているのでそれを使えばいいのではというのが今回のアイデアです。

T4の中でRoslynを使ってコードを読み込む

さてどうやってRoslynを使って読み込むのかという話ですが、残念なことにVisual Studio 2015の内部で「現在動いているRoslynのインスタンス」に直接アクセスすることはできません。

ですが、Visual Studio 2015に含まれているRoslynを使うことはできます。Roslynは C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies におかれているので、それらしいアセンブリを参照して名前空間をインポートします。

<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\System.Collections.Immutable.dll" #>

<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ import namespace="Microsoft.CodeAnalysis.MSBuild" #>

これで使えるようになります!

あとは単純にRoslynを使ってソリューションを読み込んで、コードを解析すればいい感じに処理できるでしょう。

と言ってもいきなりT4でRoslynプログラミングはそれはそれで難しいのでConsoleApplicationかLINQPadで書いてみるのがよいでしょう。

T4の外でコードを書いてみる

とりあえずどこかのC#プロジェクトにあるクラスの一覧を適当に出力する、というコードを書いてみることにします。Roslynのコードの詳しいことは今回割愛しますので別途調べてください。

というわけでLINQPadやConsoleApplicationで動きそうな単純なコードは以下のようになります。

void Main()
{
// 対象となるプロジェクトの.csprojのパス
var projPath = @"C:\Users\Tomoyo\Documents\visual studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.csproj";
var classNames = GetClassNamesAsync(projPath).Result;
classNames.Dump();
}

// Roslynを使ってプロジェクトのコードからクラスのコンストラクタ定義を引っこ抜く
async Task<List<string>> GetClassNamesAsync(string csprojPath)
{
// MSBuildのワークスペース(環境みたいなもの)を作って、プロジェクトファイルを開く
var workspace = MSBuildWorkspace.Create();
var project = await workspace.OpenProjectAsync(csprojPath);

// ソースコードをコンパイル的なことをする(出力するわけではなくて内部的なデータを作るやつ)
// これでコードをパースした結果を得ることができるようになる
var compilation = await project.GetCompilationAsync();

var classNames = new List<string>();
// シンタックスツリーをファイル単位で適当になめていく
foreach (var syntaxTree in compilation.SyntaxTrees)
{
// セマンティックモデル(シンタックスツリーは文法で、それに対応する「コード的な意味」)を取得する
var semModel = compilation.GetSemanticModel(syntaxTree);

// シンタックスツリーからクラス定義のシンタックス(記述)を引っ張り出して、
// セマンティックモデルに問い合わせることでクラス定義(意味)を引っ張り出して、ふにゃふにゃ処理する。
classNames.AddRange(
syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Select(x => semModel.GetDeclaredSymbol(x))
.Select(x => x.ToDisplayString())
);
}

return classNames;
}

実行結果はこんな感じ。

LINQPad

なおこのコードはMicrosoft.CodeAnalysis.CSharp.WorkspaceというNuGetパッケージをインストールすれば動くでしょう。

T4テンプレートの中で使う

で、これとT4テンプレートの中で使うには…という話ですが、先ほどのimportに加えて、適当にコピペしても大体動くはずです。例えば以下のように。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Threading.Tasks" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\Microsoft.CodeAnalysis.Workspaces.Desktop.dll" #>
<#@ assembly name="$(DevEnvDir)PrivateAssemblies\System.Collections.Immutable.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Threading.Tasks" #>
<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ import namespace="Microsoft.CodeAnalysis.MSBuild" #>
<#@ output extension=".txt" #>
<#
// 対象となるプロジェクトの.csprojのパス
var projPath = @"C:\Users\Tomoyo\Documents\visual studio 2015\Projects\ConsoleApplication1\ConsoleApplication1\ConsoleApplication1.csproj";
var classNames = GetClassNamesAsync(projPath).Result;
#>

<# foreach (var className in classNames) {
#>- <#= className #>
<# } #>

<#+

// Roslynを使ってプロジェクトのコードからクラスのコンストラクタ定義を引っこ抜く
async Task<List<string>> GetClassNamesAsync(string csprojPath)
{
// MSBuildのワークスペース(環境みたいなもの)を作って、プロジェクトファイルを開く
var workspace = MSBuildWorkspace.Create();
var project = await workspace.OpenProjectAsync(csprojPath);

// ソースコードをコンパイル的なことをする(出力するわけではなくて内部的なデータを作るやつ)
// これでコードをパースした結果を得ることができるようになる
var compilation = await project.GetCompilationAsync();

var classNames = new List<string>();
// シンタックスツリーをファイル単位で適当になめていく
foreach (var syntaxTree in compilation.SyntaxTrees)
{
// セマンティックモデル(シンタックスツリーは文法で、それに対応する「コード的な意味」)を取得する
var semModel = compilation.GetSemanticModel(syntaxTree);

// シンタックスツリーからクラス定義のシンタックス(記述)を引っ張り出して、
// セマンティックモデルに問い合わせることでクラス定義(意味)を引っ張り出して、ふにゃふにゃ処理する。
classNames.AddRange(
syntaxTree.GetRoot()
.DescendantNodes()
.OfType<ClassDeclarationSyntax>()
.Select(x => semModel.GetDeclaredSymbol(x))
.Select(x => x.ToDisplayString())
);
}

return classNames;
}
#>

で、このT4テンプレートを実行すると、以下のようにクラス名の一覧が記述されたファイルが吐き出されます。

- ConsoleApplication1.Hauhau
- ConsoleApplication1.Program
- ConsoleApplication1.Hoge
- ConsoleApplication1.Fuga

とても簡単ですね。

まとめ

大抵の場合においてはアセンブリを直接読み込めばいいと思いますが、もし「現在のコードを元にプロジェクトに含めるコードを生成する」という必要がある場面では役に立つのではないでしょうか。

またこの組み合わせで書いてみるとわかるのですがLINQPadでRoslynでソースコードから情報を集約するコードを書いて、それを元にジェネレートする、という手順はテンプレートからロジックの一部を切り出して実装でき、デバッグしやすいのでとてもおすすめです。

Android TVなBRAVIA

Created at:

7月ごろにSonyのBRAVIAはちょうど大きなモデルチェンジのタイミングでミドルレンジより上の機種はAndroid TVが搭載されるようになりました。
で、KDL-55X8500Cという機種を一応発売日に買ったのでそのメモというか人柱レビューというかなんというか。

端末情報:KDL-55X8500C

ちなみにAndroid TV搭載というのはどういうことかというと、テレビそのもののプラットフォームが謎のOSからスマートフォンと同じようにAndroidベースになったということです。

なのでGoogle Playのストアからアプリをインストールできたりといった今までのテレビとはちょっと違う感じになっています。

感想

Androidで動いてるというところからのお察しの通りで端的に言えば地雷です。

プラットフォームが刷新されたため、あらゆるものが作り直しされているが故の未完成さがかなりある上に安定性も微妙でテレビとしてどうなの感があります。
アップデートである程度機能が増えたり、安定したりしなかったらだいぶ厳しい感じですね。

ある程度は予想はしていたのですが思っていたよりもひどいですね。

よくないところ & 使えない機能

以前使っていたテレビはKDL-40W900Aという2013年のモデルなので大体2世代前ということでそこまで古くはないですが、そこからの比較ということで。

普通に機能がデグレってるのがかなりおおうっ…となるポイントです。

まさか2年後の機種で機能がしょぼくなってもっさりして不安定になるとは…って感じなのでアップデートで何とかしてほしいですが無理な予感がしていますね。すでに2か月が経過してますけど。

追記: そういえば設置しにきてもらってPlayStation 3をつないだら音は出るけど映らない事象に出くわして、挿すところを変えたりPS3の設定をリセットしてもダメで(Wii Uは映った)、設置に来た方(ソニーの人)がすみません調査しますって帰って、アップデートしたら直ったというところからのスタートだったということを思い出しました。

いいところ

いいところもあるにはあります。

Androidであることというのはちょっと面白いというか、中身が透けて見えるというか。
アプリ作れるとはいえJava書きたくないし、エミュレーター遅いしで結構つらいのですけど作れないよりはましです。なおadbで接続するならUSB(A)のオス-オスが必要なので注意です。

特にGoogle Castが組み込まれているところはよいです。Google Castというのはブラウザやアプリから画面を受信できるという機能です。

Google Castのテレビ用の外付けデバイスに相当するChromecastというものがありますが、Android TVなテレビはGoogle Castが組み込まれています。
テレビ自体がCastを受け取れるため何もせずスマートフォンやブラウザからCastするだけで自動で表示できます。
つまりChromecastやNexus Playerの場合にはまずその機器への入力切替が必要となりますがその操作が不要なのです。

追記: ChromecastはHDMI機器制御対応らしく、利点がつぶされたかと思いましたがCastやめたらテレビに戻ったりはできないっぽいのでセフセフ。

起動が速いのはスマホやパソコンのようにスリープ的な状態から復帰するだけなのでフル機能を使えるまでの時間が圧倒的に短いのです(もちろん再起動には時間かかる)。
対して40W900Aなどの以前のモデルはある時間帯(しかも時間指定不能)だけ高速起動可能状態になれるのですがその時間を外すとフル機能を使えるまでの時間が長いです。これは普通に良いところです。

おすすめのアプリ

ホーム: アプリ

折角ですのでおすすめのアプリを少しメモしておきます。Google Playアプリからだと検索できなかったりそもそも面倒だったりするので、パソコンからGoogle Playにブラウザでアクセスしてインストールするのをお勧めします。

その他

性能的にはスマホでいえばXperia Zかそれ以下ぐらい(デュアルコア)でRAMは1.5GBぐらいっぽいです。絶妙にしょぼい…。実際体感的にも遅いかなって感じがします。

まとめ

すごくAndroidっぽい。たぶんすぐにアップデートされなくなってAndroidっぽさが完成するのではないかという予感がしています。

というのは半分冗談としても、現状だいぶひどいですし安い買い物ではなかったのでアップデートで前の世代並みにはよくしてほしいなというのが正直なトコロです。