2014年を振り返る

振り返るシリーズ第三弾。12/30日にやってるのは誕生日なので、まぁ今年もそれで、と。今年はグラニ設立2年目になったわけですが、去年のまとめでは

ここ数年は、毎年ジェットコースター状態で目まぐるしく変化していて。けれど、大きな目標からはブレないで、年々近づけている気がします。一番最初に若くない人サイドに入ったとか、新陳代謝とか言いましたが、来年はそういうことが起こる状態を作っていきたいですね。C#が、若い人がこぞって使うような言語になってればいい、と。そのためにできること。人がすぐに思い浮かべられる、メジャーなアプリケーションの創出と、C#による圧倒的な成果、C#だからこその強さ、というのを現実に示していくこと。雇用の創出、の連鎖。

なるほど。達成度でいうと、今年は残念ながら弱いかなぁ、うむむむ。そこに向けて突き進んでいるというのは変わらないのですが、来年に向けての準備といった感になってしまったかも。あとは、どちらかというと一年目の総まとめみたいな感じ。一番大きなセッションはAWS Summit Tokyo 2014での発表、AWS + Windows(C#)で構築する.NET最先端技術によるハイパフォーマンスウェブアプリケーション開発実践かな?一端の成果を示したうえで、次のステップへ、といったような。

C#

今年は大分記事数少なめになってしまってます!過去最小かも。かわりにライブラリは過去最多で作ったかもしれません。

去年に引き続き、前半はLightNodeの作成続き。OWIN上に作られたMicro RPC/REST フレームワーク。コンセプトはいいと思うし実装もかなりいいと思うし、既にプロダクションに突っ込んで稼働してるんで、ちゃんと使えるし作って良かったとは思ってます。ASP.NET MVC 6でAction FilterがOWIN風デリゲートチェーンになってるのなんかはLightNodeでは最初からそうしてるし、絶対そのほうがいいでしょドヤァ、言ったとおりでしょ!といった先見の明もある!が、しかし、コミット止まって完全に息切れしてますね(笑)

というのも、うーん、まぁ去年後半から今年前半にかけてはOWINへの傾倒もあったのですけれど、ASP.NET vNextがね……。アレによって完全にOWIN無価値になりましたから。思想的/コードのふいんき的な面では親しいところがあるので、今やるならOWINベースで書くのは良いと思ってます。そうすればvNextへの「移植」が容易になりますから。でも、移植なんですよね、そのまま持ってく(一応互換レイヤーで持ってけますが)わけではないところからして、萎える……かなりOne ASP.NET(笑)感があって、割と嫌な気分ですねー。誰かマジに来日するScott Hanselmanに突っ込んでくださいよ(私は行きません)。とはいえ良くなってる面も理解できるんで、来年は気持ちを切り替えてvNextやりますよ、はい。ちなみにLightNode自体は、vNextベースで、ちょっと違う形で生まれ変わるはずです、という計画があります、やるやる詐欺。

RespClientというPowerShell向けのRedisクライアント/コマンドレットも今年作りました。これはまぁ、たまに私自身も便利にツカッテマス。メンテはguitarrapc先生に譲りました。Redisは相変わらずモリモリ使ってまして、素晴らしいKVSだと思います。来年はやはりこれも放置気味なCloudStructuresをStackExchange.Redisに対応させないと、という……。

そして今年最大の気合の入れ方でリリースしたのがUniRx - ReactiveExtensions for Unity。絶対に必要になる、と、こそこそ作ってたんですが、実際良いもの、欠かせないものになったと思ってます。そして、成功した!と言ってもいいかなー。uFrameに同梱されるようになったとか、海外でも反響あったうえに、国内でもじわじわ話題になりだしていて、かなりいい感じです。来年もがんがん更新していきたい(ちなみに現在AssetStoreでアップデート申請中!)。また、UnityコミュニティとC#コミュニティには若干の断絶がありますが、そこも埋められたらな、といったところですね。

ちなみにUnity関連では、他にLINQ to GameObjectという小品もリリースしたりしたり。

LINQ to BigQueryというGoogle BigQuery用のライブラリも結構な大物でした、と作るの大変だった(というか面倒だった)度合い的に。BigQueryは、正直、凄い。.NETの人もAzureの人も、とりあえず使うべき。うちの会社も基本AWSですが、BigQueryだけはBigQuery。BigQueryに突っ込むためのロギング周りについても一家言できたのですが、その辺はEtwStreamという作りかけの謎プロジェクトがあるので、それが完成した時にでも、お話しましょう(実際作りきりたいとはオモッテマス)

今年はUniRx(Rx), LINQ to BigQuery(Queryable), LINQ to GameObject(LINQ to XML)を通して、改めてLINQとは何ぞやか、というのを掲示できたのも良かったかと思います。口で説明するよりモノで黙らせたほうが早いというアレソレ。

会社

いいところは、今年も非常に強力なメンバーが多くJOINしてくれた!人は会社の原動力ですからね、うちを選んでくれたことに大変感謝です。「C#」では本当に、類を見ないほど力のある会社となっているのではないかな、と。より能力を発揮してもらうような環境を作りたいですね。

さて、今年はやけにCTOの役割とは!みたいなテーマが盛り上がったところですが、私の場合どうかしらん。広告塔代わりであったり求人面であったりなんかは、十二分すぎるほど果たせたとは思います。技術選定なんかも適度に先駆的に、的確だった。少なくとも失敗はない。合間合間にガッと作ってるライブラリ郡も(会社でコソッと作る時間も少し持ってますが、基本的には家で仕上げてますよ)、戦略的に根幹をなすようにしたりで、よくやれたんじゃないかなー。

とはいえ反省点は多かったり。割と勢いだけで突っ走れた1年目と違って2年目は中々むつかしく。特に時間が細切れになるのは避けられなくて、どうも集中しきれず成果としてはかなりイマイチ。この辺は受け入れつつ細切れでも効率的に作業できるよう自分を律するしかないですかね、といった感。そんなわけで自社のプログラムにがっつり関われたかというとかなりそうでもないのが、もにょもにょ。かなり良くない。総論するとどーも歯切れ悪い感じ。来年はドヤッ!といえるようにならないとかな。

ゲーム

PS4やXbox Oneも買ったのですが、うーん。結局やっぱりあんまプレイしてないのよねー。ただまぁPS4>超えられない壁>Xbox Oneというのは痛感しました、これはキビシイ……。Kinect2も割とガッカリ系。そんなわけでvvvvvvのiOS移植が一番楽しんだのかも。

音楽

今年中頃からはずっと大森靖子聞いてましたね、ライブにも行ったし……。YouTube動画だと弾き語りの大森靖子 LIVE @ TIF2013とバンド編成の大森靖子&THEピンクトカレフ@ZeppDiverCityあたりがお薦め。エキセントリックな情報とかインターネット時代の戦略とか、うーん、まぁ、パンクですよ、パンク(適当)。

来年

テーマは「クライアントサイドとサーバーサイドをC#で統一することのメリットの実証」「さらにリアルタイムネットワークもC#で統一」「のためのヒットアプリケーションの創出」です。指向はあんま変わってないんですが、より具体的に。来年は動く年かな、といったところなので是非期待してください。

Unityのコルーチンの分解、或いはUniRxのMainThreadDispatcherについて

この記事はUnity Advent Calendar 2014のための記事になります。昨日はkomiyakさんのUnity を使いはじめたばかりの頃の自分に伝えたい、Unity の基本 【2014年版】でした。いやー、これはまとまってて嬉しい情報です。ところでカレンダー的には穴開けちゃってます(遅刻遅延!)、すみません……。

さて、今回の内容ですが、私の作っているUniRxというReactive Programming(バズワード of 2014!)のためのライブラリを、最近ありがたいことに結構使ってみたーという声を聞くので、Rxの世界とUnityの世界を繋ぐ根幹である、MainThreadDispatcherと、その前準備に必要なコルーチンについて書きます。

Coroutine Revisited

コルーチンとはなんぞや。なんて今更ですって!はい。とりあえず、Unityは基本的にシングルスレッドで動いています。少なくともスクリプト部分に関しては。Unityのコルーチンは、IEnumeratorでyield returnすると、その次の処理を次フレーム(もしくは一定秒数/完了後などなど)に回します。あくまでシングルスレッド、ということですね。挙動について。簡単な確認用スクリプトを貼っつけて見てみると……

void Start()
{
    Debug.Log("begin-start:" + Time.frameCount);
    StartCoroutine(MyCoroutine());
    Debug.Log("end-start" + Time.frameCount);
}

IEnumerator MyCoroutine()
{
    Debug.Log("start-coroutine:" + Time.frameCount);

    yield return null;
    Debug.Log("after-yield-null:" + Time.frameCount);

    yield return new WaitForSeconds(3);
    Debug.Log("end-coroutine:" + Time.frameCount);
}

呼ばれる順番とframeCountを考えてみようクイズ!意外と引っかかるかもしれません。答えのほうですが……

begin-start:1
start-coroutine:1
end-start:1
after-yield-null:2
end-coroutine:168

となります。最後の秒数のフレームカウントはどうでもいいとして、start-coroutineが呼ばれるのはend-startの前ってのがちょっとだけヘーってとこかしら。IEnumerator自体はUnity固有の機能でもなく、むしろC#の標準機能で、通常は戻り値を持ってイテレータを生成するのに使います(Pythonでいうところのジェネレータ)

// 偶数のシーケンスを生成
IEnumerable<int> EvenSequence(int from, int to)
{
    for (int i = from; i <= to; i++)
    {
        if (i % 2 == 0)
        {
            yield return i;
        }
    }
}

void Run()
{
    var seq = EvenSequence(1, 10);

    // シーケンスはforeachで消費可能
    foreach (var item in seq)
    {
        Debug.Log(item);
    }

    // あるいはEnumeratorを取得し回す(foreachは↓のコードを生成する)
    // Unityでのコルーチンでの利用され方はこっちのイメージのほうが近い
    using (var e = seq.GetEnumerator())
    {
        while (e.MoveNext())
        {
            Debug.Log(e.Current);
        }
    }
}

Unityのコルーチンとしてのイテレータの活用法は、戻り値を原則使わず(宣言がIEnumerator)、yield returnとyield returnの間に副作用を起こすために使うということですね。これはこれで中々ナイスアイディアだとは思ってます。

言語システムとしてはC#そのままなので、誰かがIEnumeratorを消費しているということになります。もちろん、それはStartCoroutineで、呼んだ瞬間にまずはMoveNext、その後はUpdateに相当するようなタイミングで毎フレームMoveNextを呼び続けているようなイメージ。

擬似的にMonoBehaviourで再現すると

public class CoroutineConsumer : MonoBehaviour
{
    public IEnumerator TargetCoroutine; // 何か外からセットしといて

    void Update()
    {
        if (TargetCoroutine.MoveNext())
        {
            var current = TargetCoroutine.Current;
            // 基本的にCurrent自体はそんな意味を持たないで次フレームに回すだけ
            if (current == null)
            {
                // next frame
            }
            // ただしもし固有の何かが返された時はちょっとした別の挙動する
            if (current is WaitForSeconds)
            {
                // なんか適当に秒数待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            else if (current is WWW)
            {
                // isDoneになってるまで適当に待つ(ThreadをSleepするんじゃなく挙動的には次フレームへ)
            }
            // 以下略
        }
    }
}

こんな感じでしょうか!yield returnで返す値が具体的にUnityのゲームループにおいてどこに差し込まれるかは、UnityのマニュアルのScript Lifecycle Flowchartの図を見るのが分かりやすい。

nullが先頭でWaitForEndOfFrameは末尾なのね、とか。yield returnで返して意味を持つ値はYieldInstruction、ということになっているはずではあるんですが、実際のとこWWWはYieldInstructionじゃないし、YieldInstruction自体はカスタマイズ不能で自分で書けるわけじゃないんで(イマイチすぎる……)なんだかなぁー。Lifecycle Flowchartに書かれていない中でyield可能なのはAsyncOperationかな?

もしイテレータの挙動について更に詳しく知りたい人は、私の以前書いたスライドAn Internal of LINQ to Objectsの14Pを参照してくださいな。

UniRx.FromCoroutine

というわけかで(一旦)コルーチンの話はおしまい。ここからはUniRxの話。UniRxについてはneue cc - A Beginners Guide to Reactive Extensions with UniRxあたりをどうぞ。UniRxはFromCoroutineメソッドにより、コルーチンをUniRxの基盤インターフェースであるIObservable<T>に変換します。

// こんなのがあるとして
IEnumerator CoroutineA()
{
    Debug.Log("a start");
    yield return new WaitForSeconds(1);
    Debug.Log("a end");
}

// こんなふうに使える
Observable.FromCoroutine(CoroutineA)
    .Subscribe(_ => Debug.Log("complete"));
    
// 戻り値のあるバージョンがあるとして
IEnumerator CoroutineB(IObserver<int> observer)
{
    observer.OnNext(100);
    yield return new WaitForSeconds(2);
    observer.OnNext(200);
    observer.OnCompleted();
}

// こんなふうに合成もできる
var coroutineA = Observable.FromCoroutine(CoroutineA);
var coroutineB = Observable.FromCoroutine<int>(observer => CoroutineB(observer));

// Aが終わった後にBの起動、Subscribeには100, 200が送られてくる
var subscription = coroutineA.SelectMany(coroutineB).Subscribe(x => Debug.Log(x));

// Subscribeの戻り値からDisposeを呼ぶとキャンセル可能
// subscription.Dispose();

IObservable<T>になっていると何がいいかというと、合成可能になるところです。Aが終わった後にBを実行する、Bが失敗したらCを実行する、などなど。また、戻り値を返すことができるようになります。そして、コルーチンに限らず、あらゆるイベント、あらゆる非同期がIObservable<T>になるので、全てをシームレスに繋ぎ合わせることができる。そこが他のライブラリや手法と一線を画すRxの強みなんです、が、長くなるのでここでは触れません:)

また、MonoBehaviour.StartCoroutineを呼ばなくてもコルーチンが起動しています。これは結構大きな利点だと思っていて、というのも、コルーチンを使うためだけにMonoBehaviourにする必要がなくなる。やはり普通のC#クラスのほうが取り回しが良いので、MonoBehaviourにする必要がないものはしないほうがいい。けれど、コルーチンは使いたい。そうした欲求に応えてくれます。

更にFromCoroutine経由にするとEditor内部では通常は動かせないコルーチンを動かすことができます!(これについては後で説明します)

といった応用例はそのうちやるということで、とりあえずFromCoroutineの中身を見て行きましょう。

// Func<IEnumerator>はメソッド宣言的には「IEnumerator Hoge()」になる
public static IObservable<Unit> FromCoroutine(Func<IEnumerator> coroutine, bool publishEveryYield = false)
{
    return FromCoroutine<Unit>((observer, cancellationToken) => WrapEnumerator(coroutine(), observer, cancellationToken, publishEveryYield));
}

// ↑のはWrapEnumeratorを介してこれになっている
public static IObservable<T> FromCoroutine<T>(Func<IObserver<T>, CancellationToken, IEnumerator> coroutine)
{
    return Observable.Create<T>(observer =>
    {
        var cancel = new BooleanDisposable();

        MainThreadDispatcher.SendStartCoroutine(coroutine(observer, new CancellationToken(cancel)));

        return cancel;
    });
}

// WrapEnumeratorの中身は(オェェェェ
static IEnumerator WrapEnumerator(IEnumerator enumerator, IObserver<Unit> observer, CancellationToken cancellationToken, bool publishEveryYield)
{
    var hasNext = default(bool);
    var raisedError = false;
    do
    {
        try
        {
            hasNext = enumerator.MoveNext();
        }
        catch (Exception ex)
        {
            try
            {
                raisedError = true;
                observer.OnError(ex);
            }
            finally
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
            }
            yield break;
        }
        if (hasNext && publishEveryYield)
        {
            try
            {
                observer.OnNext(Unit.Default);
            }
            catch
            {
                var d = enumerator as IDisposable;
                if (d != null)
                {
                    d.Dispose();
                }
                throw;
            }
        }
        if (hasNext)
        {
            yield return enumerator.Current; // yield inner YieldInstruction
        }
    } while (hasNext && !cancellationToken.IsCancellationRequested);

    try
    {
        if (!raisedError && !cancellationToken.IsCancellationRequested)
        {
            observer.OnNext(Unit.Default); // last one
            observer.OnCompleted();
        }
    }
    finally
    {
        var d = enumerator as IDisposable;
        if (d != null)
        {
            d.Dispose();
        }
    }
}

WrapEnumeratorの中身が長くてオェェェって感じなんですが何やってるかというと、元のコルーチンを分解して、Rx的に都合のいい形に再構築したコルーチンに変換してます。都合のいい形とは「キャンセル可能」「終了時(もしくは各yield時)にObserver.OnNextを呼ぶ」「全ての完了時にObserver.OnCompletedを呼ぶ」「エラー発生時にObserver.OnErrorを呼ぶ」を満たしているもの。コルーチン自体がC#の標準機能のままで、なにも特別なことをしていないなら、別に自分で回す(enumerator.MoveNextを手で呼ぶ)ことも、何も問題はない、わけです。

そんなラップしたコルーチンを動かしているのがMainThreadDispatcher.SendStartCoroutine。今のMainThreadDispatcher.csは諸事情あって奇々怪々なんですが、SendStartCoroutineのとこだけ取り出すと

public sealed class MainThreadDispatcher : MonoBehaviour
{
    // 中略
    
    /// <summary>ThreadSafe StartCoroutine.</summary>
    public static void SendStartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return; }
#endif

        if (mainThreadToken != null)
        {
            StartCoroutine(routine);
        }
        else
        {
            Instance.queueWorker.Enqueue(() => Instance.StartCoroutine_Auto(routine));
        }
    }

    new public static Coroutine StartCoroutine(IEnumerator routine)
    {
#if UNITY_EDITOR
        if (!Application.isPlaying) { EditorThreadDispatcher.Instance.PseudoStartCoroutine(routine); return null; }
#endif

        return Instance.StartCoroutine_Auto(routine);
    }
}

if UNITY_EDITORのところは後で説明するのでスルーしてもらうとして、基本的にはInstance.StartCoroutine_Autoです。ようはMainThreadDispatcherとは、シングルトンのMonoBehaviourであり、FromCoroutineはそいつからコルーチンを起動しているだけなのであった。なんだー、単純。汚れ仕事(コルーチンの起動、MonoBehaviourであること)をMainThreadDispatcherにだけ押し付けることにより、それ以外の部分が平和に浄化される!

コルーチンの起動が一極集中して、それで実行効率とか大丈夫なの?というと存外大丈夫っぽいので大丈夫。実際、私の会社ではこないだ一本iOS向けにゲームをリリースしましたがちゃんと動いてます。しかしそうなるとStartCoroutineはMonoBehaviourのインスタンスメソッドではなく、静的メソッドであって欲しかった……。

その他、SendStartCoroutineはスレッドセーフ(他スレッドから呼ばれた場合はキューに突っ込んでメインスレッドに戻ってから起動する)なのと、UnityEditorからの起動を可能にしています(EditorThreadDispatcher.Instance.PseudoStartCoroutine経由で起動する)。なので、普通にStartCoroutineを呼ぶ以上のメリットを提供できているかな、と。

UnityEditorでコルーチンを実行する

Editorでコルーチンを動かせないのは存外不便です。WWWも動かせないし……。UniRxではFromCoroutine経由で実行すると、内部でMainThreadDispatcher.SendStartCoroutine経由になることにより、Editorで実行できます。使い方は本当にFromCoroutineしてSubscribeするだけ、と、通常時のフローとまるっきり一緒です。ここで毎回エディターの時は、通常の時は、と書き分けるのはカッタルイですからね。汚れ仕事はMainThreadDispatcherが一手に引き受けています。そんな汚れ仕事はこんな感じの実装です。

class EditorThreadDispatcher
{
    // 中略
    
    ThreadSafeQueueWorker editorQueueWorker= new ThreadSafeQueueWorker();

    EditorThreadDispatcher()
    {
        UnityEditor.EditorApplication.update += Update;
    }
    
    // 中略
    
    void Update()
    {
        editorQueueWorker.ExecuteAll(x => Debug.LogException(x));
    }

    // 中略

    public void PseudoStartCoroutine(IEnumerator routine)
    {
        editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine));
    }

    void ConsumeEnumerator(IEnumerator routine)
    {
        if (routine.MoveNext())
        {
            var current = routine.Current;
            if (current == null)
            {
                goto ENQUEUE;
            }

            var type = current.GetType();
            if (type == typeof(WWW))
            {
                var www = (WWW)current;
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitWWW(www, routine)));
                return;
            }
            else if (type == typeof(WaitForSeconds))
            {
                var waitForSeconds = (WaitForSeconds)current;
                var accessor = typeof(WaitForSeconds).GetField("m_Seconds", BindingFlags.Instance | BindingFlags.GetField | BindingFlags.NonPublic);
                var second = (float)accessor.GetValue(waitForSeconds);
                editorQueueWorker.Enqueue(() => ConsumeEnumerator(UnwrapWaitForSeconds(second, routine)));
                return;
            }
            else if (type == typeof(Coroutine))
            {
                Debug.Log("Can't wait coroutine on UnityEditor");
                goto ENQUEUE;
            }

        ENQUEUE:
            editorQueueWorker.Enqueue(() => ConsumeEnumerator(routine)); // next update
        }
    }

    IEnumerator UnwrapWaitWWW(WWW www, IEnumerator continuation)
    {
        while (!www.isDone)
        {
            yield return null;
        }
        ConsumeEnumerator(continuation);
    }

    IEnumerator UnwrapWaitForSeconds(float second, IEnumerator continuation)
    {
        var startTime = DateTimeOffset.UtcNow;
        while (true)
        {
            yield return null;

            var elapsed = (DateTimeOffset.UtcNow - startTime).TotalSeconds;
            if (elapsed >= second)
            {
                break;
            }
        };
        ConsumeEnumerator(continuation);
    }
}

ようは、UnityEditor.EditorApplication.updateでジョブキューを回しています。コルーチン(Enumerator)を手動で分解して、EditorApplication.updateに都合の良い形に再編しています。yield return nullがあったらキューに突っ込んで次のupdateに回すことで、擬似的にStartCorotineを再現。WaitForSecondsだったらリフレクションで内部の秒数を取ってきて(ひどぅい)ぐるぐるループを展開。などなど。

仕組み的には単純、なんですが結構効果的で便利かな、と。ユーザーは全くそれを意識する必要がないというのが一番いいトコですね。

ちなみにアセットストアからダウンロードできるバージョンでは、まだこの仕組みは入ってません(すびばせん!)。GitHubの最新コードか、あとは、ええと、近いうちにアップデート申請しますので来年には使えるようになっているはずです。。。

まとめ

コルーチンをコルーチンたらしめているのは消費者であるStartCoroutineであって、IEnumerator自体はただのイテレータにすぎない。なので、分解も可能だし、他の形式に展開することもできる。

UniRx経由でコルーチンを実行すると「色々なものと合成できる」「(複数の)戻り値を扱える」「キャンセルが容易」「MonoBehaviourが不要」「スレッドセーフ」「エディターでも実行可能」になる。いいことづくめっぽい!Reactive Programmingの力!そんな感じに、UniRxはなるべくシームレスにRxの世界とUnityの世界を繋げるような仕組みを用意しています。是非ダウンロードして、色々遊んでみてください。

VS2015+RoslynによるCodeRefactoringProviderの作り方と活用法

この記事はC# Advent Calendar 2014のための記事になります。私は去年のAdvent Calendarでは非同期時代のLINQというものを書いていました、うん、中々良い記事であった(自分で言う)。今年のテーマはRoslynです。

先月にVS2015のRoslynでCode Analyzerを自作する(ついでにUnityコードも解析する)という記事を書きましたが、VS2015 PreviewではRoslynで作る拡張にもう一つ、Code Refactoringがあります。こちらも簡単に作れて、中々ベンリなので(前にVS2015のRoslynは以前から後退して「あんま大したことはできない」と言いましたが、それはそれでかなり役立ちです)、是非作っていきましょう。Code Analyzerと同じく非常に簡単に作れるのと、テンプレートがよくできていてちゃんとガイドになってるので、すんなり入れるかと思います。なお、こちらはCode Analyzerと違いNuGet配布やプロジェクト単位での参照は不可能、VSIXのみ。そこはちょっと残念……。

Code Refactoring

下準備としてはCode Analyzerの時と同じくVisual Studio 2015 Previewのインストールの他に、Visual Studio 2015 Preview SDK.NET Compiler Platform SDK Templates、そして.NET Compiler Platform Syntax Visualizerを入れてください。

さて、まずテンプレートのVisual C#→Extensibilityから「Code Refactoring(VSIX)」を選びます。とりあえずこのテンプレート(がサンプルになってます)をCtrl+F5で実行しましょう。これで立ち上がるVSは通常のVSに影響を及ぼさず自作拡張がインストールされる特殊なインスタンスになってます(devenv.exeを引数「/rootsuffix Roslyn」で立ち上げてる、のがDebugのとこで確認できる)。というわけで、このテンプレートの拡張によりクラス名をCtrl+.することにより

クラス名が逆になる、という(実にどうでもいい)機能拡張が追加されました!と、いうわけで、Code RefactoringはCode Analyzerと同じく「Light Bulb」によるCode Actionが実装可能になります。Code AnalyzerはDiagnosticが起点でしたが、こちらは、指し示された位置を起点にコードの削除/追加/変更を行えるという感じ。「リファクタリング」というとコード修正のイメージが個人的には強いんですが、RoslynのCode Refactoringはどちらかというと「コード生成」に使えるな、という印象です。例えばプロパティを選択してCode RefactoringでINotifyPropertyChangedのコードに展開してしまうとか。大量に作る場合、一個一個コードスニペットで作るより、そっちのほうが速く作れそうですよね?など、色々使い手はあるでしょふ。結構可能性を感じるし、良い機能だと思っています(それR#で今までも出来たよ!とかそれEclipseで既に!とか言いたいことはあるかもしれませんが!)

ただまあ、Code Refactoringって名前は好きじゃない。けれど、じゃあ何がいいかっていうと、なんでしょうねぇ。Code Generate、ジェネレートだけじゃないから、まぁCode Actionかなぁ。AnalyzerもCode Actionだから区別付かなくて嫌だって可能性もあるか、うーん、うーん、ま、いっか……。

ArgumentNullExceptionProvider

サンプルコードがたった1ファイルのように、作るのはとても簡単です。CodeRefactoringProviderを継承してComputeRefactoringsAsyncを実装する、だけ。適当にシンタックスツリーを探索して、もしLight Bulbを出したければcontext.RegisterRefactoringにCodeActionを追加する、と。

というわけで早速何か一個作ってみましょう。実用的なのがいいなぁ、ということでnullチェック、if(hoge == null) throw new ArgumetNullException(); というクソ面倒くさい恒例のアレを自動生成しよう!絶対使うし、あるとめちゃくちゃ捗りますものね!

上の画像のが完成品です。便利そう!便利そう!

さて、まずはLight Bulbをどこで出したいか。メソッドの引数で出すんで、引数を選択してたらそれは対象にしたいかなぁ?あと、一々選択するのも面倒だから、メソッド名でも出しましょうか。ふむ、とりあえず実装が簡単そうなメソッド名だけで行きましょう。何事も作る時は単純なところから広げていくのが一番、特に初めてのものはね。

// ArgumentNullExceptionProviderという名前でプロジェクト作ったらクラス名が酷いことに、その辺はちゃんと調整しましょふ
[ExportCodeRefactoringProvider(ArgumentNullExceptionProviderCodeRefactoringProvider.RefactoringId, LanguageNames.CSharp), Shared]
internal class ArgumentNullExceptionProviderCodeRefactoringProvider : CodeRefactoringProvider
{
    public const string RefactoringId = "ArgumentNullExceptionProvider";

    public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
    {
        // とりあえずコード全体を取る(これはほとんど定形)
        var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
        
        // SemanticModel(コードをテキストとしてではなく意味を持ったモデルとして取るようにするもの、これもほぼ必須)
        var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
        
        // context.Spanが選択位置、ということで選択位置のコードを取る(この辺もほぼ定形かな)
        // もし選択範囲に含まれてるものから一部のものを取り出す、とかならroot.DescendantNodes(context.Span).OfType<XxxSyntax>() という手とか色々
        var node = root.FindNode(context.Span);
        
        // メソッド定義じゃなかったら無視
        var methodDecl = node as MethodDeclarationSyntax;
        if (methodDecl == null) return;
        
        // コード生成作る
        var action = CodeAction.Create("Generate ArgumentNullException", c => GenerateArgumentNullException(context.Document, model, root, methodDecl, c));
        
        // とりあえず追加
        context.RegisterRefactoring(action);
    }

    async Task<Document> GenerateArgumentNullException(Document document, SemanticModel model, SyntaxNode root, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)
    {
        // あとで書く:)
        return document;
    }
}

まずはこんなとこですね、ようはサンプルからClassDeclarationSyntaxをMethodDeclarationSyntaxに変えただけ + CodeActionを作るところの引数にSemanticModelとrootのSyntaxNodeを足してあります。この辺はほとんど定形で必要になってくるので、とりあえず覚えておくといいでしょう。さて、一旦こいつで実行してみて、ちゃんとLight Bulbが思ったところに出るか確認してから次に行きましょー。続いて本題のコード生成部分。

Task<Document> GenerateArgumentNullException(Document document, SemanticModel model, SyntaxNode root, MethodDeclarationSyntax methodDecl, CancellationToken cancellationToken)
{
    // 引数はParameterListから取れる。
    // この辺はVSでMethodDeclarationSyntaxをF12で飛んで、metadataからそれっぽいのを探しだすといいんじゃないかな?
    // ドキュメントがなくてもVisual StudioとIntelliSenseがあれば、なんとなく作れてしまうのがC#のいいところだからね!
    var parameterList = methodDecl.ParameterList;

    // ただのType(TypeSyntax)はコード上のテキスト以上の意味を持たない、
    // そこからstructかclassか、など型としての情報を取るにはSemanticModelから照合する必要がある
    var targets = parameterList.Parameters
        .Where(x =>
        {
            var typeSymbol = model.GetTypeInfo(x.Type).Type;
            return typeSymbol != null && typeSymbol.IsReferenceType;
        });

    // C#コードを手組みするのは(Trivia対応とか入れると)死ぬほど面倒なのでParseする
    var statements = targets.Select(x =>
    {
        var name = x.Identifier.Text;
        // String Interpolationベンリ(ただし文法はまだ変更される模様……)
        return SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
    }).ToArray();

    // 追加、メソッドBodyはBody以下なのでそこの先頭に(AddStatementsだと一番下に置かれてしまうのでダメ、nullチェックは「先頭」にしたい)
    var newBody = methodDecl.Body.WithStatements(methodDecl.Body.Statements.InsertRange(0, statements));

    // 入れ替え
    var newRoot = root.ReplaceNode(methodDecl.Body, newBody);
    var newDocument = document.WithSyntaxRoot(newRoot);

    return Task.FromResult(newDocument);
}

SemanticModelがキーです。SyntaxTreeから取ってきただけのものは、何の情報も持ってません、ほんとただのコード上の字面だけです。今回で言うとnullチェックしたいのは参照型だけですが、それを識別することが出来ません。そこからTypeInfoを取り出すことができるのがSemanticModelになります。例えば「Dictionary」から「System.Collections.Generic.Dictionary」といったフルネームを取り出したりなど、とかくコード操作するには重要です。今回はこれでIsReferenceTypeを引き出しています。

あとは、コードの手組みは辛すぎるのでSyntaxFactory.ParseXxxを活用して済ませちゃうのは楽です。その後は、rootからReplaceと、documentから丸っと差し替えなどは定形ですね。あ、そうそう、あとnameofはC# 6.0の新機能です。ベンリベンリ。

では、実際使ってみると……。

// これが
static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
{
}

// こうなる、あ、れ……?
static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
{
if (a == null) throw new ArgumentNullException(nameof(a));if (b == null) throw new ArgumentNullException(nameof(b));if (huga == null) throw new ArgumentNullException(nameof(huga));}

はい。ちゃんと機能するコードができてはいます。が、なんじゃこりゃーーーーー。なんでかっていうとTrivia(空白とか改行)が一切考慮されてないから、なんですね。Code Refactoringを作る上でメンドウクサイのは、こうしたTriviaへの考慮です。置換なら既存のTriviaをそのまま使えるんですが、コード追加系だと、自分でTrivia入れたり削ったりの調整しないと見れたもんじゃなくなります。というわけで、こっから先が地獄……。まぁ、頑張りましょう。さすがにそのままだと使えないので、調整しましょう。

まずは改行です。改行は結構簡単で、(大抵の場合)TrailingTrivia(後方のTrivia(空白や改行など))にCRLFを仕込むだけ。

// statementsのところをこう変更すると
var statements = targets.Select(x =>
{
    var name = x.Identifier.Text;
    var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");
    // TrailingTriviaは行末、というわけで改行を仕込む
    return statement.WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
})
.ToArray();

        // 置換結果はこうなります、だいぶ良くなった!
        static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
        {
if (a == null) throw new ArgumentNullException(nameof(a));
if (b == null) throw new ArgumentNullException(nameof(b));
if (huga == null) throw new ArgumentNullException(nameof(huga));
        }

だいぶいい線いってますね!このぐらいできれば、あとは実行後にCtrl+K, D(ドキュメントフォーマット)押してね、で済むんで全然妥協ラインです。が、もう少し完璧にしたいならインデントも挟みましょうか。インデントの量は直前の{から引っ張ってきて調整してみましょふ。

// WithLeadingTriviaの追加
var statements = targets.Select(x =>
{
    var name = x.Identifier.Text;
    var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");

    // LeadingTriviaに「{」のとこのインデント + 4つ分の空白を入れる
    return statement
        .WithLeadingTrivia(methodDecl.Body.OpenBraceToken.LeadingTrivia.Add(SyntaxFactory.Whitespace("    ")))
        .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed);
})
.ToArray();

        // 置換結果はこうなります、完璧!
        static void Hoge(string[] a, string b, Dictionary<int, int> huga, int tako)
        {
            if (a == null) throw new ArgumentNullException(nameof(a));
            if (b == null) throw new ArgumentNullException(nameof(b));
            if (huga == null) throw new ArgumentNullException(nameof(huga));
        }

こんなところですね。さて、勿論このコードはOpenBraceTokenが改行された位置にあることを前提にしているので、後ろに{を入れるスタイルのコードには適用できません。また、空白4つをインデントとして使うというのが決め打ちされています。また、なんども実行しても大丈夫なように既にthrow new ArgumentNullExceptionが記述されてる引数は無視したいよねえ、などなど、完璧を求めるとキリがありません。キリがないということは、適当なところでやめておくのが無難ということです、適度な妥協大事!

フォーマットする

とはいえ、フォーマットはもう少しきちんとやりたいところです。実は簡単にやる手段が用意されていて、.WithAdditionalAnnotations(Formatter.Annotation) を呼ぶことで、その部分だけフォーマットがかかります。正確にはフォーマットが可能になるタイミングでフォーマットがかかるようになります、どういうことかというと、例えばインデントのフォーマットは前後のコード情報がなければかけることは出来ません。このコード例でいうとif()...は前後空白もない完全一行だけなのでフォーマットもなにもできない。なのでAnnotationのついたコード片がフォーマット可能なドキュメントにくっついたタイミングで自動でかかるようになります。AnnotationはRoslynの構文木内でのみ使われるオプション情報とでも思ってもらえれば。

var statements = targetParameters
    .Select(x =>
    {
        var name = x.Identifier.Text;
        var statement = SyntaxFactory.ParseStatement("if (\{name} == null) throw new ArgumentNullException(nameof(\{name}));");

        // Formatter.Annotationつけてフォーマット
        return statement
            .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
            .WithAdditionalAnnotations(Formatter.Annotation);
    })
    .ToArray();

これは簡単でイイですね!!!なのでTriviaの付与は、最低限のCRLFぐらいを部分的に入れるだけでOK。これは、これからもめちゃくちゃ多用するのではかと思われます。他にAnnotationにはSimplifier.Annotationなどが用意されてます。

フルコード

最初に妥協した(?)引数を選択してたらそれも対象に、ってコードも入れましょうか。というのを含めたフルコードは以下になります。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Grani.RoslynTools
{
    [ExportCodeRefactoringProvider(GenerateArgumentNullExceptionCodeRefactoringProvider.RefactoringId, LanguageNames.CSharp), Shared]
    internal class GenerateArgumentNullExceptionCodeRefactoringProvider : CodeRefactoringProvider
    {
        public const string RefactoringId = "GenerateArgumentNullException";

        public sealed override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
        {
            // とりあえずコード全体を取る(これはほとんど定形)
            var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);

            // SemanticModel(コードをテキストとしてではなく意味を持ったモデルとして取るようにするもの、これもほぼ必須)
            var model = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);

            // context.Spanが選択位置、ということで選択位置のコードを取る(この辺もほぼ定形かな)
            var node = root.FindNode(context.Span);

            // パラメータリストの場合はそれだけ、クラス名だったらその定義の全部の引数を取る
            MethodDeclarationSyntax methodDecl;
            IEnumerable<ParameterSyntax> selectedParameters;
            if (node is MethodDeclarationSyntax)
            {
                methodDecl = (MethodDeclarationSyntax)node;
                selectedParameters = methodDecl.ParameterList.Parameters;
            }
            else
            {
                // 単品選択のばやい(名前の部分選択でParameterSyntax、型の部分選択でParentがParameterSyntax)
                if (node is ParameterSyntax || node.Parent is ParameterSyntax)
                {
                    var targetParameter = (node as ParameterSyntax) ?? (node.Parent as ParameterSyntax);
                    // 親方向にMethodDeclarationSyntaxを探す
                    methodDecl = targetParameter.Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault();
                    selectedParameters = new[] { targetParameter };
                }
                else
                {
                    // 選択範囲から取り出すばやい
                    var parameters = root.DescendantNodes(context.Span).OfType<ParameterSyntax>().ToArray();
                    if (parameters.Length == 0) return;
                    methodDecl = parameters[0].Ancestors().OfType<MethodDeclarationSyntax>().FirstOrDefault();
                    selectedParameters = parameters;
                }
            }
            if (methodDecl == null) return;

            // ただのType(TypeSyntax)はコード上のテキスト以上の意味を持たない、
            // そこからstructかclassか、など型としての情報を取るにはSemanticModelから照合する必要がある
            var replaceTargets = selectedParameters
                .Where(x =>
                {
                    var typeSymbol = model.GetTypeInfo(x.Type).Type;
                    // ジェネリック型で型引数がclassでstructでもない場合はIsXxxが両方false、これはif(xxx == null)の対象にする
                    return typeSymbol != null && typeSymbol.IsReferenceType || (!typeSymbol.IsReferenceType && !typeSymbol.IsValueType);
                })
                .ToArray();

            if (replaceTargets.Length == 0) return;

            // コード生成作る(nameof利用の有無で2つ作ってみたり)
            var action1 = CodeAction.Create("Generate ArgumentNullException", c => GenerateArgumentNullException(context.Document, root, methodDecl, replaceTargets, true, c));
            var action2 = CodeAction.Create("Generate ArgumentNullException(unuse nameof)", c => GenerateArgumentNullException(context.Document, root, methodDecl, replaceTargets, false, c));

            // 追加
            context.RegisterRefactoring(action1);
            context.RegisterRefactoring(action2);
        }
        
        Task<Document> GenerateArgumentNullException(Document document, SyntaxNode root, MethodDeclarationSyntax methodDecl, ParameterSyntax[] targetParameters, bool useNameof, CancellationToken cancellationToken)
        {
            // nameof版と非nameof版を用意
            var template = (useNameof)
                ? "if ({0} == null) throw new ArgumentNullException(nameof({0}));"
                : "if ({0} == null) throw new ArgumentNullException(\"{0}\");";

            var statements = targetParameters
                .Select(x =>
                {
                    // C#コードを手組みするのは(Trivia対応とか入れると)死ぬほど面倒なのでParseする
                    var name = x.Identifier.Text;
                    var statement = SyntaxFactory.ParseStatement(string.Format(template, name));

                    // Formatter.Annotationつけてフォーマット
                    return statement
                        .WithTrailingTrivia(SyntaxFactory.CarriageReturnLineFeed)
                        .WithAdditionalAnnotations(Formatter.Annotation);
                })
                .ToArray();

            // 追加
            var newBody = methodDecl.Body.WithStatements(methodDecl.Body.Statements.InsertRange(0, statements));

            // 入れ替え
            var newRoot = root.ReplaceNode(methodDecl.Body, newBody);
            var newDocument = document.WithSyntaxRoot(newRoot);

            return Task.FromResult(newDocument);
        }
    }
}

まずLight Bulbを出すためのチェックがFindNodeだけでは済まなくなるので、root.DescendantNodes(context.Span)が活躍します。あとは、もう、色々もしゃもしゃと。とにかくコーナーケース探していくとかなり面倒くさかったり。例えば、引数の型の部分を選択した時と、名前の部分を選択した時の対処、などなど……。しょうがないけれどね。

それと、もしメソッドの中に参照型の引数がなかったらLight Bulbを出さないようにするため、replaceTargetsの生成をRegisterRefactoringの前に変更しています。それと、ジェネリックな型引数の場合で制約がついてない状態も生成対象に含めるよう微調整。といったような、この辺の細かい調整はある程度出来上がってからやってくのが良いでしょうねー。

Portable?

ところで、CodeRefactoringにせよAnalyzerにせよ、テンプレートではコア部分はPCLで生成されています。ということは、実は、System.IOとか使えない。これ、ちょっと色々な邪道な操作したい時に不便なんですよ……。ていうかVisual Studio前提なんだからPCLである必要ないじゃん!いみわからない、なんでもPCLって言っておけばいいってもんでもないでしょう!あー、もう私はPCL大嫌いだよぅー。しかもPCLで生成されたプロジェクトは普通のプロジェクトタイプには簡単には戻せないんですよ、うわぁ……。

まあcsprojを手で書き換えればできます。やりかたは

<!-- こいつを消す -->
<ProjectTypeGuids>{786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>

<!-- こいつを消す -->
<TargetFrameworkProfile>Profile7</TargetFrameworkProfile>

<!-- こいつを消して -->
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
<!-- かわりにこれを追加 -->
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

なんだかなぁー。

ビルトイン拡張

VS2015ではビルトインで幾つかCode Refactoringが入っています。が、ノーヒント(Ctrl+.を押すまではLight Bulbが出ない)なので、宝探し状態です!例えばプロパティを選択すると「Generate Constructor」が出てきたり。まぁ、これはそのうちリストが公開されるでしょう。

まとめ

拡張を作るのは簡単!うーん、んー、簡単?まぁ簡単!少なくとも今までよりは比較的遥かに簡単カジュアルに作れるようになりました。こうした自動生成って、一般的なものだけではなく、プロジェクト固有で必要になるものもあると思います。例えばうちの会社ではシリアライザにprotobuf-netを使っているので、クラスのプロパティの各属性にDataMemberをつけて回る必要がある。ちゃんとOrderの順番を連番にして。でも手作業は面倒、そこで、CodeRefactoring。

こんな風にプロパティを選択してCtrl+.でほいっと生成できる。このコードはちゃんと属性の振り直しとかも配慮してある(場合によって追加、場合によって置き換え、とかのコードを書くのはやっぱり結構面倒!フルコードはneuecc/AddDataMemberWithOrderCodeRefactoringProvider.csに置いておきます、使いたい方 もしくは CodeRefactoring作りの参考にどうぞ)

こうしたプロジェクト固有の必要な自動生成のためのコードがサクッと作れるようになったのは地味に革命的かなぁ、なんて思ってます。どんどん活用されていくといいですね、少なくともうちの会社(グラニ)では社内用に皆で色々作って配布していこうかなあと計画しています(ところでグラニはまだまだアクティブにエンジニアの求人してます!最先端のC#に興味のある方は是非是非どうぞ、「今から」VS2015を使い倒しましょう)。

AnalyzerもCode Refactoringも、すっごくミニマムな仕様に落ち着いていて、限りなく削っていった形なんだろうなぁ、と想像します。すごく小さくて、でも、すごく使い手がある。いい落とし所だと思ってます。あとはもう活用するもしないも全て自分達次第。とにかくまずは、是非触ってみてください。可能性を感じましょう。

kpm wrapでVS2015 Previewでもcsprojをkprojで参照する

この記事は土壇場駆け込みで爆誕したASP.NET Advent Calendar 2014の3日目の記事です。昨日はASP.NET Web API で Jil JSON Serializer を使ってみるでした。Jilいいよね、うん、使われるようになるといいと思ってます。

さて、次のASP.NET(ASP.NET 5/vNext)は、もうみんな薄々気付いていると思いますが、Web APIとかOwinとかは結局vNextにおいて……。口には出せなくても!絶対にオフィシャルにアナウンスされることはなくても!ふむ、まぁ、その辺は置いておきましょう、はい。そんな不穏なことはね!ね!というわけでそんなことは全く関係なく今回はkpm wrapの話です。何それ、というと、多分、今の間しか使うことはないと思います。いや、むしろ今の間だからこそ直接使う人はそんないないと思います。つまり、誰にとっても全く役に立たないお話。

失われたプロジェクト参照

なんとASP.NET 5(旧vNext、ちなみにMVCは6なので割と紛らわしいネーミングじゃ)のプロジェクトでは、csprojが参照できません。へー、どーいうことなのー?というとこーいうことです。

参照設定でプロジェクト選んでも参照できない!何がnot supportedだよ、クソが!じゃあどうするか、というと、ASP.NET 5 Class Libraryという新設されたK専用のクラスライブラリプロジェクトを選べば作れる。ほぅ……、そんなポータブルじゃなさすぎるクラスライブラリ作ってたまるか、何が嬉しくてクラスライブラリをウェブ専用(っぽく)しなきゃならんのだ!

別の道としてはNuGet経由でdllを渡すとか、そーいうアプローチが推奨されてて、それは一見良いように見えて全然よくないです。csprojを参照してソースコードと直にひっつくってのはすごく大事なんです。クラスライブラリもF12でソースにダイレクトに飛べる環境を作るのはめちゃくちゃ大事なんです。NuGetでdllで分離して理想的!だとかそんなお花畑理論に乗っかるきは毛頭ない。

kpm wrap

そんなわけでものすごく憤慨して、Kマジダメだわー、ありえないわー、センスないわー、ぐらいに思ってたりなかったりしたんですが、さすがに突っ込まれる。さすがに気づく。というわけで、最新のKRuntimeは、ちゃんとkprojでもcsprojを参照できるようになっています。また、一定の手順を踏めばVS2015でも参照が可能になります。さすがに次のバージョンのVS2015では(アルファ?ベータ?)対応してくると思うので、短い寿命のお話、或いはそういう風な仕組みになっているのねのお話。

まず、aspnet/HomeからDownload ZIPしてkvm.cmdを拾ってきます。こっから先はcmdで叩いていきます。まずkvm listすると1.0.0-beta1が入ってるのを確認できるはず。VS2015 Preview同梱がそれなんですね。というわけで、1.0.0-beta2を入れましょう。「kvm install latest」コマンドだと恐らく最新のmasterバージョンになってしまってよろしくないので、バージョン指定しましょう。MyGet - aspnetvnextを見るとバージョン確認できるので、そこから任意にバージョン指定でいれましょう。しかしそれでも404 NotFoundが返ってきてうまく入れられないことがあります!その場合は上のURLのところからKRE-CLR-x86のアイコンをクリックすれば生のnupkgが拾えるのでそいつを手で解凍して↓の場所に配置しましょう、nupkgをzipに変えるだけですがちゃんとNuGet.exeで展開してもいいです(どうでもいい)

あとは「%UserProfile%.kre\packages\」のbeta2のbinのとこにkpmが転がってるので、そいつでkpm wrapコマンドを叩けばOK。あとすると

kpm wrap "c:\ToaruWebApp\src\ToaruClassLibrary"

Wrapping project 'ToaruClassLibrary' for '.NETFramework,Version=v4.5'
  Source c:\ToaruWebApp\src\ToaruClassLibrary\ToaruClassLibrary.csproj
  Target c:\ToaruWebApp\wrap\ToaruClassLibrary\project.json
  Adding bin paths for '.NETFramework,Version=v4.5'
    Assembly: ../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.dll
    Pdb: ../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.pdb

というありがたいメッセージによりラッピングが完了します。あとはGUIからAdd Referenceすれば……、まぁ当然not supportedと怒られます。が、project.jsonを手編集すればいけるようになります!dependenciesに

"ToaruClassLibrary": "1.0.0.0"

とでも足してやれば(ちゃんとIntelliSenseも効いてる)あら不思議、謎の空っぽいASP.NETライブラリが追加されてリファレンスにもきちんと追加されてコードでも参照できるようになる(ようになる時もある、なんかむしろあんまうまく行かないことのほうが多いので、なんか別の条件というか再現手順間違えてるかも……とりあえず動かなかったらしょーがないということで!)

仕組み?

wrapコマンドを実行するとglobal.jsonが

{
  "sources": [
    "src",
    "test",
    "wrap"
  ]
}

になってます。global.jsonについてはmiso_soup3 Blog - ASP.NET 5 について一部に詳しく書いてありますが、プロジェクトを探すためのルートですね。で、増えたのはwrapで、wrapフォルダにこの場合だとToaruClassLibraryというASP.NET 5クラスライブラリプロジェクトができています。wrapコマンドにより生成される実体はこいつで、こいつのproject.jsonは

{
  "version": "1.0.0.0",
  "frameworks": {
    "net45": {
      "wrappedProject": "../../src/ToaruClassLibrary/ToaruClassLibrary.csproj",
      "bin": {
        "assembly": "../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.dll",
        "pdb": "../../src/ToaruClassLibrary/obj/{configuration}/ToaruClassLibrary.pdb"
      }
    }
  }
}

何が何なのかは、十分想像できそうですね。

というわけで、KのウェブプロジェクトがASP.NET 5 クラスライブラリしか参照できないという原則に変化はありません。ただしkpm wrapコマンドを叩くことでcsprojのdllから参照を作ってくれます。まぁ、dllということでビルドしないと反映されないじゃん!とかありますが、とりあえず一応実用上は問題ないレベルにまではなっている、かな……?(もしSubModuleとかで参照されてる共通ライブラリのcsprojが更新されたとして、各自のローカルで明示的にそれをリビルドしないと変更反映されないことになって不便そうだなあ、とか辛そうな点は幾らでも探せますけね)

まとめ

まぁ、VS上だとすっごく不安定で、動いたり動かなかったりって感じなんで、現状あんま実用性はない、かな……。とりあえず、次のバージョンぐらいではcsprojの参照も行けるようになった、という確認が取れた、というだけで十二分です。ASP.NET 5は仕組みがやりすぎに複雑で、VSとの統合もうまくいってるんだかいってないんだか(例えばVS2015でついにできるようになったウォッチウィンドウ上でのラムダ式が何故かASP.NETプロジェクトでは効かない、とか)ってところですが、まぁリリース版にはその辺も解決されるでしょう、と思いたい!

さて、明日のASP.NET Advent Calendar 2014はDapperの話のようです。Dapperは私もヘヴィに使ってますからね!楽しみです(ついちょっと前まで埋まってなかったんですがギリギリ繋がったようでホッ)

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive