Archive - Programming

.NET Core時代のT4によるC#のテキストテンプレート術

C# Advent Calendar 2019用の記事となります。C# Advent Calendar 2019はその2もあって、そちらも埋まってるので大変めでたい。

さて、今回のテーマはT4で、この場合にやりたいのはソースコードジェネレートです。つまるところC#でC#を作る、ということをやりたい!そのためのツールがテンプレートエンジンです。.NETにおいてメジャーなテンプレートエンジンといえばRazorなわけですが、アレはASP.NET MVCのHTML用のViewのためのテンプレートエンジンなため、文法が全くソースコード生成に向いていません、完全にHTML特化なのです。また、利用のためのパイプラインもソースコード生成に全く向いていない(無理やりなんとか使おうとするRazorEngineといったプロジェクトもありますが……)ので、やめておいたほうが無難です。

では何を使えばいいのか、の答えがT4(Text Template Transfomration Toolkit)です。過去にはMicro-ORMとテーブルのクラス定義自動生成についてという記事で、データベースのテーブル定義からマッピング用のC#コードを生成する方法を紹介しました。テーブル定義は、実際にDB通信してもいいし、あるいは何らかの定義ファイルを解析して生成、とかでもいいですね。また、最近の私の作っているライブラリには(UnityのIL2CPP対策で)コードジェネレーターがついていますが(MagicOnion, MasterMemory, MessagePack-CSharpなど)、それらの生成テンプレートは全てT4で作っています。

元々はVisual Studioべったりでしたし、実際べったりなのですが、Mac環境ではVisual Studio for Macで、あるいは(一手間入りますが)VS Codeで使用したり、最近だとRiderが2019.3 EAP 5からばっちしサポートされたようなので、MacやRider派の人も安心です。RiderのT4サポートは曰く

You asked us to support T4, and we’ve answered! T4 support is here, based on our own generator and available as a pre-installed plugin.
The plugin is open-sourced (https://github.com/JetBrains/ForTea) and your contributions are very welcome.
Feature-rich C# support in code blocks includes highlighting, navigation, code completion, typing assistance, refactorings, context actions, inspections, formatting, and more.
T4-specific features include inspections, typing assistance, folding, brace matching, etc.
Extensive support is offered for includes to make the resolve in C# code as correct as possible.
You can execute T4 templates.
You can also debug T4 templates.
All these features work across Windows, macOS, and Linux.

だそうで、なかなかイケてるじゃないですか。Visual Studioも、Riderもそうですが、テンプレートエンジンでデバッガ動かしてステップ実行できたりするのが地味に便利です。VS Codeだと設定に一手間が必要なので(Qiitaにて.NET Core+VS CodeでもT4 テンプレートエンジンでコード生成したい!といった紹介もありますが)、VS2019, VS for Mac, Riderを使ったほうが楽そうです。

T4には2種類の生成パターン、デザイン時コード生成(TextTemplatingFileGenerator)と、実行時テキスト生成(TextTemplatingFilePreprocessor)の2種類がありますが、両方紹介します。

また、詳細なドキュメントがコード生成と T4 テキスト テンプレートにあり、実際かなり複雑な機能も搭載してはいますが、テキストテンプレートで複雑なことはやるべきではない(テキストテンプレートなんてあまり使わないような機能で使い倒した複雑なことやられても解読に困るだけ)ので、シンプルに使いましょう。シンプルに使う分には、全く難しくないです。

デザイン時コード生成

デザイン時コード生成とは、テンプレート単体でテキストを出力するタイプです。出力されたコードが、そのまま自身のプロジェクトのコンパイル対象になるイメージ。使い道としては、手書きだと面倒くさい単純な繰り返しのあるソースコードを一気に量産するパターンがあります。例えばジェネレクスのT1~T16までの似たようなコードを作るのに、一個雛形をテンプレートとして用意して for(1..16) で生成すれば一発、というわけです。他に、プリミティブの型(intとかdoubleとか)に対してのコード生成などもよくやりますね。またマークダウンで書かれた表(別の人がドキュメントとして書いているもの)から、enumを生成する、なんてこともやったりしますね。違うようで違わないようで実際微妙に違う退屈なユニットテストコードの生成、なんかにも使えます。

例としてMessagePackのカスタムシリアライザとして、以下のようなものを作りたいとします(単純な繰り返しの例としてちょうどよかったというだけなので、MessagePackの細かいAPIの部分は見なくてもいいです)。

using System;
using System.Buffers;
 
namespace MessagePack.Formatters
{
    public sealed class ForceInt16BlockFormatter : IMessagePackFormatter<Int16>
    {
        public static readonly ForceInt16BlockFormatter Instance = new ForceInt16BlockFormatter();
 
        private ForceInt16BlockFormatter()
        {
        }
 
        public void Serialize(ref MessagePackWriter writer, Int16 value, MessagePackSerializerOptions options)
        {
            writer.WriteInt16(value);
        }
 
        public Int16 Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
        {
            return reader.ReadInt16();
        }
    }
 
    // 以下、Int16の部分がInt32だったりDoubleだったりするのを用意したい。
}

Int16になっている部分を、Int32やDoubleなど全てのプリミティブにあてはめて作りたい、と。手書きでも気合でなんとかなりますが、そもそも面倒くさいうえに、修正の時は更に面倒くさい。なので、これはT4を使うのが最適な案件といえます。

例はVisual Studio 2019(for Windows)で説明していきますが、T4の中身自体は他のツールを使っても一緒なのと、最後にRiderでの使用方法を解説するので、安心してください。

まずは新しい項目の追加で、「テキスト テンプレート」を選びます。

すると、以下のような空のテンプレートが生成されたんじゃないかと思われます。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>

csprojには以下のような追加のされ方をしています。

<ItemGroup>
    <None Update="ForceSizePrimitiveFormatter.tt">
        <Generator>TextTemplatingFileGenerator</Generator>
        <LastGenOutput>ForceSizePrimitiveFormatter.txt</LastGenOutput>
    </None>
    <None Update="ForceSizePrimitiveFormatter.txt">
        <DesignTime>True</DesignTime>
        <AutoGen>True</AutoGen>
        <DependentUpon>ForceSizePrimitiveFormatter.tt</DependentUpon>
    </None>
</ItemGroup>

2個のItemが追加されているのは、T4本体と、生成物の2つ分です。<Generator>TextTemplatingFileGenerator</Generator>というのがT4をこれで処理しますよ、という話で、DependentUponはソリューションエクスプローラーでの見た目上、ネストする親を指定、ということになっています。Visual Studioの不正終了なので、たまに***1.csなどといった末尾にインクリメントされたファイルしか生成されなくなってムカつく!という状況にたまによく陥るのですが、その場合はLastGenOutputあたりを手書きで修正してけば直ります。

さて、まず思うのはシンタックスハイライトが効いていない!ということなので、しょうがないのでVS拡張を入れます。私がよく使うのはtangible T4 Editor 2.5.0 plus UML modeling toolsというやつで、インストール時にmodeling Toolsとかいういらないやつはチェック外してT4 Editorだけ入れておきましょう。なお、Visual Studioは閉じておかないとインストールできません。他のT4用拡張も幾つかあるのですが、コードフォーマッタがキモいとかキモいとか色々な問題があるので、私はこれがお気に入りです(そもそもコードフォーマットがついてない!変な整形されるぐらいなら、ないほうが百億倍マシです)。

さて、<#@ ... #>が基本的な設定部分で、必要な何かがあればここに足していくことになります。assembly nameは参照アセンブリ、基本的なアセンブリも最小限しか参照されていないので、必要に応じて足しておきましょう。ちなみにRiderだと何も参照されていない空テンプレートが生成されるので、デフォだとLINQすら使えなくてハァァァ?となるので要注意。System.Coreぐらい入れておけよ……。

import namespaceはまんま、名前空間のusingです。output extensionは、フツーは.csを吐きたいと思うので.csにしておきましょう。場合によってはcsvとかjsonを吐きたい場合もあるかもしれないですが。

次に、<# … #> を使って、テンプレートに使う変数群を用意します。この中ではC#コードが書けて、別に先頭じゃなくてもいいんですが、あまりテンプレート中にC#コードが散らかっても読みづらいので、用意できるものはここで全部用意しておきましょう。

<#
    var types = new[]
    {
        typeof(Int16),
        typeof(Int32),
        typeof(Int64),
        typeof(UInt16),
        typeof(UInt32),
        typeof(UInt64),
        typeof(byte), 
        typeof(sbyte),
    };
 
    Func<Type, string> GetSuffix = t =>
    {
        return t.Name == nameof(Byte) ? "UInt8" : (t.Name == nameof(SByte)) ? "Int8" : t.Name;
    };
#>

今回は生成したいプリミティブ型を並べた配列を用意しておきます。また、ここではローカル関数を定義して、くり返し使う処理をまとめることもできるんですが(テンプレート中に式を書くと見にくくなるので、引数を受け取ってstringを返す関数を用意しておくと見やすくなります)、tangible T4 Editorがショボくてローカル関数に対してシンタックスエラー扱いしてくるので(動作はする)、Funcを使うことでお茶を濁します。

ここから先は本体ですが、短いので↑で見せた部分も含めてフルコードが以下になります。Visual Studioの場合は保存時、Riderの場合は右クリックから手動で実行した場合に、ちゃんとファイルが生成されていることが確認できるはずです!

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    var types = new[]
    {
        typeof(Int16),
        typeof(Int32),
        typeof(Int64),
        typeof(UInt16),
        typeof(UInt32),
        typeof(UInt64),
        typeof(byte), 
        typeof(sbyte),
    };
 
    Func<Type, string> GetSuffix = t =>
    {
        return t.Name == nameof(Byte) ? "UInt8" : (t.Name == nameof(SByte)) ? "Int8" : t.Name;
    };
#>
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY T4. DO NOT CHANGE IT. CHANGE THE .tt FILE INSTEAD.
// </auto-generated>
 
using System;
 
namespace MessagePack.Formatters
{
<# foreach(var t in types) { #>
    public sealed class Force<#= t.Name #>BlockFormatter : IMessagePackFormatter<<#= t.Name #>>
    {
        public static readonly Force<#= t.Name #>BlockFormatter Instance = new Force<#= t.Name #>BlockFormatter();
 
        private Force<#= t.Name #>BlockFormatter()
        {
        }
 
        public void Serialize(ref MessagePackWriter writer, <#= t.Name #> value, MessagePackSerializerOptions options)
        {
            writer.Write<#= GetSuffix(t) #>(value);
        }
 
        public <#= t.Name #> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
        {
            return reader.Read<#= t.Name #>();
        }
    }
 
<# } #>
}

記法の基本的なルールは<# … #>で括っている部分以外は平文で出力されます。そして、<#= … #>が式で、そこが文字列として展開されます。

なお、テンプレートを書くにあたって、長年の経験から得たおすすめのお作法があります。

  • 計算式はなるべくテンプレート中で書くのを避ける、ために事前に関数として定義しておく
  • <auto-generated>を冒頭に書く、これはlintによる解析対象から外れる効果があるのと、このファイルがどこから来ているのかを伝えるために有用
  • foreachやifなどはなるべく一文で書く、複数行に渡っているとテンプレート中のノイズになって見難くなる。また、あまり
  • <#の開始行はインデントつけずに先頭で、というのもインデントつけてると平文のテンプレート生成対象になるため生成コードのインデントがズレやすい

この辺を守ると、割と綺麗に書けると思います。テンプレートはどうしてもコード埋め込みがあって汚くなりやすいので、なるべく綺麗にしておくのは大事です。それでもどうしても避けられないifだらけで、読みにくくなったりはしてしまいますが、そこはしょうがない。

実行時テキスト生成

実行時テキスト生成は、いわゆるふつーに想像するテンプレートエンジンの動作をするもので、プログラム実行時に、変数を渡したらテンプレートにあてはめてstringを返してくれるクラスを生成します。データベースのテーブル定義からマッピング用のC#コードを生成するツール、であったり、私がよくやってるのはRoslyn(C# Compiler)でC#コードを解析して、それをもとにして更にC#コードを生成するツールであったり、というかむしろC#コードを解析してKotlinコードを生成したりなど、やれることはいっぱいあります。リフレクションでアセンブリを舐めて、条件に一致した型、メソッドから何かを作る、みたいなのも全然よくありますね。

これの作り方は、追加→新しい項目から「ランタイム テキスト テンプレート」を選ぶと、悲しいことに別に普通の「テキスト テンプレート」とほぼ同じものが生成されてます(VS2019/.NET Coreプロジェクトの場合)。なぜかというと、csprojを開くとGeneratorがTextTemplatingFileGenerator、つまり普通のテキストテンプレートと同じ指定になっているからです。これは、多分バグですね、でもなんか昔から全然直されてないのでそういうもんだと思って諦めましょう。

しょーがないので手書きで直します。GeneratorのTextTemplatingFileGeneratorをTextTemplatingFilePreprocessorに変えてください。そうすると以下のようなファイルが生成されています(Visual Studioの場合は保存時、Riderの場合は右クリックから手動で実行した場合)

    #line 1 "C:\Users\neuecc\Source\Repos\ConsoleApp14\ConsoleApp14\MyCodeGenerator.tt"
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "16.0.0.0")]
    public partial class MyCodeGenerator : MyCodeGeneratorBase
    {
        /// <summary>
        /// Create the template output
        /// </summary>
        public virtual string TransformText()
        {
            return this.GenerationEnvironment.ToString();
        }
    }

ファイル名で作られたパーシャルクラスと、TransformTextメソッドがあるのが分かると思います。これで何となく使い方は想像つくと思いますが、new MyCodeGenerator().TransformText()でテンプレート結果が実行時に得られる、という寸法です。

さて、しかしきっとコンパイルエラーが出ているはずです!これはNuGetで「System.CodeDom」を参照に追加することで解決されます。別にCodeDomなんて使ってなくて、CompilerErrorとCompilerErrorCollectionというExceptionを処理する生成コードが吐かれてるから、というだけなので、自分でその辺のクラスを定義しちゃってもいいんですが、まぁ面倒くさいんでCodeDom参照するのが楽ちんです。この辺ねー、昔の名残って感じでめっちゃイケてないんですがしょーがない。

それと行番号がフルパスで書かれててめっちゃ嫌、というかフルパスが書かれたのをバージョン管理に突っ込めねーよ、って感じなので、これも消しましょう。linePragmas=”false”をテンプレート冒頭に書いておけば消えます。

<#@ template language="C#" linePragmas="false" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

この対応はほとんど必須ですね、ていうかデフォでfalseにしといてくれよ……。

さて、テンプレートにパラメーターを渡す方法ですが、これは生成されたクラス名と同じpartial classを定義すればOK。

namespace ConsoleApp14
{
    // 名前空間/クラス名(ファイル名)を合わせる(親クラスの継承部分は書かなくてOK)
    public partial class MyCodeGenerator
    {
        public GenerationContext Context { get; }
 
        // とりあえずコンストラクタで受け取る
        public MyCodeGenerator(GenerationContext context)
        {
            this.Context = context;
        }
    }
 
    // 生成に使うパラメーターはクラス一個にまとめておいたほうが取り回しは良い
    public class GenerationContext
    {
        public string NamespaceName { get; set; }
        public string TypeSuffix { get; set; }
        public int RepeatCount { get; set; }
    }
}

これでテンプレート中でContextが変数として使えりょうになります。テンプレートの例として、面白みゼロのサンプルコードを出すと……(Roslynとか使うともっと意味のあるコード例になるのですが、本題と違うところがかさばってしまうので、すみません……)

<#@ template language="C#" linePragmas="false" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
// <auto-generated>
// THIS (.cs) FILE IS GENERATED BY ConsoleApp14.exe. DO NOT CHANGE IT.
// </auto-generated>
namespace <#= Context.NamespaceName #>
{
<# foreach(var i in Enumerable.Range(0, Context.RepeatCount)) { #>
    public class Foo<#= Context.TypeSuffix #><#= i #>
    {
    }
 
<# } #>
}

RepeatCountの回数で中身空のクラスを作るというしょっぱい例でした。これの注意事項は、普通のテキストテンプレートと特に変わりはないのですが、auto-generatedのところに、生成ツール名を入れておいたほうがいいです。外部ツールでコード生成するという形になるため、ファイルだけ見てもなにで生成されたかわからないんですね。あとから、何かで作られたであろう何で作られたかわからない自動生成ファイルを解析する羽目になると、ツールの特定から始めなくちゃいけなくてイライラするので、この辺をヘッダにちゃんと書いといてあげましょう。ただたんに「これは自動生成されたコードです」と書いてあるだけよりも親切で良い。

記述におけるコツは、やはりテンプレート中に式を書いて文字列を生成するよりかは、この場合だとContext側にメソッドを作って、引数をもらってstringを返すようにすると見通しが良いものが作りやすいでしょう。ちなみにthis.ContextのContextのIntelliSenseは効きません、そっちの解析までしてくれないので。テンプレートファイルにおける入力補完は甘え、おまけみたいなもんなので、基本的には頼らず書きましょう。もちろん、そのせいでばんばんTypoしてエラーもらいます。これが動的型付け言語の世界だ!をC#をやりながら体感できるので、いやあ、やっぱ静的型付け言語はいいですねえ、という気持ちに浸れます。

さて、こうして出来たクラスは、ただのパラメーターを受け取ってstringを返すだけのクラスなので、何に使っても良いのですが、9割はシンプルなコマンドラインツールになるのではないでしょうか。

C#でコマンドラインツールといったら!私の作ってるMicroBatchFrameworkが当然ながらオススメなので、それを使ってツールに仕立ててみましょう。

using MicroBatchFramework;
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
 
namespace ConsoleApp14
{
    class Program : BatchBase
    {
        static async Task Main(string[] args)
        {
            await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<Program>(args);
        }
 
        public void Run(
            [Option("o", "output file path")]string outputPath,
            [Option("n", "namespace name")]string namespaceName,
            [Option("t", "type name suffix")]string typeSuffix = "Foo",
            [Option("c", "type generate count")]int repeatCount = 10
            )
        {
            // パラメータを作って
            var context = new GenerationContext
            {
                NamespaceName = namespaceName,
                TypeSuffix = typeSuffix,
                RepeatCount = repeatCount
            };
 
            // テキストを生成して
            var text = new MyCodeGenerator(context).TransformText();
 
            // UTF8(BOMなし)で出力
            File.WriteAllText(outputPath, text, new UTF8Encoding(false));
 
            Console.WriteLine("Success generate:" + outputPath);
        }
    }
}

これで ConsoleApp14.exe -o "foo.cs" -n "HogeHoge" -t "Bar" -c 99 というしょっぱいコマンドでfoo.csが吐かれるツールが完成しました!

RiderにおけるT4

RiderでのT4は、Visual Studioが使っている生成ツールとは違う、T4の記法として互換性のあるJet Brains独自のツールを実行している気配があります(そのため、場合によっては互換性がないところもあるかもしれません、というか実際linePragms=falseで行番号が消えなかった……)

実行自体は簡単で、ttに対して右クリックしてRunを選べばデザイン時コード生成、Preprocessを選べば実行時テキスト生成の出力結果が得られます。

image

現時点ではEAP、つまりプレビュー版ですが、まぁ他にもいっぱい良い機能が追加されてるっぽいので、Riderユーザーは積極的にEAPを使っていけばいいんじゃないかしら。

T4にできないこと

T4は、基本的に.ttで書かれたファイルを事前に(VSだと保存時、Riderだと任意に、あるいは設定次第でビルド時に)外部ツールが叩いて、テキスト(.csだったり色々)を生成します。いいところはC#プロジェクトと一体化して埋め込まれるので、パフォーマンスもいいし、そもそもT4生成時に型が合わなければエラーが出てくれる(動的の塊であるテンプレートファイルにとって、書き間違えは日常茶飯事なので、これは結構嬉しい)。実行効率も良いです、ファイル読んでパースして必要があればキャッシュして云々というのがないので。

が、しかし、プログラムが実行時に動的にテンプレートを読み込んでなにかする、みたいなことはできません。ツールのビルド時にテンプレートが出来上がってないといけないので、プラグイン的に足すのは無理です。それは成約になる場合もある、でしょうし、私的には型もわからないようなのを動的に合わせてテンプレート作るとか苦痛なので(←最近なんかそういうのやる機会が多くて、その度にシンドイ思いをしてる)、パラメータ渡しだけでなんとかして済むようにして諦めて作り込んだほうが百億倍マシ、ぐらいには思っていますが、まぁそういうのがしたいというシチュエーション自体は否定できません。その場合は、他のテンプレートエンジンライブラリを選んで組み込めば良いでしょう、T4がやや特殊なだけで、他のテンプレートエンジンライブラリは、むしろそういう挙動だけをサポートしているので。

まとめ

T4は十分使い物になります。微妙にメンテされてるのかされてないのか不安なところもありますが、そもそもMicrosoftもバリバリ使っているので(GitHubに公開されているcorefxのコードとかはT4で生成されているものもかなりあります、最近追加されたようなコードでも)、全く問題ないでしょう。実際、記法も必要十分揃っているし、特に極端に見にくいということもないと思います、ていうかテンプレートエンジンとしてフツーなシンタックスですしね。

仮に複雑なことやりたければ、例えばテンプレートをパーツ化して使い回すために分割して、都度インポートとか、というのもできます(T4 インクルード ディレクティブでは.t4という拡張子が紹介されていますが(拡張子はなんでもいい)、一般的には.ttincludeという拡張子が使われています)。他いろいろな機能がありますが、あらためて、テキストテンプレートごときで複雑なことやられると、追いかける気が失せるので、なるべく単純に保ちましょう。やるとしてもせいぜいincludeまで。それ以上はやらない。

というわけで、どうでしょう。MicroBatchFrameworkとも合わせて、C#のこの辺の環境は割といいほうだと思ってます。コード生成覚えるとやれることも広がるので、ぜひぜひ、コード生成生活を楽しんでください!

また、Unityでも普通に便利に使えると思います。Editor拡張でソースコード生成するものを用意するパターンは多いと思いますが(なにかリソースを読み込んで生成したり、あるいはそのままAssembly.GetExecutingAssembly().GetTypes()して型から生成したり)、その時のテンプレートとして、普通にstringの連結で作ってるケースも少なくなさそうですが、「デザイン時コード生成」も「実行時テキスト生成」も、どっちも全然いけます。「T4にできないこと」セクションに書いたとおり、よくも悪くも外部ツールで実行環境への依存がないので。

Unite Tokyo 2019でC# Structの進化の話をしてきました

Unite Tokyo 2019にて、「Understanding C# Struct All Things」と題して登壇してきました!動画は後日Unity Learning Materialsに公開される予定です。

【Unite Tokyo 2019】Understanding C# Struct All Things from UnityTechnologiesJapan002

C#成分強めなので、Unityに馴染みのない人でも読んで楽しめる内容になっていると思います。とはいえ勿論、Uniteでのセッションであるということに納得してもらえるようUnity成分もきちんと取り入れています。というわけで、どちらの属性の人にも楽しんでいただければ!

structに関する機能強化、実際めっちゃ多いんですが、それをカタログ的に延々と紹介してもつまらないので、そうしたカタログ紹介っぽさが出ないように気を配ってます。あと、あんましそういうのでは脳みそに入ってこないというのもあるので。

応用例的なところのものは、ないとつまらないよねーということで色々持ってきたのですが、もう少し説明厚くしても良かったかなー感はありました。セッション内でも雰囲気で流した感じありますしね。とはいえ尺とか尺とか。まぁ雰囲気を分かってもらえれば(structは色々遊べるよ、という)いい、と割り切った面もあるにはあります。詳しくは資料を熟読してください!

Span/NativeArrayの説明も厚くしたくはあったんですが、structそのものの本題からは若干外れるので見送り。あと尺とか尺とか。

今年のUnite、もの凄くいいイベントでした。神運営とはこのことか……。そしてDOTSが熱い。めっちゃやるやる詐欺なので、いい加減本当にそろそろDOTSに手を出して楽しみたいですます!

あと、MessagePack-CSharp v2はいい加減そろそろ出るはず予定です、ちなみにSystem.Memoryとかにめっちゃ依存しているので、MessagePack-CSharpを入れるとSpanとかSystem.Runtime.CompilerServices.Unsafeとかが解禁されます(依存ライブラリとして同梱する予定なので)。いいのかわるいのか。まあ、いいでしょふ。未来未来。

CEDEC 2019にてMagicOnion、或いは他言語とC#の協調について話しました

セッション名はUnity C# × gRPC × サーバーサイドKotlinによる次世代のサーバー/クライアント通信 〜ハイパフォーマンスな通信基盤の開発とMagicOnionによるリアルタイム通信の実現〜(長い!)ということで二部構成になっていて、私は後半部分を担当しました。

Unity C#と.NET Core(MagicOnion) C# そしてKotlinによるハーモニー from Yoshifumi Kawai

Cysharpは他社さんのお仕事も(ボリューム次第で、今はちょっとしたコンサルティングぐらいしか空きがないんですが)受けたりも可能です、ということでアプリボットさんのお手伝いをちょいちょいしています。リアルなMagicOnionの採用の話として、どんな風にやってるんですかねーというところの一環をエモ成分強めで語ってみました。リリースどころかタイトルもまだ未発表なので技術的な部分が弱めなので、次はリアルな実例として色々詰めたいところですね!

前半部、というかがっちゃんこされている資料はこちらで公開されています。前半でgRPCいいぞ!という話をしているのに、こちらは冒頭でprotoは嫌だお!という展開で繋げるアレゲさでしたが、まあジョークの一環です。多分。はい。protoのいいところは中間形式であり言語agnosticなところで、protoのよくないところは中間形式であること、ですね。これが何を言っているかを理解できれば100点満点です!是非の議論は、このことを理解してから進めましょう。

MagicOnionは、ちょうど今日Ver 2.4.0をリリースしまして、やる気満々です。次の展開もいろいろ考えているので、というか積みタスクがんがんがんが。まぁ、順次やってきます。

さて、9月はもう一つ、「Understanding C# Struct All Things」と第してUnite Tokyo 2019でセッションします。 Day 2のRoom A、13:30からでライブ配信もあるので、そちらも見ていただければ!

TaskとValueTaskの使い分け、或いはValueTaskSupplementによる福音

ValueTaskSupplementというライブラリを新しく作って公開しました!

これは、ValueTaskにWhenAny, WhenAll, Lazyを追加するという代物で、それだけだとヘーそーなんだー、としか思えないと思われます。しかし、ValueTaskを使っていくと、めっちゃくちゃ欲しくなる機能になってます。ないと死ぬレベルで。

と、いうわけで、なんでこれが必要なのか、っていうところから説明します。

TaskとValueTask

C# 5.0にasync/awaitが導入された当初はTaskしか存在しませんでした。標準APIのあらゆるメソッドにasyncメソッドを生やすなど、Microsoftの多大な努力により、C#はいち早く非同期時代を迎え、async/awaitは多用(濫用とも言う)されるようになりました。しかし、多用された結果、当初思ってたよりもTaskのオーバーヘッド多くね?同期をラップするだけのシチュエーションも少なくなくね?ということに気付き、C# 7.0から登場したのがValueTaskです。

登場当時のValueTaskは T | Task[T] という、もし中身が同期の場合はTを、非同期の場合はTaskをラップしたものとして存在しました。なので、TaskとValueTaskの使い分けは、中身が非同期の場合が確定している場合はラップが不要で、かつ(当時)スタンダードな定義に沿うTaskを基本に考えていくのが良いでしょう。と、されていました。

が、しかし、実際にアプリケーションを作っていくと、都度使い分けなんて考えられるものじゃないし、ValueTaskのオーバーヘッドといってもstructでラップするだけの話でそこまで大きいわけじゃない(同期のものをTaskで定義したほうがよほど大きい)ので、普通にアプリケーションで定義する場合のルールは全部ValueTaskでいーんじゃね?と思っていたりは、私の個人的な見解どまりでありました。

そして、更にパフォーマンスを追求する中で、ValueTask->Task変換のオーバーヘッドをなくし、中身をそれぞれに特化したコードを挟み込めるように IValueTaskSource というものが導入されました。これによりValueTaskは T | Task[T] | IValueTaskSource のどれかの状態を持つという共用体となり、個別に実装されたシナリオでは中身がTask[T]の場合よりもIValueTaskSourceの場合のほうがパフォーマンスが高いということで、名実ともにValueTaskの天下の時代が始まりました。

大々的にパブリックAPIにも露出してくるのは.NET Core 3以降だと思われますが、今でも問題なく使える状態&KestrelやSystem.IO.PipelinesにはValueTaskによるAPIが既に露出しています。MagicOnionのフィルターもValueTaskだったりしたりしましたり。

なお、別の世界線ではUniTaskというものも存在しますが、これは ValueTask + IValueTaskSource に近い代物です。つまり別にTaskなんていらなかったんや……。

ValueTaskの欠点

そんなValueTask最大の欠点は、ユーティリティの欠如。つまり、WhenAllやWhenAnyができない。それらが必要な際はAsTaskでTaskに変換する必要がありました。が、Taskに変換する時点でオーバーヘッドじゃーん。しかもいちいちAsTaskするのはクソ面倒くさい!せっかく IValueTaskSource があるなら、IValueTaskSourceを使ってネイティブなValueTask用のWhenAllやWhenAnyを作ればハイパフォーマンスじゃん!というわけで、それらを提供するのがValueTaskSupplementです。

using ValueTaskSupplement; // namespace
 
async ValueTask Demo()
{
    // `ValueTaskEx`が使う唯一の型です
 
    // こんな風な別々の型のValueTaskがあったとしても
    ValueTask<int> task1 = LoadAsyncA();
    ValueTask<string> task2 = LoadAsyncB();
    ValueTask<bool> task3 = LoadAsyncC();
 
    // awaitできて、タプル記法でサクッと分解できて便利!
    var (a, b, c) = await ValueTaskEx.WhenAll(task1, task2, task3);
 
    // WhenAnyでは int winIndexでどれが最初に値を返したか判定できます
    var (winIndex, a, b, c) = await ValueTaskEx.WhenAny(task1, task2, task2);
 
    // Timeoutみたいなものの実装はこんな風に
    var (hasLeftResult, value) = await ValueTaskEx.WhenAny(task1, Task.Delay(TimeSpan.FromSeconds(1)));
    if (!hasLeftResult) throw new TimeoutException();
 
    // Lazyも用意されています!
    // awaitを呼ぶまで遅延&値がキャッシュされるAsyncLazyのような代物ですが
    // 型がValueTask<T>そのものなので、フィールドに保持したまま、WhenAllなどがそのまま書けて便利
    ValueTask<int> asyncLazy = ValueTaskEx.Lazy(async () => 9999);
}

と、いったように、ただのTask.Xxxよりも更に便利になった機能が追加されていて、もう全部ValueTaskで統一でいいっしょ、って気になれます(特に var (a, b, c) = await ….)が便利ですよ!

まとめ

時代はValueTask。Taskのことは忘れて全部ValueTaskで良いのですー、良いのですー。そして、ValueTaskで統一したら、すぐに標準のまんまじゃしんどいのですー、ってことに気づくでしょふ。そこでValueTaskSupplementですよ、っという流れです。絶対そうなります。というわけで諦めて(?)使いましょう。

ところで、最近よくCygames Engineers’ Blogに寄稿しているのですが、なんとなくの私の中の使い分けは、Unityに関する成分が含まれる(新規)ライブラリはCygamesのブログのほうに、そうじゃないものはここに、みたいな気持ちではいます。まぁ、どっちも見ていただければればですです。

また、直近イベントでは9月4日にCEDEC 2019で「Unity C# × gRPC × サーバーサイドKotlinによる次世代のサーバー/クライアント通信 〜ハイパフォーマンスな通信基盤の開発とMagicOnionによるリアルタイム通信の実現〜」、9月26日にUnite Tokyo 2019で「Understanding C# Struct All Things」というセッションを行うので、是非是非見に来てください!

C#のOpenTelemetry事情とCollectorをMagicOnionに実装した話

を、してきました。昨日。OpenCensus/OpenTelemetry meetup vol.2にて。

Implements OpenTelemetry Collector in DotNet from Yoshifumi Kawai

もともとトレースとかメトリクスの標準化として、OpenCensus(Google)陣営とOpenTracing(CNCF)陣営がいて、正直どっちも流行ってる気配を感じなかったのですが、合流してOpenTelemetryが爆誕!これは本当に今年の頭に発表されたばかりで、仕様策定が完了するのが今年9月(予定)といった感じで、まだ練ってる最中というところです。ただ全体的にはOpenCensusベースでSDKが組まれているので、OpenCensusの時点で完成度がある程度高かったSDKは、割とそのままでも使えそうな雰囲気はあります。

個人的な意義とかはスライドにも書きましたが、まあ、流行って欲しいですね。概念はめっちゃ良いと思うので、きっちり浸透して使われるようになって欲しい。ぎっはぶスターだけが尺度ではないとはいえ、リファレンス実装のJava版の125が最大スター数とか、ワールドワイドで注目度弱すぎないか!?みたいな気は、あります。大丈夫かな。大丈夫ですよね……。一応、参画企業は名だたるところも多いし仕様分裂してるわけでもないので、乗っかる価値はあるんじゃないかと、思います。

.NET SDKの事情

opentelemetry-dotnetで、メインに開発してるのはMicrosoftの人間が一人やってますね。GitHubの草で見ても毎日張り付いてopentelemetry系のなにかでなにかやってるので、仕事として専任で頑張ってるんじゃないでしょうか、多分。クオリティ的には高くもなく低くもなくというところで、過度に期待しなきゃあそんなもんでしょーということで受け入れられそうです。もともとJavaがリファレンス実装になってて、他の言語は、まず基本的なAPIはそれを踏襲すること、というところもあるので、あまりブーブー言ってもしょうがないかもしれません。

Collectorの実装がちょっと面白くて、AspNetCoreのCollectorHttpClientのCollectorは、 DiagnosticSourceという比較的新しい仕組みから情報を取るようになっています。これによって、プロファイラAPIによるAuto Instrumentによるパフォーマンス低下などもなく、しかし特にユーザーはなにもせずに、メトリクスが取得できるようになっています。

ADO.NETのCollectorがないのでDB系が取れないんですが、多分まだADO.NETがDiagnosticSourceに対応していないので、それが対応するまではやらないみたいなつもりなんだと思います。さすがにADO.NETのCollectorがないと話にならないでしょー。

まとめ

MagicOnionの事情としては実装のPRは用意してますが、まだMergeしてません。ただまぁ、スライドに書いたのですが、結構これらを入れるだけで、撮って出しでもいい感じのダッシュボードが作れるんじゃないかと思います。このへんは前職でリリースしたゲームのダッシュボードを作り込んだ経験が生きてるかな、ってのはありますね。いやほんと。

ともあれというわけで、私はOpenTelemetryにベットするんで、みんなも是非やっていきましょう!ね!流行らせないと未来はない!

MagicOnion勉強会を開催しました

【Unity / .NET Core】 MagicOnion勉強会。正確には開催してもらいました、ですが!バーチャルキャストさん、ありがとうございました!

こちらは私のスライド、 The Usage and Patterns of MagicOnion になります。

The Usage and Patterns of MagicOnion from Yoshifumi Kawai

実際何に使えるの、というところについて(妄想の)繋ぎ方を紹介したりしました。まぁ、ようするになんでも使えます、ということですね。

MagicOnionはGitHub Starも1000超えたので、野良ぽっと出謎ライブラリからは、少し脱却したんじゃないかと思います。まだメジャー級とは言い難いですが。アングラ級?

私はP2P推すのどうかなーって思ってるわけなんですが、その理由はポジショントーク、じゃなくて自分がサーバー書けるから、ってのは勿論あるんですが、本当にP2Pでいいんすかねー、というのがあり。実際真面目に。リレーサーバー用意するんだったらもう自前でやる領域を秒速で超えちゃうし、P2P→Dedicated Serverだと、機能制限されたサーバーモデル(サーバーがリレーとしてしか機能できなくてロジック積んだりモロモロができない)になっちゃうので微妙に感じたり、結局自前でやるならP2Pでもマッチングどうすんねんであったり、まぁもろもろ色々と。信頼できるクライアント -> サーバーのRPCが一つあるだけで、色々すっきり解決できるんじゃないのかなー、ってのはずっと思っているところで。

MagicOnionに問題がないとは言わないんですが、特にネイティブDLLは問題の塊なのでPure C#実装に変えたいねえ、そうすればプラットフォームの制限もなくなるしねえ、とかもあったりはあったりはあったりはしますが、まぁそのうちなんとかします:) コード生成に関しては肯定的なんですが(リフレクション拒否した非コードジェネレーションのモデルは、やれることにかなり制約入りますですのです)、現状のヘボジェネレーターはよろしくないのでそれも早急に直しまうす。インフラ系はドキュメントとかの拡充でカバーですかね、知識がいるのは事実なので。

発表一覧

勉強会レポ : 【Unity / .NET Core】 MagicOnion勉強会さんのところにまとまっているのですが、こちらでも改めてリンク集で。

これだけトークが集まって、大感謝です。

第二回の開催、は(あるとしても)当面先だとは思いますが、実際MagicOnionを使用した開発に入っているプロジェクトは割とないわけではない(?)という感じですので、ご安心を(?)。一応歴史的にはかなりの負荷を捌いている実績もあるので……!Cysharpとしても、「会社として」力を入れているところがあるので、その辺も安心材料に含めていただければと思っています。最悪、本当に困ったらお問い合わせ下されば色々解決のお手伝いもできるかもしれません。

また、CEDEC 2019ではUnity C# × gRPC × サーバーサイドKotlinによる次世代のサーバー/クライアント通信 〜ハイパフォーマンスな通信基盤の開発とMagicOnionによるリアルタイム通信の実現〜と第して、アプリボットさんと共同でセッションを行うので、そちらも是非是非。

LitJWTに見るモダンなC#のbyte[]とSpan操作法

LitJWT、という超高速な認証ライブラリを作りました。

なんと今回はUnity用が、ない!どころか.NET Standardですら、ない!.NET Core専用になってます(今のとこ)。理由はパフォーマンス都合で現状.NET CoreにしかないAPIを使いすぎたので修正が面倒ということなので、そのうちなんとかするかもしれませんかもしれません。

5倍高速

image

そもそも認証ライブラリでパフォーマンス追求しているものなんてない!ので!まぁそりゃそうだという感じではある。実際、そこまで認証で必要か?というと疑問符が付くところなので、ただのオーバーエンジニアリングなのですが、とはいえ速いというのは良いことです。シンプルに。

JWTに関しては特に説明することもないので(セッションにでも何にでも使えばいいんじゃないかしら、実際、私はMagicOnionのステートレスセッションのために必要なので用意しました)、ここから先は実装の話をします。

モダンBase64(Url)

JWTは大雑把にはJSONをBase64Urlでエンコードして署名を引っ付けたもの、です。Base64UrlというのはBase64の亜種で、URLセーフになるように、使う文字列が少し異なります。性質上、GETでURLにトークンが引っ付いたりするかもですしね。なるほど。

さて、しかしそんなマイナーなBase64Urlをエンコードするメソッドは用意されていないので、普通はこんな風に書いてます。

Convert.ToBase64String(input)
    .TrimEnd('=')      // 新しいstringを作る
    .Replace('+', '-') // 新しいstringを作る
    .Replace('/', '_') // 新しいstringを作る

改めてBase64Urlは、ようするにパディング(4の倍数に収まらない場合に末尾につく)の=が不要で、+が-、/が_なBase64なので、置換!ただたんに置換!する、すなわち新規文字列を無駄に作成!無駄に検索して無駄に作成!なわけです。

実際、別にこのBase64の変換表の一部を差し替えるだけの話なのに。

無駄すぎて発狂しちゃうので、ここは普通に自前でBase64を実装することで大解決しましょう。実際それしか方法はない、しょうがない。

せっかく作るので、今風のAPIにしましょう。例えばデコードのAPIはこんな感じに。

public static bool TryFromBase64UrlString(string s, Span<byte> bytes, out int bytesWritten)
public static bool TryFromBase64UrlChars(ReadOnlySpan<char> chars, Span<byte> bytes, out int bytesWritten)
public static bool TryFromBase64UrlUtf8(ReadOnlySpan<byte> utf8, Span<byte> bytes, out int bytesWritten)

stringだけ受け入れるのではなくて、ReadOnlySpan<char>と、UTF8を直接受け入れられるようにReadOnlySpan<byte>のオーバーロードを用意しましょう(面倒くせえ……)。中身の実装はcharとbyteで似てるようで若干違うので今回は雑にコピペコードで済ませてます。コピペ最強。

ともあれこれでゼロアロケーションなデコードです。

ちなみにSystem.Security.Cryptographyも、こうしたSpan対応のAPIが(.NET Core 2.1なら)あります。.NET Standard 2.0にはありません。2.1から、なのでまだ先です。

bool TryComputeHash(ReadOnlySpan<byte> source, Span<byte> destination, out int bytesWritten)
bool TrySignData(ReadOnlySpan<byte> data, Span<byte> destination, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding, out int bytesWritten)
bool VerifyData(ReadOnlySpan<byte> data, ReadOnlySpan<byte> signature, HashAlgorithmName hashAlgorithm, RSASignaturePadding padding)

今回の最初のリリースが.NETCore Appのみなのは、主にこの辺が理由です。迂回できないこともないんですけどねえ。

stackallocとArrayPoolをめっちゃ使う

先のBase64のデコード繋がりで説明すると、デコード先のbyte[]をどう用意するか、という話であり。headerのBase64とかsignatureのBase64とか、あまり大きくないのが確定しているので、stackallocをSpanで受けて、デコード先を作ります。

Span<byte> bytes = stackalloc byte[Base64.GetMaxBase64UrlDecodeLength(header.Length)];
if (!Base64.TryFromBase64UrlUtf8(header, bytes, out var bytesWritten))

Payloadは長さがわからない(そこそこ大きい可能性もある)ので、stackallocで受けるのは不安があるので、ArrayPoolを使いましょう。

var rentBytes = ArrayPool<byte>.Shared.Rent(Base64.GetMaxBase64UrlDecodeLength(payload.Length));
try
{
    Span<byte> bytes = rentBytes.AsSpan();
    if (!Base64.TryFromBase64UrlUtf8(payload, bytes, out var bytesWritten))
    {
        return DecodeResult.InvalidBase64UrlPayload;
    }
    bytes = bytes.Slice(0, bytesWritten);
 
    // ....
}
finally
{
    ArrayPool<byte>.Shared.Return(rentBytes);
}

ようするに、今どきnew byte[]なんてしたら殺されるぞ!

ReadOnlySpanの辞書を作る

ReadOnlySpan<byte>はref struct!つまりDictionaryのKeyにはできない!けどルックアップはしたい!

どーいうことかというと、例えば

image

HeaderのJSONを舐めて、デコードに使うアルゴリズムが何であるかあるかチェックしたいわけですが、まず、今どきはJSONをstringで検索したりはしません。UTF8のままやります(System.Text.Json(preview)やUtf8Jsonを使いましょう)。特に、今回はBase64Urlからデコードしてきたバイナリなので、更にstringにデコードしてしまうのは無駄の極みなので、絶対避けたいわけです。

そうして、algのvalue部分に相当するReadOnlySpanが切り出せたとしましょう。さて、じゃあこれが何であるか。HS256なのかRS512なのか、そして、それをキーにしてIJwtAlgorithmを取り出したいわけです。必要なデータ構造はDictionary<ReadOnlySpan<byte>, IJwtAlgorithm>>なわけです。が、それは無理。C#の言語仕様がそれを許してくれないのです。困ったねえ。

もちろん、答えは自作しましょう。今回はReadOnlyUtf8StringDictionaryというものを用意しました。Dictionary内部で持っておくキーは別にSpanである必要はないので、普通にbyte[]で確保しておきます。ルックアップだけ

public bool TryGetValue(ReadOnlySpan<byte> key, out TValue value)

というAPIを用意すればOKという寸法です。

実装において、byte[]の一致比較はSpanのSequenceEqualを使えば良いんですが、GetHashCodeの実装だけはどうにもなりません(Utf8Stringも控えてることだし、標準でいい感じのがそろそろ入るといいんですけどねえ)。私は延々と使いまわせいているFarmHashの実装をコピペで用意していますが、適当にxxHashを実装したり何かするといいと思います。適当に拾ってきたものを使うとパフォーマンス的に意味のないクソ実装の可能性もあるので、その辺は適当に気をつけましょう。

最後まで配列の切り出しをしない実装を作る

jwtEncoderのEncodeメソッドは、3つのオーバーロード(名前違い含む)を持ってます。

string Encode<T>(...)
byte[] EncodeAsUtf8Bytes<T>(...)
void Encode<T>(IBufferWriter<byte> writer, ...)

一番使うのは、stringだとは思います。Httpのヘッダーとかに埋めたりするケースが多いと思うので、stringが要求されるのでしょーがない。でも、byte[]を返すもののほうが高速です。内部的には全てUtf8 byte[]で処理しているので、stringへのエンコード処理をバイパスできるからです。例えばgRPCは(MagicOnionも)、バイナリヘッダーを許容しているので、stringヘッダーよりも高速に処理できます。

// gRPC Header
var metadata = new Metadata();
metadata.Add("auth-token-bin", encoder.EncodeAsUtf8Bytes());

さて、じゃあ最後の IBufferWriter<byte> はなにかというと、直接これに書き込みます。まぁ、Span<byte>,int bytesWrittenみたいなものですが、Span<byte>を渡すのが使えるのって、処理後の長さが概ね分かっているときで、JwtのエンコードはPayloadの処理とかあるので、基本的には処理が完了するまで分かりません。ので、bytesWritten形式のAPIは向いてません。

IBufferWriterはStreamみたいなもので、これに直接書き込みます。新しいI/O APIである System.IO.Pipelines で使われているAPIで、つまりは、一応それに対応しているということで。MessagePack-CSharpのv2(現在絶賛制作中)も、IBufferWriterが主役になっています。時代はダイレクトライト。

System.IdentityModel.Tokens.Jwtは最低

JWTの話は特にするつもりはなかったんですが、とにかくSystem.IdentityModel.Tokens.Jwtが最低だということは言っておきたい!とにかくAPIがヤバい!まぁ、これ、他の認証系も統合された抽象化の上に乗っているので、JWT的に不要で意味不明なものがいっぱいついているうえに、その抽象化がエンタープライズグレード(笑)の重厚長大な酷いもので、Microsoftの認証が難しいと感じるとしたら(実際難しい)、ただたんにライブラリのAPIが腐ってるから難しいだけですからね。

何かのフレームワークと統合されてて、ワンポチで導入される、とかだったらまだいいんですが、直接は触りたくないですねえ。誰が作ってんだかって感じですが(お、公開されてる先はAzure配下かな……)

まとめ

MagicOnionで――というのもありますが、認証系はJWT中心に、ちょっと色々考えてます。あとまぁ、さすがにパフォーマンスだけが差別化要因というのはしょっぱいので、Unity対応しよ。

MagicOnion Ver 2.1.0

MagicOnionのVer 2.1.0を出しました。前回が2月28日なので、3ヶ月ぶりで少し間が空いてしまった感じもありますが、色々良くなったので紹介していきまうまう。

StramingHubClientでメッセージが詰まるバグの修正

いきなり致命的な話なんですが、StreamingHubClientが1フレにつき1メッセージしか送信されないという、しょうもないバグが存在していました。このバグの原因が面白くて(?)、元はこんな感じのコードだったんですよ。

// readerはIAsyncEnumeratorというMoveNext, Currentでデータを取ってくる非同期イテレーター
while (await reader.MoveNext())
{
    var message = reader.Current; // byte[]
    OnBroadcastEvent(message);    // messageは実際にはヘッダ解析したり色々してます
}

gRPCはIAsyncEnumeratorというかっこつけたインターフェイスを採用しているので、awaitでサーバーからデータが届くのを非同期で待機できる。

で、このawaitが問題で、UnityだとUnitySynchronizationContext経由してawaitの先が実行されます。なので安全にメインスレッドでOnBroadcastEvent(これは最終的にユーザーが実装したインターフェイス定義のメソッドが呼ばれる)が呼ばれて嬉しい。のですが、reader自体は別スレッドで動いているので、awaitの度にメインスレッドへの同期を待っているのです。

正確にはUnitySynchronizationContextがawaitの度にメインスレッド上だろうがなんだろうが問答無用で次フレームに叩き込む仕様だから、なのですけれど。

何れにせよ、そんなわけで、サーバーから同一フレームで沢山のデータが送られてきたとしても、クライアント側は1フレームに1メッセージしか捌けないので、どんどん詰まっていくわけです。もちろん、バグです。仕様じゃなく。普通に。バグ。

var syncContext = SynchronizationContext.Current;
 
// ConfigureAwait(false)でSyncContextを外して、このループはずっと別スレッドで動かす
while (await reader.MoveNext().ConfigureAwait(false))
{
    var message = reader.Current;
    if (syncContext != null)
    {
        // 手動でPostする(待たない)
        syncContext.Post(() => OnBroadcastEvent(message));
    }
    else
    {
        OnBroadcastEvent(message);
    }
}

と、いうわけで、こんな具合に半手動でPostするコードに書き換えました(Postでラムダ式のキャプチャが発生する問題がありますがshoganai。正確にはobject stateが渡せるのですが、実際のデータでは複数の値が必要になるのでTupleを作る必要があって、余計なオブジェクトが必要という点で変わらない)。ConfigureAwait(false)をつけないことは意識して、意図してやったこと(同期コンテキストを維持してメインスレッド上でコールバックを飛ばす)だったんですが、そこまで意識しといてこういうバグにつなげちゃうのは完全に甘かった、ということで反省しきりです。

ともあれこれで詰まり問題は大解決です。

MagicOnion.Hosting

最初のサンプルがConsole.ReadLineで待っているコードなのでアレなのですが、普通に実開発ではMagicOnion.Hostingというプロジェクトを使って欲しいと思っています。

// using MagicOnion.Hosting
static async Task Main(string[] args)
{
    await MagicOnionHost.CreateDefaultBuilder()
        .UseMagicOnion(
            new MagicOnionOptions(isReturnExceptionStackTraceInErrorDetail: true),
            new ServerPort("localhost", 12345, ServerCredentials.Insecure))
        .RunConsoleAsync();
}

Hostingとは何かと言うと、Genric Hostという.NET Core時代の基盤フレームワークの上に乗っかっています。これは、こないだ作った MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワーク と同じ仕組みです。

.NET Generic Hostは、標準的な仕組みとしてロギング/コンフィグ読み込み/DIをサポートしています。これによりコンフィグのマッピング、ロギングなどを標準的な作法でフルサポートしています。

というわけで、何が嬉しいかと言うと、↑の件をフルサポートしてくれていることです。コンフィグとか何をどう読み込めばいいんですかー?という話は、Generic Hostの仕組みを使ってください、というのが答えになります。ドキュメントもMicrosoftのドキュメントサイトで沢山解説されていて、それがそっくりそのまま使えるので、良いことしかない!

また、これによってコンストラクタインジェクションでのDIも使えるようになりました。

static async Task Main(string[] args)
{
    await MagicOnionHost.CreateDefaultBuilder()
        .ConfigureServices((hostContext, services) =>
        {
            // DI, you can register types on this section.
 
            // mapping config json to IOption<MyConfig>
            // requires "Microsoft.Extensions.Options.ConfigurationExtensions" package
            services.Configure<MyConfig>(hostContext.Configuration);
        })
        .RunConsoleAsync();
}
 
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    IOptions<MyConfig> config;
    ILogger<MyFirstService> logger;
 
    public MyFirstService(IOptions<MyConfig> config, ILogger<MyFirstService> logger)
    {
        this.config = config;
        this.logger = logger;
    }
 
    // ...
}

好きな型を、ConfigureServicesのとこで追加してもらえれば、コンストラクタで設定されたのが入ってきます。

今後

v2のリリース告知から半年経って、かなり注目度が上がっているというのが肌感としてあります。GitHub Starも962まで来ていますし、海外からの問い合わせも国内からも来ていて、盛り上がりありますよ!時代はC#!かもしれない!

というわけかで、来月の6月4日に初のMagicOnion勉強会が開催されます。私も登壇しますので、ぜひぜひ来てください(今はもうキャンセル待ちですが……!)

開発的には、サーバーサイドゲームループ(まだ未サポート)などの追加を挟みつつ、もう少し野心的なものも狙っていますので、是非是非楽しみにしていただければと思います。コードジェネレーターの使い勝手が悪いのも、(MessagePack-CSharpともども)改善の最優先タスクの一つになってますので、なんとかします。

また、フィードバック超大事!なので、ぜひ使ってみて、Twitterでつぶやくなり(捕捉してます)、Qiitaに書いてくれるなり(やったー!)、Issueで報告してもらったりなどなどしてくれると嬉しいです。

CIや実機でUnityのユニットテストを実行してSlackに通知するなどする

前回(?)CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまででは、ユニットテストに関する部分がうまく行ってなくて放置でした。放置でいっかな、と思ってたんですが、改めてユニットテストをCIでがっつり実行したい、というかIL2CPPのテストをがっつしやりたい。という切実な要望が私の中で発生したので(N回目)、改めて取り組んでみました。

さて、オフィシャルな(?)ユニットテストのコマンドラインの実行の口は、Writing and executing tests in Unity Test Runnerの最後の方のRunning from the command lineの節に書いてありました(コマンドライン引数のほうのマニュアルにはリンクすら張ってなかったので気づかなかった……!)。つまり、こんなふうにやればいい、と。

Unity.exe -runTests -testResults C:\temp\results.xml -testPlatform StandaloneWindows64

そうすると、テストが失敗しても正常終了して(?) results.xml に結果が入ってるからそっち見ればOK、と。んー、いや、何か違うような。「Run all in player」で出てくるGUI画面も意味不明だし、Editor上のTest Runnerはいい感じなのだけれど、ビルドしてのテストだとイマイチ感がめっちゃ否めない。

と、いうわけで、なんとなく見えてきたのは、テストはUnity Test Runnerでそのまま書きたいしエディタ上でPlay Modeテストもしたい。それをそのままCIや実機でテストできるように、表示やパイプラインだけをいい具合に処理するビルドを作る何かを用意すればいいんじゃないか、と。

RuntimeUnitTestToolkit v2

ちょうどUnity Test Runnerがイマイチだった頃に作った俺々テストフレームワークがありました。ので、それを元にして、Unity Test RunnerのCLI/GUIのフロントエンドとして機能するようにリニューアルしました。コード的には全面書き換えですね……!

Unity Test RunnerのPlayModeで動くテストがあれば、それだけで他に何もする必要はありません。例えばこんなやつがあるとして

image

メニューのほうで適当にターゲットフレームワークとかIL2CPPがどうのとかを設定してもらって

image

BuildUnitTestを押すと、こんなような結果が得られます。

image

比較的ヒューマンリーダブルなログ!WindowsでもIL2CPPビルドができるようになったのがとっても捗るところで、検証用の小さめプロジェクトなら1分あればコード編集からチェックまで行けるので、リフレクションのキワイ部分をごりごり突いてもなんとかなる!昔のiOSでビルドして動かしてをやってたのは本当に死ぬほど辛かった……。

これはHeadless(CUI)でビルドしたものですが、GUIでのビルドも可能です。

image

イケてる画面かどうかでは微妙ですが、機能的には十二分です。Headlessだと上から下まで全部のテストを実行しちゃいますが、GUIだとピンポイントで実行するテストを選べるので(ただしメソッド単位ではなくクラス単位)、テストプロジェクトが大きくなっている場合はこっちのほうが便利ですね。

さて、Headlessでビルドしたものは、もちろんCIでそのまま実行できます。

image

これはNGが出ている例ですが、ちゃんと真っ赤にCIのパイプラインが止まるようになってます。止まればもちろんCIの通知設定で、Slackでもなんでもどこにでもサクッと飛ばせます。実に正しい普通で普遍なやり方でいいじゃないですか。はい。というわけでやりたかったことが完璧にできてるのでめでたしめでたし。

Linux ContainerとUnity

相変わらずCircleCIで色々トライしているのですが、Linuxコンテナ + Unityでの限界、というかUnityのLinux対応が後手に回ってる影響をくらってビミョーという現実がやっと見えてきました。まず、そもそもにLinux + IL2CPPはまだサポートされてないので、CI上でIL2CPPビルドしたものを実行してテスト、みたいなのはその時点でできない。残念。しゃーないのでWindows + IL2CPPビルドを作って、実行だけ手元でやるのでもいっか、と思ったらそもそもLinuxでIL2CPPビルドができない。なるほど、そりゃそうか、って気もしますが悲しみはある。

と、いうわけで、コンテナベースでやるとどうしてもLinuxの上でのパターンが中心になってしまうので、Unityだと結構厳しいところはありますよねえ、という。

さて、CircleCIの場合は(有料プランでは)Mac VMも使えるので、多少コンフィグの書き方も変わってきますが(マシンセットアップ部分が面倒くさくなる!)、動かせなくもないんちゃうんちゃうんといったところです。或いはAzure DevOpsなどを使えばWindowsマシンが使えるので、こちらもUnityのインストールなどのセットアップは必要ですが、安心感はありますね。どちらにせよWindowsでしかビルドできないもの(Hololensとか)もあるので、ちょっとちゃんと考えてみるのはいいのかなあ、と思ってます。

何れにせよ、VMでやるんだったらそりゃ普通にできますよね、という当たり前の結論に戻ってくるのが世の中きびすぃ。とりあえず私的にはIL2CPPビルドが実行できればいいので、Linux + IL2CPP対応をどうかどうか……。

RandomFixtureKit

ユニットテスト用にもう一個、RandomFixtureKitというライブラリを作りました。こちらは .NET Core用とUnity用の両対応です。

なにかというと、オブジェクトにランダムで適当な値を自動で詰め込むという代物です。当然リフレクションの塊で、これのIL2CPP対応に、先のRuntimeUnitTestToolkitが役に立ちました。

APIも単純でFixtureFactory.Createで取り出すだけ。

// get single value
var value = FixtureFactory.Create<Foo>();
 
// get array
var values = FixtureFactory.CreateMany<Bar>();
 
// get temporal value(you can use this values to use invoke target method)
var (x, y, z) = FixtureFactory.Create<(int, string, short)>();

テスト書いていてダミーのデータを延々と書くの面倒くせー、という局面はめっちゃあって、別に賢い名前なんて必要なくて(例えばAddressにはそれっぽい住所、Nameにはそれっぽい人名を入れてくれるとか)、全然ランダム英数でもいいから詰めてくれればそれでいいの!というところにピッタリはまります。

実用的には、私はシリアライザの入れ替えとか(なぜか)よくやるんですが、旧シリアライザと新シリアライザで互換性なくて壊れたりしないように、相互に値を詰めたりとかして、同一の結果が得られることを確認したりします。そのときに、dllをなめて対象になる数百の型を取って、RandomFixtureKitを使って、適当な値を詰めた上で、一致を比較するユニットテストを用意するとかやったりします。

面白い機能としては、ランダムな値ではなくて、エッジケースになり得る値だけを詰めるモードを用意しています。

たとえばintだったらint.MinValue, MaxValue, 0, -1, 1を。コレクションだったらnull, 長さ0, 長さ1, 長さ9の中からランダムで詰める、といったものですね。

こういうキワいデータが入ったときにー、みたいなことは想定しなきゃいけないし、テストも書いておかなきゃなのは分かってるけれど、毎回データ変えて流すのクソ面倒くさいんですよね(私はシリアライザを(なぜか)よく書くので、本当にこういうデータをいっぱい用意する必要が実際ある)。ので、CreateManyで1000個ぐらい作って流し込んでチェックすれば、多少はケースが埋まった状態になるでしょうというあれそれです。使ってみると意外と便利ですよ。

ところで

ゴールデンウィークの最終日なのですが、ほとんど何もやってない!始まる前は、MessagePack-CSharpやMagicOnionのタスクを潰しつつ、Pure C#のHTTP/2 Clientを作ってMagicOnionを強化するぜ、とか息巻いていたのですが全然できてない。副産物というか横道にそれたユニットテスト関連を仕上げて終わりとか、なんと虚しい……。

できなかった理由の半分はSwitchでCelesteを遊び始めたらめちゃくちゃハマって延々とやり続けちゃったせいなのですが、まぁそれはそれで面白いゲームをたっぷり楽しめたということで有意義なのでよしということにしておきます。

MagicOnionは6月4日に勉強会をやります。というわけで、やる気もかなりあるし、アップデートネタも溜まっているんですが、実際にアップデートはできてないので(Issueのヘンジはちゃんとやってます!)、GWでガッと手を入れておきたかったんですが、うーん、まぁ明けてからやりまうす。色々良い感じになっていると思います。いやほんと。

True Cloud Native Batch Workflow for .NET with MicroBatchFramework

AWS .NET Developer User Group 勉強会 #1にて、先日リリースしたMicroBatchFrameworkについて、話してきました。

True Cloud Native Batch Workflow for .NET with MicroBatchFramework from Yoshifumi Kawai

タイトルが英語的に怪しいですが、まぁいいでしょう(よくない)

MicroBatchFrameworkの概要については、リリース時にCygames Engineers’ BlogにてMicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワークとして書かせていただきました。そう、最近はそっち(どっち)に書いてしまうのでこっち(あっち)に書かれなくなる傾向が!リポジトリの置き場としても、Cysharpオーガナイゼーション中心になってきています。これは会社としてメンテナンス体制とかもしっかり整えていくぞ、の現れなので基本的にはいいことです。

ちなみにCysharp、ページ下段にお問い合わせフォームが(ついに)付きました。興味ある方は応募していただいてもよろしくてよ?ビジネスのお問い合わせも歓迎です。別にゲームに限らずで.NET Coreの支援とかでもいいですよ。ただしオールドレガシーWindows案件はやりません。

クラウドネイティブ

これはセッションで口頭で言いましたが、バズワードだから付けてます。という側面は大いにあります。世の中マーケティングなのでしょーがないね。そもそも私はそういうのに乗っかるの、好きです。

そんな中身のないクラウドネイティブですが(真面目な定義はCNCFのDefinitionにちゃんとあります)、まぁコンテナ化です。ベンダー中立な。というのをコンテナ化ビリティの高さという表現に落としました。.NET Coreは結構いい線言ってると思いますよ。実際。

さて、そんなクラウドネイティブなふいんきのところでの、理想のバッチ処理ってなんやねん。というのを考えて、逆算でアプリケーション側で埋めるべきものを埋めるために作ったのがMicroBatchFrameworkです。インフラ側の欠けてるところはそのうちクラウド事業者が埋めてくれるか、現状でも全然実用レベルで回避はどうとでもなるでしょう。

私としてはC#が快適にかければなんだっていいんですが、なんだっていいというだけではなくC#としての自由の追求に関しては相当ラディカルなのですが、でも、それって割とクラウドネイティブの定義(ちゃんとしたほうの)通りなんですよね。別にコンテナに夢見てるわけじゃなくて、意外と堅実に正しく定義どおりのことやってるわけです。まー、FaaSのオーケストレーターは私の理想からベクトル真逆だし、FaaSのランタイムの重さ(実行が遅いという意味ではなくてシステムとしてのヘヴィさ)も受け入れ難いんで、世の中の正しい進化について正面から向かい合うのが結局一番ということで。

ところでMicroBatchFrameworkのウェブホスティング機能(MicroBatchFramework.WebHosting)はSwaggerによる実行可能なドキュメント生成、のほかに、HTTPをトリガーにする待ち受けという側面もあります。GCP Cloud Runの実行のためにはそういうの必要ですからね。毎回コンテナ起動みたいな夢見たモデルだけじゃなくて、割とちゃんと現実に即して機能は用意してます。意外と。割とちゃんと。そもそも、その辺は実用主義なので。

MicroBatchFrameworkはいい具合のバランス感覚で作れていると思うので、実際良いと思います。というわけで、是非試していただければですね。

CircleCIでUnityをテスト/ビルドする、或いは.unitypackageを作るまで

死ぬほどお久しぶりです!別にインターネット的には沈黙してるわけじゃなくTwitterにもいるし、会社(Cysharp)関連で露出あるかもないかもというわけで、決して沈黙していたわけでもないはずですが、しかしブログは完全に放置していました、あらあら。

C#的にも色々やっていて、CloudStructuresのv2を@xin9leさんとともにリリースしたり、多分、今日に詳細を書くつもりですがMicroBatchFrameworkというライブラリをリリースしたり、Ulidというライブラリをリリースしてたり、まぁ色々やってます。ちゃんと。実際。今月はそのMicroBatchFramework関連で、AWS .NET Developer User Group 勉強会 #1に登壇しますし。リブートしたMagicOnionも来月勉強会開催予定だったりで、めっちゃやる気です。

さて、そんなやる気に満ち溢れている私なのですが(実際Cysharpもいい感じに動き出せているので!お問い合わせフォームないけどお問い合わせ絶賛募集中!)、ブログは放置。よくないね。というわけで表題の件について。

目的と目標

CIの有効性について未だに言う必要なんてなにもないわけですが、しかし、.unitypackageを手作業で作っていたのです。今まで。私は。UniRxとかMessagePack-CSharpの。そして死ぬほど面倒くさいがゆえに更新もリリースも億劫になるという泥沼にハマったのです。やる気が満ち溢れている時は手作業でもやれるけれど、やる気が低下している時でも継続してリリースできなければならないし、そのためにCIはきっちりセットアップしておかなければならないのです。という真理にようやく至りました。なんで今さらなのかというと、私がアプリケーション書くマンであることと、CIとかそういうのは全部、部下に丸投げして自分は一切手を付けてこなかったマンだからです。しかし会社のこともあるので、いい加減にそれで済まなくなってきたので(今更やっとようやく)真面目に勉強しだしたのですね……!

で、CIにはCircleCIを使います。なんでCircleCIなのかというと、一つはUnity Cloud Buildだとunitypackageを作れない(多分)というのが一つ。もう一つは、私が.NET CoreのCIもCircleCIに寄せているので、統一して扱えるといいよねというところです。また、Linuxの他にMacでのビルドもできるので(有料プラン)、iOSに、とかも可能になってくるかもしれませんしね。あと、単純にCircleCIが昨今のCIサービスで王者なので、長いものに巻かれろ理論でもある。でも私自身も最近使っていてかなり気に入ってるので、実際良いかと良いかと。コンテナベースで記述するのがとても小気味よいわけです、モダンっぽいし。

ゴールは

  • リポジトリの一部ソース郡から.unitypackageを作る
  • EditorでUnitTestを行う
  • IL2CPP/Windowsでビルドする(↑のUnitTestのIL2CPP版を吐く)

となります。普通はAndroidやiOSビルドがしたいって話だと思うのですが、私はライブラリAuthorなので、まずそっちの要求のほうが先ということで(そのうちやりたいですけどね!)。Editorテストだけじゃなくて、IL2CPPで動作するか不安度もあるので、そっちのexeも吐ければ嬉しい。できればIL2CPPビルドのものも、ヘッドレスで起動して結果レポーティングまでやれればいいん&ちょっと作りこめばそこまで行けそうですが、とりあえずのゴールはビルドして生成物を保存するところまでにしておきましょう。そこまで書いてると記事長くなるし。

認証を通してUnityをCircleCI上で動かす

CircleCIということでコンテナで動かすんですが、まぁUnityのイメージを持ってきてbatchmodeで起動して成果を取り出すという、それだけの話です。適当にUnityのコマンドライン引数とにらめっこすれば良い、と。

コンテナイメージに関しては、幸い誰か(gablerouxさん)がgableroux/unity3d/tagsに公開してくれていて、綺麗にタグを振ってくれています。コンテナの良いところっていっぱいあると思いますが、コンテナレジストリが良い具合に抽象化されたファイル置き場として機能するのも素敵なとこですねえ。また、こうして公開してくれていれば、社内CIのUnityインストール管理とかしないで済むのも良いところです。大変よろしい。

で、Unityの実態は /opt/Unity/Editor/Unity にあるので、それを適当に -batchmode で叩けばいいんでしょって話ですが、しかし最大の関門はライセンス認証。それに関してはイメージを公開してくれているgablerouxさんのGabLeRoux/unity3d-ci-exampleや、そして日本語ではCircleCIでUnityのTest&Buildを雰囲気理解で走らせたに、手取り足取り乗っているので、基本的にはその通りに動かせば大丈夫です。

ただ、ちょっと情報が古いっぽくて、今のUnityだともう少し手順を簡単にできるので(というのを試行錯誤してたら苦戦してしまった!)、少しシンプルになったものを以下に載せます。

まず、ローカル上でライセンスファイルを作る必要があります。これはdockerイメージ上で行います。また、ここで使うイメージはCIで実際に使うイメージと同じバージョンでなければなりません。バージョン変わったらライセンス作り直しってことですね、しょーがない。そのうちここも自動化したくなるかもですが、今は手動でやりましょう。

docker run -it gableroux/unity3d:2018.3.11f1 bash
cd /opt/Unity/Editor
./Unity -quit -batchmode -nographics -logFile -createManualActivationFile
cat Unity_v2018.3.11f1.alf

イメージを落としてきて、 -quit -batchmode -nographics -logFile -createManualActivationFile でUnityを叩くと Unity_v***.alf という中身はXMLの、ライセンスファイルの元(まだuseridもpasswordも入力してないので、テンプレみたいなものです)が生成されます。こいつを、とりあえず手元(ホスト側)に持ってきます。docker cpでコンテナ->ホストにファイルを移動させてもいいんですが、まぁ1ファイルだけなのでcatしてコピペして適当に保存でもOK。

次にhttps://license.unity3d.com/manualを開いて、上記のalfファイルを上げると Unity_v2018.x.ulf ファイルがもらえます。これが実体です。生成過程でUnityのサイトにログインしているはずで、そのuserid/passwordが元になって、ライセンスファイルの実体が生成されました。中身はXMLです。

で、これは大事な情報なのでCircleCI上のEnvironment Variablesで秘匿しよう、という話になるんですが、改行の入った長いXMLなので、そのまんま中身をコピペるとファイルが、たいていどこか壊れて認証通らなくなります(散々通らないでなんでかなぁ、と悩みました!)。とはいえファイルそのものをリポジトリに上げるのはよろしくないので、CircleCIでUnityのTest&Buildを雰囲気理解で走らせたにあるとおり、暗号化したものをリポジトリに追加して、Environment VariablesにはKeyを追加しましょう。

openssl aes-256-cbc -e -in ./Unity_v2018.x.ulf -out ./Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY}

${CIPHER_KEY}は、適当な文字列に置き換えてもらって、そしてこれをCircleCI上のEnvironment Variablesにも設定します。ファイルの置き場所は、とりあえず私は .circleci/Unity_v2018.x.ulf-cipher に置きました、CIでしか使わないものなので。

またはマルチラインキーの場合は base64を使うことが推奨されているようです => Encoding Multi-Line Environment Variables。こちらのほうが良さそうですね。

あとは .circleci/config.ymlを書くだけ、ということで、最小の構成はこんな感じになります。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
workflows:
  version: 2
  build:
    jobs:
      - build-test

-nographicsにすることでそのまま叩けるのと、-manualLicenseFileでライセンスファイルを渡してやるだけです。 認証する際の || exit 0 がお洒落ポイントで、認証が正常に済んでもexit code 1が返ってくるという謎仕様なので、とりあえずこのステップは強制的に正常終了扱いにしてあげることで、なんとかなります。なんか変ですが、まぁそんなものです。世の中。

まぁしかしGabLeRoux/unity3d-ci-exampleの(無駄に)複雑な例に比べれば随分すっきりしたのではないでしょうか。いやまぁ、Unityのイメージ作ってもらってるので感謝ではあるのですけれど、しかしサンプルが複雑なのは頂けないかなあ。私はサンプルは限りなくシンプルにすべき主義者なので。

.unitypackageを作る

バッチモードでは -executeMethod により特定のstatic methodが叩けるので、それでunitypackageを作るコードを用意します。 今回は Editor/PackageExport.cs に以下のようなファイルを。

using System;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
 
// namespaceがあると動かなさそうなので、グローバル名前空間に置く
public static class PackageExport
{
    // メソッドはstaticでなければならない
    [MenuItem("Tools/Export Unitypackage")]
    public static void Export()
    {
        // configure
        var root = "Scripts/CISample";
        var exportPath = "./CISample.unitypackage";
 
        var path = Path.Combine(Application.dataPath, root);
        var assets = Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
            .Where(x => Path.GetExtension(x) == ".cs")
            .Select(x => "Assets" + x.Replace(Application.dataPath, "").Replace(@"\", "/"))
            .ToArray();
 
        UnityEngine.Debug.Log("Export below files" + Environment.NewLine + string.Join(Environment.NewLine, assets));
 
        AssetDatabase.ExportPackage(
            assets,
            exportPath,
            ExportPackageOptions.Default);
 
        UnityEngine.Debug.Log("Export complete: " + Path.GetFullPath(exportPath));
    }
}

ちょっとassetsを取るところが長くなってしまっているのですが、.cs以外をフィルタするコードを入れています。たまに割と入れたくないものが混ざっていたりするので。あとは、CIではライセンス認証のあとに、これを叩くコマンドと、artifactに保存するコマンドを載せれば良いでしょう。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
workflows:
  version: 2
  build:
    jobs:
      - build-test

完璧です!

コマンドに関しては普通にWindowsのUnity.exeで試してから挑むのがいいわけですが、一つWindowsには難点があって、ログが標準出力ではなく %USERPROFILE%\AppData\Local\Unity\Editor\Editor.log にしか吐かれないということです。というわけで、Editor.logを開いてにらめっこしながらコマンドを作り込みましょう。めんどくせ。

EditorでUnitTestを行う

基本的に -runEditorTests をつけるだけなのですが、注意点としては -quit は外しましょう。ついてると正常に動きません(はまった)。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
 
      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results
workflows:
  version: 2
  build:
    jobs:
      - build-test

editorTestsResultFile で指定し、store_test_resultsに格納することでCircleCI上でテスト結果を見ることができます。

と、思ったんですが、なんかテスト周りは全体的にうまく動かせてないんで後でまた調べて修正します……。或いは教えてくださいです。

IL2CPP/Windowsでビルドする

なぜWindowsかというと、私がWindowsを使っているからというだけなので、その他のビルドが欲しい場合はそれぞれのビルドをしてあげると良いんじゃないかと思います!

いい加減コンフィグも長くなってきましたが、-buildWindows64Playerでビルドして、zipで固めてぽんということです。

version: 2.1
executors:
  unity:
    docker:
      # https://hub.docker.com/r/gableroux/unity3d/tags
      - image: gableroux/unity3d:2018.3.11f1
jobs:
  build-test:
    executor: unity
    steps:
      - checkout
      - run: openssl aes-256-cbc -d -in .circleci/Unity_v2018.x.ulf-cipher -k ${CIPHER_KEY} >> .circleci/Unity_v2018.x.ulf
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -logFile -manualLicenseFile .circleci/Unity_v2018.x.ulf || exit 0
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -executeMethod PackageExport.Export
      - store_artifacts:
          path: ./CISample.unitypackage
          destination: ./CISample.unitypackage
 
      - run: /opt/Unity/Editor/Unity -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -runEditorTests -editorTestsResultFile ./test-results/results.xml
      - store_test_results:
          path: test_results
 
      - run: /opt/Unity/Editor/Unity -quit -batchmode -nographics -silent-crashes -noUpm -logFile -projectPath . -buildWindows64Player ./bin-win64/CISample.exe
      - run: apt-get update
      - run: apt-get install zip -y
      - run: zip -r CISampleWin64Binary.zip ./bin-win64
      - store_artifacts:
          path: ./CISampleWin64Binary.zip
          destination: ./CISampleWin64Binary.zip
workflows:
  version: 2
  build:
    jobs:
      - build-test

これで一旦は希望のものは全てできました!

以上な感じが最終結果になります。

CircleCIでUnityビルドはプロダクトで使えるか

今回の例のようなライブラリ程度だと、リソースもほとんどないしリポジトリも全然小さいんでいいんですが、実プロダクトで使えるかというと、どうでしょう。まずリポジトリのサイズの問題で、次にビルド時間の問題で。クソデカい&高級マシンでも焼き上がり1時間は普通とか、そういう世界ですものね。常識的に考えてこれをクラウドでやるのは難しそう。オンプレのCircleCI Enterpriseだったら行けそうな気もしますが、どうでしょうねえ。しかしJenkinsマンやるよりは、こちらのほうが夢があるのと、実際うまくクラスタを組めば、ばかばかコンテナ立ち上げて同時並列でー、というビルドキュー長蛇列で待ちぼうけも軽減できたりで、良い未来は感じます。試してみたさはあります、あまりJenkinsに戻りたくもないし。

一回構築してみれば、ymlもそこそこシンプルだし、(ライセンス認証以外は)ymlコピペで済むので、Unity Cloud Build使わなくてもいいかなー、色々自由にできるし。っていうのはあります。というわけで、是非一緒にUnityでCircleCI道を突き進んでみましょう:) 今回はAndroidビルドやiOSビルドという面倒くさいところには一切手を付けてませんが、まぁほとんどビルドできてるわけで、やりゃあできるでしょう。いや、でもiOSとか死ぬほど面倒くさ(そう)なので、そのへんよしなにやってくれつつマシンパワーもそこそこ用意してくれるUnity Cloud Buildは偉い。

ところでこのブログ、ymlのシンタックスハイライトがない模様。やべー。このブログのメンテこそが一番最重要な気がしてきた。

MagicOnion v1 -> v2リブート, gRPCによる.NET Core/Unity用ネットワークエンジン

先にCygames Engineers’ BlogでMagicOnion – C#による .NET Core/Unity 用のリアルタイム通信フレームワークとしてリリースを出しましたが、改めまして、MagicOnionというフレームワークを正式公開しました。

GitHub - Cysharp/MagicOnion

MagicOnionはAPI通信系とリアルタイム通信系を一つのフレームワークで賄う、というコンセプトを元に、前職のグラニで「黒騎士と白の魔王」の開発において必要に迫られて捻り出されたものでした。

「黒騎士と白の魔王」gRPCによるHTTP/2 - API, Streamingの実践 from Yoshifumi Kawai

で、今更気づいたのがMagicOnionって正式リリースしてなかったんですよね、このブログでも↑のような形でしか触れていなくて、公式ドキュメントも貧弱な謎フレームワークだったという。今回Ver2って言ってますが、その前はVer0.5でしたし。まぁここでは便宜的にv1と呼びます。

何故に正式リリースまで行かなかったかというと、リアルタイム通信部分が微妙だったから。↑のp.39-40で説明していますが、Unary + ServerStreamingという構成で組んだのが、かなり開発的に辛かったんですね。時間的問題もあり強行するしかなかったんですが、ちゃんと自分が納得いく代案を出せない限りは、大々的には出していけないなあ、と。

その後すったもんだがあったりなかったりで、プレーンなgRPCでリアルタイム通信を組む機会があって、↑の時に考えていたDuplexStreaming一本で、コマンドの違いをProtobufのoneofで吸収する、という案でやってみたのですが、すぐに気づきましたね、これ無理だと。UnaryはRPCなのですが、Duplex一本はRPCじゃないんで、ただたんにoneofをswitchしてメソッド呼び出す、だけじゃ全然機能足りてない、と。

ただまぁコネクション的にはDuplex一本案は間違ってなさそうだったので、その中で手触りの良いRPCを組むにはどうすればいいか……。と、そこでMagicOnionをリブートさせるのが一番手っ取り早いじゃん、というわけで着手したりしなかったりしたりしたのでした。その間にCysharpの設立の話とかもあり、事業の中心に据えるものとしても丁度良かったという思惑もあります。

早速(?)Qiitaでも何件か紹介記事書いてもらいました。

v1 -> v2

Unary系(API通信系)はほとんど変わっていません。それは、v1の時点で十分に高い完成度があって、あんま手を加える余地はなかったからですね。ただしフィルターだけ戻り値をTaskからValueTaskに変えています。これはフィルターの実行はメソッド実行前に同期的にフック(ヘッダから値取り出してみるとか)するだけ、みたいなものも多いので、TaskよりValueTaskのほうが効率的に実行できるからですね。

元々フィルターを重ねることによるオーバーヘッドを極小にするため、純粋にメソッド呼び出しが一個増えるだけになるように構成してあったのですが、更により効率的に動作するようになったと思います。

SwaggerのUIを更新するのと、HttpGatewayの処理を効率化するのが課題として残っているので、それは次のアップデートでやっていきます。

また、Unity向けにはコードジェネレート時にインターフェイス定義でTaskをIObservableに変換していたのですが、今のUnityは.NET 4.xも使えるということで、インターフェイスはそのまま使ってもらうようにしています。Taskのままで。

StreamingHub

の、導入に伴って、v1でリアルタイム通信系をやるための補助機構である StreamingContextRepository を廃止しました。StreamingContextRepositoryは、まぁ、正直微妙と思っていたのでなくせて良かったかな。決して機能してないわけではないのですけれど。

代わりのコネクションを束ねる仕組みはGroupという概念を持ってきました。これはASP.NETのWebsocketライブラリであるASP.NET Core SignalRにあるものを、再解釈して実装しています。

MagicOnionのGroupの面白いところは、裏側の実装を変えられることで、デフォルトはImmutableArrayGroupという、MORPGのようなルームに入って少人数で頻繁にやり取りするようなグルーピングに最適化された実装になっています。もう一種類はConcurrentDictionaryGroupという、こちらはMMORPGやグローバルチャットのような、多人数が頻繁に出入りするようなグルーピングのための実装です。更に、RedisGroupというバックエンドにRedisのPubSubを置いて複数サーバー間でグループを共有するシステムも用意しています、これはチャットや、全体通知などに有効でしょう。

また、GroupにはInMemoryStorage[T]というプロパティが用意されていて、グループ内各メンバーに紐付いた値をセットできるようにしています。これは、通信のブロードキャスト用グループの他に、値の管理のためにConcurrentDictionary[ConnectionId, T]のようなものを用意してデータを保持したりが手間で面倒くさいんで、いっそグループ自体にその機能持ってたほうが便利で最高に楽じゃん、という話で、実際多分これめちゃくちゃ便利です。

まとめ

というわけで、リブートしました!最初チョロいと思ってたんですが、割とそんなことはなくて、この形にまとめあげるまではそれなりに大変でした……。の甲斐もあって、今回のMagicOnionはかなり自信を持って推進できます。以前はそもそもgRPC本体をフォークして魔改造したり、というのもあったのですが、今は公式ビルドを使えるようになったのでUnity向けにも良い具合になってきています。

MagicOnion2の内容は、(v1を)実際に使ってリリースした後の反省点が盛り込まれているので、そういう点で二周目の強みがあります。最初からこの形で出すのは絶対にできないであろうものなので、しっかりと経験が活かされています。実プロダクトで使って初めて見えるものっていっぱいありますからねー。とはいえv1はv1で大きな役割を果たしたと思いますし、まぁあと自分で言うのもアレですが「黒騎士と白の魔王」が証明したこと(gRPCがUnityでいけるんだぞ、という)ってメチャクチャ大きかったなあ、と。

CysharpとしてもMagicOnion、使っていきますし、ほんと是非是非使ってみてもらえると嬉しいです。コードジェネレーターもついにWin/Mac/Linux対応しましたので(まだ微妙にバグいのですが年内か、年明け早々にはなんとかします)、ガッツリと使っていけるのではないかとです。

UniTask(UniRx.Async)から見るasync/awaitの未来

C# Advent Calendar 2018大遅刻会です。間に合った。間に合ってない。ごめんなさい……。今回ネタとして、改めてコード生成に関して、去年は「動的」な手法を解説した - Introduction to the pragmatic IL via C#ので、現代的な「静的」な手法について説明してみよう、と考えていたのですが、そういえばもう一つ大遅刻がありました。

7月にUniTask - Unity + async/awaitの完全でハイパフォーマンスな統合という記事を出して、リリースしたUniTaskですが、その後もちょこちょこと更新をしていて、内部実装含め当初よりもかなり機能強化されています。といった諸々を含めて、Unity 非同期完全に理解した勉強会で話してきました。

Deep Dive async/await in Unity with UniTask(UniRx.Async)

9月!更新内容の告知もしてなければ、この発表のフォローアップもしてない!最近はこうした文章仕事がめっちゃ遅延するようになってしまいました、めっちゃよくない傾向です。来年はこの辺もなんとかしていきたい。

と、いうわけで、予定を変えてUniRx.Asyncについて、というか、それだとUnity Advent Calendarに書けよって話になるので、UniRx.Asyncは独自のTask生態系を作っている、これは.NET Core 2.1からのValueTaskの拡張であるIValueTaskSourceに繋がる話なので、その辺を絡めながら見ていってもらえると思います。

Incremental Compilerが不要に

告知が遅延しまくっている間にUnity 2018.3が本格リリースされて、標準でC# 7.xに対応したため、最初のリリース時の注釈のような別途Incremental Compiler(preview)を入れる必要がなくなりました。Incremental Compiler、悪くはないのですが、やっぱpreviewで怪しい動きもしていたため、標準まんまで行けるのは相当嬉しいです。というわけで今まで敬遠していた人も早速試しましょう。

new Progress[T] is Evil

これは普通の.NETにも言える話なのですが、C#のasync/awaitの世界観では進捗状況はIProgress[T]で通知していくということになっています(別にAction[T]でよくね?そっちのほうが速いし、説はある)。進捗はReport(T value)メソッドで通知していくことになりますが、こいつは必ずSynchronizationContext.Post経由で値を送ります。これがどういうことかというと、Unityだとfloatを使う、つまりIProgress[float]で表現する場合が多いはずですが、なんと、ボックス化します。(If T is a value type, it will get boxed here.)じゃねーよボケが。アホか。これはオプションで回避不能なので、new Progress[T]は地雷だと思って「絶対に」使わないようにしましょう。

代わりにUniRx.AsyncではProgress.Createを用意しました。これはSynchronizationContextを使いません。もしSyncContext経由で同期したいならマニュアルでやってくれ。Unityの場合、進捗が取れるシチュエーションはメインスレッド上のはずなので、ほとんどのケースでは不要なはずです。

こういった、あらゆる箇所での.NET標準の余計なお世話を観察し、Unityに適した形に置き直していくことをUniRx.Asyncではやってるので、async/await使うならUniRx.Asyncを使ったほうがいいのです。標準のも、今の時代で設計するならこうはなってないと思うんですけどね、まぁ時代が時代なのでshoganai。

コルーチンの置き換えとして

コルーチン、或いはRxでできた処理は、改めて全部精査して、全てasync/awaitで実装できるようにしました。

Add UniTask.WaitUntil
Add UniTask.WaitWhile
Add UniTask.WaitUntilValueChanged
Add UniTask.WaitUntilValueChangedWithIsDestroyed
Add UniTask.SwitchToThreadPool
Add UniTask.SwitchToTaskPool
Add UniTask.SwitchToMainThread
Add UniTask.SwitchToSynchronizationContext
Add UniTask.Yield
Add UniTask.Run
Add UniTask.Lazy
Add UniTask.Void
Add UniTask.ConfigureAwait
Add UniTask.DelayFrame
Add UniTask.Delay(..., bool ignoreTimeScale = false, ...) parameter

概ね名前からイメージ付くでしょう、イメージ通りの挙動をします。こんだけ用意しておきゃほとんど困らないはず(逆に言えば、標準のasync/awaitには何もありません)

ちなみにSwitchTo***は、最初のVisual Studio Async CTP(お試しエディション)に搭載されていたメソッドで、すぐに廃止されました。というのも、async/awaitが自動でスレッド(SynchronizationContext)をコントロールするというデザインになったからですね。あまりにも最初期すぎる話なのでこの辺の話が残っているものも少ないのですが、ちゃんと岩永さんのブログには残っていたので大変素晴らしい。

UniRx.Asyncでは不要なオーバーヘッドを避けるため(そもそも特にUnityだとメインスレッド張り付きの場合のほうが多い)、自動でSynchronizationContextを切り替えることはせず、必要な場合に手動で変更してもらうというデザインを取っています。というか、今からasync/await作り直すなら絶対こうなったと思うんだけどなぁ、どうなんでしょうねぇ。ちょっとSynchronizationContextに夢見すぎだった時代&Windows Phone 7(うわー)とかの要請が強すぎたせいっていう時代背景は感じます。

Everything is awaitable

考えられるありとあらゆるものをawait可能にしました。AsyncOperationだけじゃなくてWWWやJobHandle(そう、C# Job Systemもawaitできます!)、そしてReactivePropertyやReactiveCommand、uGUI Events(button.OnClickAsyncなど)からMonoBehaviour Eventsまで。

さて、AsyncOpeartionなど長さ1の非同期処理がawait可能なら、そらそーだ、って話なのですが、イベントがawait可能ってどういうこっちゃ、というところはあります。

// ようするところこんな風に待てる
async UniTask TripleClick(CancellationToken token)
{
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    await button.OnClickAsync(token);
    Debug.Log("Three times clicked");
}

コレに関してはスライドに書いておきましたが、「複雑なイベントの合成」をする際に、Rxよりも可読性良く書ける可能性があります。

Rxは「複雑なイベントハンドリング」を簡単にするものじゃなかったの!?という答えは、YesでもありNoでもありで、複雑なものは複雑で、難しいものは難しいままです。イベントハンドリングは手続き的に記述出来ない(イベントコールバックが飛び飛びになる)ため、コールバックを集約させて合成できるRxが、素のままでやるより効果的だったわけですが、async/awaitはイベントコールバックを手続き的に記述できるため、C#のネイティブのコントロールフロー(for, if, whileなど)や自然な変数の保持が可能になります。これは関数合成で無理やり実現するよりも、可読性良く実現できる可能性が高いです。

単純なものをasync/awaitで記述するのは、それはそれで効率やキャンセルに関する対応を考慮しなければならなくて、正しく処理するのは地味に難易度が高かったりするので、基本的にはRxで、困ったときの必殺技として手段を知っている、ぐらいの心持ちが良いでしょう

async UniTask TripleClick(CancellationToken token)
{
    // 都度OnClick/token渡しするよりも最初にHandlerを取得するほうが高効率
    using (var handler = button.GetAsyncClickEventHandler(token))
    {
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        await handler.OnClickAsync();
        Debug.Log("Three times clicked");
    }
}

↑こういう色々なことを考えるのが面倒くさい。

Exception/Cancellationの扱いをより強固に

UniTaskでは未処理の例外はUniTaskScheduler.UnobservedTaskExceptionによって設定されている未処理例外ハンドラによって処理されます(デフォルトはロギング)。これは、UniTaskVoid、或いはUniTask.Forgetを呼び出している場合は即時に、そうでない場合はUniTaskがGCされた時に未処理例外ハンドラを呼びます。

async/awaitが持つべきステータスは「正常な場合」「エラーの場合」「キャンセルの場合」の3つがあります。しかし、async/awaitならびにC#の伝搬システムは、正常系は戻り値、異常系は例外の二択しかないため、「キャンセルの場合」の表現としてawaitされた元にはOperationCanceledExceptionが投げられます。よって、例外の中で、OperationCanceledExceptionは「特別な例外」です。デフォルトではこの例外が検出されて未処理の場合は、未処理例外ハンドラを無視します。何もしません。キャンセルは定形の処理だと判断して、無視します。

また、例外を使うためパフォーマンス上の懸念もあります。そこで、UniTask.SuppressCancellationThrowを使うことで、対象のUniTaskが例外の発生源であれば(throw済みで上の階層に伝搬されたものではない)、例外の送出ではなく、Tupleでの戻り値としてキャンセルを受け取り、例外発生のコストを抑えることができます。これはイベントハンドリングなどの場合に有用です、が、正しく使うことは内部をかなりのレベルで理解していないといけないため、ぶっちゃけムズい。ただたんにSuppressCancellationThrowを使うだけでパフォーマンスOKというわけにはいかんのだ。というわけで、どうしてもパフォーマンス的に困ったときのための逃げ道、ぐらいに思っておいてください。

UniTaskTracker

とはいえなんのかんのでTaskがリークしてしまったり、想像以上に多く起動してしまっていたりもあるでしょう。UnityのEditor拡張でトラッキングウィンドウを用意したので、すべて追跡できます。

image

こういうのRxにも欲しいわー。そうですね、なんか実装方法は考えてみようかとは思いますが一ミリも期待しないで待たないでください。

IValueTaskSourceでWhenAllを進化させる

.NET CoreのC#はTaskとValueTaskに分かれているわけですが、面倒くせーから全てValueTaskでいーじゃん、というわけにはいきません(なお、私の意見は全部ValueTaskでいいと思ってます、というのも使い分けなんて実アプリ開発でできるわけないから)。そうはいかない一番大きな理由はWhenAllで、このTaskで最も使われる演算子であろうWhenAllは、Taskしか受け取らないので、Taskへの変換が必要になってきます。せっかくValueTaskなのにモッタイナイ。じゃあValueTask用のWhenAllを作ればいいじゃん、というとそれも無理で、Task.WhenAllはTaskのinternalなメソッドに依存して最適化が施されているので、外部からはどうしても非効率的なWhenAllしか作れない仕様になっています(クソですね!)。

が、しかし、そもそもWhenAllってあんま効率的じゃなくないっすか?というのがある。と、いうのも、配列を受け取るAPIでも、まず保守的にコピーしてるんですよね。可変長引数でWhenAll(new[]{ foo, bar, baz })みたいに渡してもコピーされてるとか馬鹿らしい!あと、WhenAllの利用シーンでもう一つ多いのが WhenAll(source.Select(x => x.FooAsync()))のような、元ソース起点に非同期メソッドを呼んで、それを全部待つ、みたいなシチュエーション。なんかねー、別に配列作んなくてもいいじゃん、みたいな気になるんですよね。

と、そこでIValueTaskSourceの出番で、Task(ValueTaskですが)の中身を完全に自分の実装に置き換えることができるようになった、のがIValueTaskSourceです。よって、真に効率的なValueTaskに最適化されたうえで↑のような事情を鑑みたWhenAll作れるじゃん、って。思ったわけですよ。

そこでMagicOnionでは(UniRx.Asyncじゃないのかよって、IValueTaskSourceはUnityの話じゃないですから!)ReservedWhenAllPromiseというカスタムなWhenAllを用意してみました。

var promise = new WhenAllPromise(source.Length);
foreach (var item in source)
{
    promise.Add(item.FooAsync());
}
await promise.AsValueTask();

のように書けます。つまり何かと言うと、WhenAllに必要なのは「個数」で、個数が最初から確定しているなら、それを渡せばいいし、WhenAll自体の駆動に配列は必要ないので、随時Addしてあげてもいいわけです。これで、一切配列を使わない効率的なWhenAllが実装できました。めでたし。

他にも型が異なるTaskをawaitするのにValueTupleで受け取りたい、というのをTask.WhenAllを介さずにその個数に最適化したWhenAllを用意するとか、やりたい放題にめっちゃ最適化できるわけです。

と、いうのも踏まえて、(サーバーサイドC#における)アプリケーションのTaskの定義はValueTaskで統一しちゃっていいと思うし、そのかわりに幾つかの最適化したValueTask用のWhenAllを用意しましょう。というのが良い未来なんじゃないかなー、って思ってます。(このValueTask用のWhenAllのバリエーションはCysharpとして作ったらOSSで公開するので、こちらは期待して待っててください!)

まとめ

UniRx.AsyncナシでUnityにasync/awaitを持ち込んで使いこなすのはかなりの無理ゲーなので、よほどUnity以外で使い込んできた経験がある、とかでなければ、素直に使って頂ければと思います。また、そうでなくてもUnity向けに完全に作り直しているUniTaskの存在価値というのは、スライドのほうで十分理解してもらえてるのではとも思っています。

別にCLRの実装は至高のものだ!ってこたぁ全然なくて、時代とかもあるんで、後の世に作り直されるこたぁ往々にめっちゃある。Microsoftのハイパーエンジニアが練りに練ったものだろうがなんだろうが、永遠に輝き続けるコードなんてあんまなく、時代が経ちゃあどれだけ丁寧に作られたものでも滅びるんです。人間もプログラムも老化には逆らえない(WPFなんて何年前のUIフレームワークなんでしょう!)。というわけで、あんまり脳みそ固くせず、自分の意志で時々に見直して考えてみるといいんじゃないでしょうか。(古の)Microsoftよりも(現代の観点では)私のほうが正しい、とか自信持って言っておきましょう。

さて、UniRx.Asyncは(UniRxも)まだまだ完成しきってるとは言えない、のにドキュメント放置、更新放置で例によって半年ぐらい来てしまったのですが、その間は株式会社Cysharpを設立しましたであったり、MagicOnionのリブートであったり、結構わたわたしてしまったところがありなのですが、ようやく諸々落ち着いてきたので、また腰据えて改善に取り組んでいきたいと思います。まぁドキュメントが全然足りないんですけど(UniRx.Asyncの機能は、かなり膨大なのです……)。

C#的にも、自分でTaskの全域を見つめ直して作り直すという経験を通して得られたものも多かったので、今回の記事もそうですが、Unity関係なくasync/awaitを使っていく上で使える話は色々出せていければというところですね。ではまた次回の更新の時まで!次こそはすぐブログ書きますから!

Memory Management of C# with Unity Native Collections

と、題してECS完全に理解した勉強会で登壇してきました。

Memory Management of C# with Unity Native Collections from Yoshifumi Kawai

ECSは今後力を入れていきたい分野で、LTドリブン開発ということで、登壇するからにはやってこにゃ!という意気込みだったのですが諸々が諸々で色々間に合わずだったので、ややお茶を濁した展開になってしまいました。なむ。それは別として、これ自体は結構いい話なんじゃないかとは思います。

制約には必ず理由があるはずで、UnityやECSが持つ制約(それは時にC#らしくない、という難癖に繋がる)も、その理由をちゃんと紐解けば合理的な判断に見えるはずです。そこを示していきたいな、というのが今回の発表の流れです。時間的都合もあってECS成分が薄くなってしまいましたが、意味や繋がりは分かってもらえたはずです。私はCoreCLRのアプローチもUnityのアプローチも、どっちもいいと思ってるしどっちも面白く感じられているので、両者を見ながらC#の可能性を広げていきたいですね。

まるでC++というか原始時代に回帰してると言えなくもないんですが、表面のレイヤーはmanagedなC#であることに変わりないし、なるべくその表面のレイヤーを増やす努力は続いていると思われます!ただ、一昔前では、そこC++がー、とかそこはランタイムがー、で賄っていた部分がC#で実装するものとして表に出てきたんですね。これ自体はいいことなのですが、故に、使いこなすための知識としては、回帰してます。(Spanはunsafeまみれじゃないぞ、と言いたいかもしれませんが、Unsafe.***はunsafeマークのついてない実質unsafeなので、むしろより悪質です)。

時代は変わっていくし、C#らしさも変わっていくわけなので、そこは「面白く思うこと」が何より大事だし、変わったものには素直に従って深く追求していく姿勢が大事。乗り遅れず、最前線でやっていきましょう!

最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する

と、題しまして、CEDEC 2018にて講演してきました。

CEDEC 2018 最速のC#の書き方 - C#大統一理論へ向けて性能的課題を払拭する from Yoshifumi Kawai

今回、事前情報で事務局による誤記が漏れたり、当日のスポンサードの表記から分かる通り、縁がありこのセッションは某社にスポンサードいただきました。公募で落ちてしまい話したくても話せないというパターンも少なくない中で、このような形で枠を得られたのは、申し訳なさもありますが、同時に幸運でもありますので、いつになく気合を入れて仕上げてきました!(ほんと)。難易度はそこまで高いわけじゃない(実際、中辛で設定しました)ので、もっとdopeな話を聞きたい人には物足りなさもあるかもですが、C#というテーマだとあまり語られることのない、目新しい領域の話ですので、新鮮に聞いていただけた方も多いのではないかと思います。

会場も満員御礼で立ち見からの入場規制まで行きましたし、反響も資料は多くリツイートされましたし、反応もかなり良いようなので、かなり満足度高いセッションということでいいのではないでしょうか(多分!)。

謎のChapter4, 5, 6は勿論、最初から存在しない……!次回にご期待下さい。また来年。

Chapter 4: async/await Hackに関しては、来月9/15に行われる「Unity 非同期完全に理解した勉強会」にて話させていただく「Deep Dive UniRx.Async(UniTask)」に内容含まれますので、そちらに乞うご期待。ぜひ参加どうぞどうぞ、と言いたいところなのですがめちょくちょ埋まってしまっているので、ぜひ生放送のほうを見てください。

C#元年を始めよう

去年Uniteにて発表したC#大統一理論がかなりインパクトあったというのもあり、また、その波及効果なのかはさておき、最近手応えを感じ始めてきところでもあります。なのでまぁ、諦めたくないんですね、ここで踏み込んでいけばまだまだ行けるはずなんだ、という。

というわけかで、これは自分自身へのスローガンでもあり、あらゆる方向からアプローチしていこう、と。最初の方で冗談めいて言ってるトライアングル論法は、割と真面目な話でもあり、UniRxの強化(async/await, UniTask)は、よりサーバーサイドC#との親和性を高めていくことにも実は繋がっているのです。実は。

全方位で武器を磨き示すことによって、多くの人が合理的な判断としてC#を選択していく。そんな時代が作れるんじゃないか、始まるんじゃないか。そんな期待と、そのために色々やっていくぞ、という宣言なわけです。ほんと。

こういうの昔もなんかあったなー、と思い出すものがありまして、2012年にgloopsを辞めてからグラニが表に出る前の謎の空白期間があったのですが、その頃のような心境です。何か大きなことを仕込んでいて、やってやるぞ!という、あれそれです。

というわけで、外から見て私が何やってるかは完全に謎だとは思いますが、今後の私の活動にも期待しておいてくださいな。

Prev |

Search/Archive

Category

Profile


Yoshifumi Kawai
Microsoft MVP for Developer Technologies(C#)

April 2011
|
July 2020

Twitter:@neuecc
GitHub:neuecc
ils@neue.cc