IL2C短信: milestone3

IL2Cの短信です。milestone3となる、ValueTypeのサポートを(限定的ですが)行いました。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project


IL2Cをmicro:bitで動かす (#6-24・#6-25)

  • #6-24・#6-25の時点のIL2Cを使用して、micro:bitArduino UNOのCコンパイラを使って実際に極小環境で動かしました。

Making Archive IL2C at .NET Conf 2017 Tokyo (#6-28)

namespace il2c_test_target
{
    public class Hoge1
    {
        public static int Add1(int a, bool isTwo)
        {
            return a + (isTwo ? 2 : 1);
        }
    }
}
int32_t il2c_test_target_Hoge1_Add1(int32_t a, bool isTwo)
{
    int32_t local0;

    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_int32_t = a;
    __stack1_int32_t = isTwo ? 1 : 0;
    if (__stack1_int32_t != 0) goto L_0000;
    __stack1_int32_t = 1;
    goto L_0001;
L_0000:
    __stack1_int32_t = 2;
L_0001:
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    local0 = __stack0_int32_t;
    goto L_0002;
L_0002:
    __stack0_int32_t = local0;
    return __stack0_int32_t;
}

milestone3 (#6-37)

  • byte, sbyte, short, ushort, boolのサポート。
  • ValueType(構造体)のフィールドとメソッドをサポート。スタティックとインスタンスメンバの両方共に変換出来ます。
    スタティックメンバはCソースコード上、グローバル変数とグローバル関数として変換し、インスタンスフィールドは構造体、インスタンスメソッドはグローバル関数として変換。
  • 名前空間と型名についてのマングリングの導入。
  • 値を返さないメソッド(void)のret OpCodeのサポート
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace il2c_test_target
{
    public struct Hoge3
    {
        public static int Value1 = 123;
        public int Value2;

        public int GetValue2(int a, int b)
        {
            return this.Value2 + a + b;
        }
    }

    public class Hoge4
    {
        public static int Test4()
        {
            var hoge3 = new Hoge3();
            hoge3.Value2 = 456;

            return hoge3.Value2;
        }

        public static int Test5()
        {
            var hoge3 = new Hoge3();
            hoge3.Value2 = 789;

            var result = hoge3.GetValue2(123, 456);
            return result;
        }
    }
}
#ifndef __MODULE_il2c_test_target__
#define __MODULE_il2c_test_target__

#include <stdint.h>
#include <string.h>
#include <stdbool.h>

typedef struct il2c_test_target_Hoge3
{
    int32_t Value2;
} il2c_test_target_Hoge3;

extern int32_t il2c_test_target_Hoge3_Value1;

extern int32_t il2c_test_target_Hoge3_GetValue2(il2c_test_target_Hoge3* __this, int32_t a, int32_t b);
extern int32_t il2c_test_target_Hoge4_Test4(void);
extern int32_t il2c_test_target_Hoge4_Test5(void);

#endif
#include "il2c_test_target.h"

int32_t il2c_test_target_Hoge3_Value1 = 123;

int32_t il2c_test_target_Hoge3_GetValue2(il2c_test_target_Hoge3* __this, int32_t a, int32_t b)
{
    int32_t local0;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = __this;
    __stack0_int32_t = __stack0_il2c_test_target_Hoge3_reference->Value2;
    __stack1_int32_t = a;
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    __stack1_int32_t = b;
    __stack0_int32_t = __stack0_int32_t + __stack1_int32_t;
    local0 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local0;
    return __stack0_int32_t;
}

int32_t il2c_test_target_Hoge4_Test4(void)
{
    il2c_test_target_Hoge3 local0;
    int32_t local1;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    il2c_test_target_Hoge3 __stack0_il2c_test_target_Hoge3;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = &local0;
    memset(__stack0_il2c_test_target_Hoge3_reference, 0x00, sizeof(il2c_test_target_Hoge3));
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 456;
    __stack0_il2c_test_target_Hoge3_reference->Value2 = __stack1_int32_t;
    __stack0_il2c_test_target_Hoge3 = local0;
    __stack0_int32_t = __stack0_il2c_test_target_Hoge3.Value2;
    local1 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local1;
    return __stack0_int32_t;
}

int32_t il2c_test_target_Hoge4_Test5(void)
{
    il2c_test_target_Hoge3 local0;
    int32_t local1;
    int32_t local2;

    il2c_test_target_Hoge3* __stack0_il2c_test_target_Hoge3_reference;
    int32_t __stack0_int32_t;
    int32_t __stack1_int32_t;
    int32_t __stack2_int32_t;

    __stack0_il2c_test_target_Hoge3_reference = &local0;
    memset(__stack0_il2c_test_target_Hoge3_reference, 0x00, sizeof(il2c_test_target_Hoge3));
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 789;
    __stack0_il2c_test_target_Hoge3_reference->Value2 = __stack1_int32_t;
    __stack0_il2c_test_target_Hoge3_reference = &local0;
    __stack1_int32_t = 123;
    __stack2_int32_t = 456;
    __stack0_int32_t = il2c_test_target_Hoge3_GetValue2(__stack0_il2c_test_target_Hoge3_reference, __stack1_int32_t, __stack2_int32_t);
    local1 = __stack0_int32_t;
    __stack0_int32_t = local1;
    local2 = __stack0_int32_t;
    goto L_0000;
L_0000:
    __stack0_int32_t = local2;
    return __stack0_int32_t;
}

IL2Cプロジェクト – Extensive Xamarin

技術書典3で配布予定の”Extensive Xamarin”に、IL2Cを寄稿させて頂きました。
是非、皆さん入手して読んでください :)

範囲はおおよそ.NET Confの発表分です。ビデオシリーズではすべて網羅していますが、その中でも印象深かったトピックについて、書籍化で解説しやすい部分を抜粋してあります。
締切がギリギリでしたが掲載を快諾していただいたXamaritansのメンバーの方々に感謝します。

何故Xamarin本に寄稿したのかですが、#6-38で喋っています:

IL2C短信: milestone2

IL2Cの短信です。milestoneを決めてやっているわけではないのですが、一区切りついたのでmilestone2ということにしておこう。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project


milestone1 (#6-4)

Hello world的なシンプルコードの変換が出来る:

public static int main()
{
  var a = 1;
  var b = 2;
  var c = a + b;
  return c;
}
int main(void)
{
  int local0;
  int local1;
  int local2;
  int local3;

  local0 = 1;
  local1 = 2;
  local2 = local1 + local0;
  local3 = local2;
  return local3;
}

milestone2 (#6-16)

  • プリミティブ型のプラットフォーム依存性を回避したいので、stdint.hを使うようにする。
  • 64ビット整数の足し算のサポート(今後の方向性検証のため)。
  • スタックの高度な再利用を含むコードの変換が出来る。
    これが一番大変だった… ILの実行パス解析に型推論が必要(知識ないのでもっと直接的な手法で実装した)とか。今の方法にも積み残しがあるけど。
  • 無条件・条件ジャンプに対応。
    上が出来たので、必然的にこれも出来るようになった。
public static long main()
{
  var a = 1L;
  var b = 2L;
  var c = a + b;
  return c;
}
#include <stdint.h>

int64_t main(void)
{
  int64_t local0;
  int64_t local1;
  int64_t local2;
  int64_t local3;

  int32_t __stack0_int32_t;
  int64_t __stack0_int64_t;
  int64_t __stack1_int64_t;

  __stack0_int32_t = 1;
  __stack0_int64_t = (int64_t)__stack0_int32_t;
  local0 = __stack0_int64_t;
  __stack0_int32_t = 2;
  __stack0_int64_t = (int64_t)__stack0_int32_t;
  local1 = __stack0_int64_t;
  __stack0_int64_t = local0;
  __stack1_int64_t = local1;
  __stack0_int64_t = __stack0_int64_t + __stack1_int64_t;
  local2 = __stack0_int64_t;
  __stack0_int64_t = local2;
  local3 = __stack0_int64_t;
  goto L_000b;
L_000b:
  __stack0_int64_t = local3;
  return __stack0_int64_t;
}

この冗長なコードは、VC++のReleaseビルドで2命令に短縮されます:

ビデオネタ: IL2C

IL2Cと言うのは、Unityで言うところのIL2CPPのC言語版です。これをフルスクラッチで作ってみるというビデオシリーズです。
つまり、C#(というか、それをコンパイルしたILを含むDLLやEXE)を入力に、C言語のソースコードを生成するツール、というわけです。

YouTube: Playlist: Making archive IL2C
GitHub: IL2C step-by-step design project

現在第6回までやったところですが、以下のようなC#のコード:

public static int main()
{
  var a = 1;
  var b = 2;
  var c = a + b;
  return c;
}

が、

#include <stdint.h>
int32_t main(void)
{
  int32_t local0;
  int32_t local1;
  int32_t local2;
  int32_t local3;

  local0 = 1;
  local1 = 2;
  local2 = local1 + local0;
  local3 = local2;
  return local3;
}

というC言語のソースコードに変換できる所まで出来ました。また、これの64ビット(long, int64_t)も出来たところです。

開発の方法の検討・設計・実装・テストまで含めて、全体を一からフルスクラッチでやってるところを、ほぼカット無しで収録しています。メタプログラミングに興味のある方は視聴してみてください。

この調子で続けると、40回~50回ぐらいで完成かな… 先は長そうだ :)

あと、途中でHuman Resource Machineというゲームをちょっとだけ紹介しています。IL(というかアセンブリ言語)をゲーム感覚で学べるアプリでお薦めです。

ビデオネタ開始

こんにちは、最近ネタを楽に公開するいい方法が無いかと考えてたんですが、「そうだ、ビデオ公開すれば喋るだけだからいいんでね?」と、ちょっとおかしなことを考えたのでやってみました。掛け合い式にしたほうが面白んじゃないか(&初版出す勇気)ということで、@matsujirushi12さんと撮ってます。

YouTube: Center CLR channel
Channel9: Channel 9 author Kouji Matsui

下の埋め込みはYouTubeです。


About .NET Micro Framework in 2017

「2017年、.NET Micro Frameworkに動きはあったのか? NETMFの開発チームは2015年ごろから動きが鈍くなっていました。しかし、最近になって、NETMFのコードをフォークして移植性を高めようという動きがあります。このようなNETMFの新しい動きについて、ラフに総括してみます」


TinyCLR OS

「TinyCLR OSは、従来の環境よりも更に扱いやすくなっています。私たちはTinyCLR OSをどうやって始めれば良いか、デモも交えてディスカッションします」


Start F#

「これを見れば、F#の開発を始められる! 私たちはF#を使った開発の罠についてディスカッションしました。F#で楽しい開発をするポイントはどこにあるか?」


勉強会で初めて登壇するときに、聴衆に圧倒されたりするのって普通にあるんじゃないかと思ってるんですが(え?ない?)、「聴衆が居ない」カメラに向かって喋るというのも、一種の慣れが必要だなと思いました。まあ、喋りだせばそんなこと忘れてるんですがw それも相方のおかげですね。

ちょっとNGっぽいシーンもあったりしますがw そもそも「コンテンツを楽に公開する」のを趣旨としてるので、色々完璧にやる気は無いです(それだと意味がないので)。できるだけ「撮って出し」みたいな感じで行けるような方向で工夫をしていきます。楽屋ネタもそのうちやろうかと思うんですが、機材とか専門外なのでスムーズにやるのが難しい… そのうち知見も貯まるかな。

時間さえ取れれば、半定期的な感じで撮っていこうと考えてます。

それでは!

C#でわかる こわくないモナド – F# 勉強会 岐阜

今年久々の投稿。「C#でわかる こわくないM」というお題で、F# 勉強会 岐阜で登壇してきました。

「M」とは「モナド」のことです。内容について誰にも推敲依頼しなかったので、実際に発表するまで内容の正確性に自信がなかったのですが、大きな問題はなさそうでホッとしました。
最初に喋りましたが、これについては解説の筋道を2年ぐらい考えていました。なんとか形になってよかったのと、自分の口で説明することでより理解が深まったのが大きな収穫でした。

帰りにはもみあげさんとコレクションへのモナドの適用(要するにLINQのSelectMany)がモナドであるのか否かの話も出来たので、これも大きかったです。次はモノイドの理解とモナド・モノイドの(頭のなかでの)統合が課題かな。このネタは、Fsharp Bootcamp Tokyo 2016 with Tomas Petricekのときに出た、モナド(又はモノイド)に対するコンピュテーション式の適用での矛盾が元ネタで、この時にはまだ消化しきれなかったので、いつか噛み砕いて理解しようと思っていたことです。このあたりが理解できると、抽象化の道具がまた一つモノになったと言える気がします。今日の発表で、一歩近づいたかな。

* 内容はbleisさんのGistに書いてありますが、今見てもまだ怯んでしまうな…

それではまた。

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

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

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


.NET Coreという存在

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

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

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

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

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

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

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

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

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

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


.NET Standard(仕切り直し)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


project.json(安楽死)

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

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

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

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

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

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

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

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

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


総評

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

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

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

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

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

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

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

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

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

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

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


Expandable F# Compiler project (fscx)

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

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

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

使用者から見た構造

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

type InsertLoggingVisitor() =
  inherit AstInheritableVisitor()

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

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

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

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

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

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

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

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

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

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


構文木変形の難しさ

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


フィルタのミドルウェア

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このようなコードが:

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

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

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

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

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

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


まとめ

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

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

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

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

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

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